Skip to content

Commit

Permalink
Refinements
Browse files Browse the repository at this point in the history
  • Loading branch information
axosoft-ramint committed Dec 17, 2024
1 parent b8d2892 commit 0eeb7a1
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 70 deletions.
21 changes: 7 additions & 14 deletions src/commands/git/branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Repository } from '../../git/models/repository';
import type { GitWorktree } from '../../git/models/worktree';
import { getWorktreesByBranch } from '../../git/models/worktree.utils';
import { showGenericErrorMessage } from '../../messages';
import { getIssueOwner } from '../../plus/integrations/providers/utils';
import type { QuickPickItemOfT } from '../../quickpicks/items/common';
import { createQuickPickSeparator } from '../../quickpicks/items/common';
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags';
Expand Down Expand Up @@ -62,7 +63,7 @@ interface CreateState {
suggestNameOnly?: boolean;
suggestRepoOnly?: boolean;
confirmOptions?: CreateFlags[];
withIssue?: IssueShape;
associateWithIssue?: IssueShape;
}

function isCreateState(state: Partial<State> | undefined): state is Partial<CreateState> {
Expand Down Expand Up @@ -430,21 +431,13 @@ export class BranchGitCommand extends QuickCommand {
}
}

if (state.withIssue != null) {
const issue = state.withIssue;
if (state.associateWithIssue != null) {
const issue = state.associateWithIssue;
const branch = await state.repo.git.getBranch(state.name);
// TODO: These descriptors are hacked in. Use an integration function to get the right resource for the issue.
const resource = issue.repository
? {
key: `${issue.repository.owner}/${issue.repository.repo}`,
owner: issue.repository.owner,
name: issue.repository.repo,
}
: issue.project
? { key: issue.project.resourceId, id: issue.project.resourceId, name: issue.project.resourceId }
: undefined;
if (branch != null && resource != null) {
await addAssociatedIssueToBranch(this.container, branch, { ...issue, type: 'issue' }, resource);
const owner = getIssueOwner(issue);
if (branch != null && owner != null) {
await addAssociatedIssueToBranch(this.container, branch, { ...issue, type: 'issue' }, owner);
}
}
}
Expand Down
67 changes: 33 additions & 34 deletions src/git/models/branch.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import type { Container } from '../../container';
import type { IssueResourceDescriptor, RepositoryDescriptor } from '../../plus/integrations/integration';
import type { GitConfigEntityIdentifier } from '../../plus/integrations/providers/models';
import {
decodeEntityIdentifiersFromGitConfig,
encodeIssueOrPullRequestForGitConfig,
getIssueFromGitConfigEntityIdentifier,
} from '../../plus/integrations/providers/utils';
import { Logger } from '../../system/logger';
import { PageableResult } from '../../system/paging';
import type { MaybePausedResult } from '../../system/promise';
import { pauseOnCancelOrTimeout } from '../../system/promise';
import { getSettledValue, pauseOnCancelOrTimeout } from '../../system/promise';
import type { GitBranch } from './branch';
import type { Issue } from './issue';
import type { PullRequest } from './pullRequest';
Expand Down Expand Up @@ -189,36 +190,28 @@ export async function getAssociatedIssuesForBranch(
timeout?: number;
},
): Promise<MaybePausedResult<Issue[] | undefined>> {
const associatedIssuesConfigKey: GitConfigKeys = `branch.${branch.name}.gk-associated-issues`;
// Associated issues encoded as a string array of stringified JSON objects
const associatedIssuesEncoded = await container.git.getConfig(branch.repoPath, associatedIssuesConfigKey);
const { encoded } = await getConfigKeyAndEncodedAssociatedIssuesForBranch(container, branch);
if (options?.cancellation?.isCancellationRequested) return { value: undefined, paused: false };

let associatedIssues: GitConfigEntityIdentifier[] | undefined;
if (associatedIssuesEncoded != null) {
if (encoded != null) {
try {
associatedIssues = JSON.parse(associatedIssuesEncoded) as GitConfigEntityIdentifier[];
associatedIssues = decodeEntityIdentifiersFromGitConfig(encoded);
} catch (ex) {
Logger.error(ex, 'getAssociatedIssuesForBranch');
return { value: undefined, paused: false };
}

if (options?.cancellation?.isCancellationRequested) return { value: undefined, paused: false };
if (associatedIssues != null) {
return pauseOnCancelOrTimeout(
(async () => {
const output = [];
for (const issueDecoded of associatedIssues) {
try {
const issue = await getIssueFromGitConfigEntityIdentifier(container, issueDecoded);
if (issue != null) {
output.push(issue);
}
} catch (ex) {
Logger.error(ex, 'getAssociatedIssuesForBranch');
}
}
return output;
return (
await Promise.allSettled(
(associatedIssues ?? []).map(i => getIssueFromGitConfigEntityIdentifier(container, i)),
)
)
.map(r => getSettledValue(r))
.filter((i): i is Issue => i != null);
})(),
options?.cancellation,
options?.timeout,
Expand All @@ -233,23 +226,22 @@ export async function addAssociatedIssueToBranch(
container: Container,
branch: GitBranchReference,
issue: Issue,
resource: RepositoryDescriptor | IssueResourceDescriptor,
owner: RepositoryDescriptor | IssueResourceDescriptor,
options?: {
cancellation?: CancellationToken;
},
) {
const associatedIssuesConfigKey: GitConfigKeys = `branch.${branch.name}.gk-associated-issues`;
const associatedIssuesEncoded = await container.git.getConfig(branch.repoPath, associatedIssuesConfigKey);
const { key, encoded } = await getConfigKeyAndEncodedAssociatedIssuesForBranch(container, branch);
if (options?.cancellation?.isCancellationRequested) return;
try {
const associatedIssues: GitConfigEntityIdentifier[] = associatedIssuesEncoded
? (JSON.parse(associatedIssuesEncoded) as GitConfigEntityIdentifier[])
const associatedIssues: GitConfigEntityIdentifier[] = encoded
? (JSON.parse(encoded) as GitConfigEntityIdentifier[])
: [];
if (options?.cancellation?.isCancellationRequested || associatedIssues.some(i => i.entityId === issue.nodeId)) {
if (associatedIssues.some(i => i.entityId === issue.nodeId)) {
return;
}
associatedIssues.push(encodeIssueOrPullRequestForGitConfig(issue, resource));
await container.git.setConfig(branch.repoPath, associatedIssuesConfigKey, JSON.stringify(associatedIssues));
associatedIssues.push(encodeIssueOrPullRequestForGitConfig(issue, owner));
await container.git.setConfig(branch.repoPath, key, JSON.stringify(associatedIssues));
} catch (ex) {
Logger.error(ex, 'addAssociatedIssueToBranch');
}
Expand All @@ -263,21 +255,28 @@ export async function removeAssociatedIssueFromBranch(
cancellation?: CancellationToken;
},
) {
const associatedIssuesConfigKey: GitConfigKeys = `branch.${branch.name}.gk-associated-issues`;
const associatedIssuesEncoded = await container.git.getConfig(branch.repoPath, associatedIssuesConfigKey);
const { key, encoded } = await getConfigKeyAndEncodedAssociatedIssuesForBranch(container, branch);
if (options?.cancellation?.isCancellationRequested) return;
try {
let associatedIssues: GitConfigEntityIdentifier[] = associatedIssuesEncoded
? (JSON.parse(associatedIssuesEncoded) as GitConfigEntityIdentifier[])
let associatedIssues: GitConfigEntityIdentifier[] = encoded
? (JSON.parse(encoded) as GitConfigEntityIdentifier[])
: [];
if (options?.cancellation?.isCancellationRequested) return;
associatedIssues = associatedIssues.filter(i => i.entityId !== id);
if (associatedIssues.length === 0) {
await container.git.setConfig(branch.repoPath, associatedIssuesConfigKey, undefined);
await container.git.setConfig(branch.repoPath, key, undefined);
} else {
await container.git.setConfig(branch.repoPath, associatedIssuesConfigKey, JSON.stringify(associatedIssues));
await container.git.setConfig(branch.repoPath, key, JSON.stringify(associatedIssues));
}
} catch (ex) {
Logger.error(ex, 'removeAssociatedIssueFromBranch');
}
}

async function getConfigKeyAndEncodedAssociatedIssuesForBranch(
container: Container,
branch: GitBranchReference,
): Promise<{ key: GitConfigKeys; encoded: string | undefined }> {
const key = `branch.${branch.name}.gk-associated-issues` satisfies GitConfigKeys;
const encoded = await container.git.getConfig(branch.repoPath, key);
return { key: key, encoded: encoded };
}
72 changes: 51 additions & 21 deletions src/plus/integrations/providers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { EntityIdentifierProviderType, EntityType, EntityVersion } from '@gitkra
import type { IntegrationId } from '../../../constants.integrations';
import { HostingIntegrationId, IssueIntegrationId, SelfHostedIntegrationId } from '../../../constants.integrations';
import type { Container } from '../../../container';
import type { Issue, IssueOrPullRequest } from '../../../git/models/issue';
import type { Issue, IssueOrPullRequest, IssueShape } from '../../../git/models/issue';
import type { PullRequest } from '../../../git/models/pullRequest';
import { Logger } from '../../../system/logger';
import { equalsIgnoreCase } from '../../../system/string';
import type { LaunchpadItem } from '../../launchpad/launchpadProvider';
import type { IssueResourceDescriptor, RepositoryDescriptor } from '../integration';
Expand Down Expand Up @@ -102,27 +103,27 @@ function fromStringToEntityIdentifierProviderType(str: string): EntityIdentifier

export function encodeIssueOrPullRequestForGitConfig(
entity: Issue | PullRequest,
resource: RepositoryDescriptor | IssueResourceDescriptor,
owner: RepositoryDescriptor | IssueResourceDescriptor,
): GitConfigEntityIdentifier {
const encodedResource: GitConfigEntityIdentifier['metadata']['owner'] = {
key: resource.key,
name: resource.name,
const encodedOwner: GitConfigEntityIdentifier['metadata']['owner'] = {
key: owner.key,
name: owner.name,
id: undefined,
owner: undefined,
};
if (isRepositoryDescriptor(resource)) {
encodedResource.owner = resource.owner;
} else if (isIssueResourceDescriptor(resource)) {
encodedResource.id = resource.id;
if (isRepositoryDescriptor(owner)) {
encodedOwner.owner = owner.owner;
} else if (isIssueResourceDescriptor(owner)) {
encodedOwner.id = owner.id;
} else {
throw new Error('Invalid resource');
throw new Error('Invalid owner');
}

return {
...getEntityIdentifierInput(entity),
metadata: {
id: entity.id,
owner: encodedResource,
owner: encodedOwner,
createdDate: new Date().toISOString(),
},
};
Expand All @@ -140,23 +141,40 @@ export function isGitConfigEntityIdentifier(entity: unknown): entity is GitConfi
entity.version != null &&
'entityId' in entity &&
entity.entityId != null &&
'searchId' in entity &&
entity.searchId != null
'metadata' in entity &&
entity.metadata != null &&
typeof entity.metadata === 'object' &&
'id' in entity.metadata &&
entity.metadata.id != null &&
'owner' in entity.metadata &&
entity.metadata.owner != null &&
'createdDate' in entity.metadata &&
entity.metadata.createdDate != null
);
}

export function decodeEntityFromGitConfig(str: string): GitConfigEntityIdentifier {
export function isGitConfigEntityIdentifiers(entities: unknown): entities is GitConfigEntityIdentifier[] {
return Array.isArray(entities) && entities.every(entity => isGitConfigEntityIdentifier(entity));
}

export function decodeEntityIdentifiersFromGitConfig(str: string): GitConfigEntityIdentifier[] {
const decoded = JSON.parse(str);

if (!isGitConfigEntityIdentifier(decoded)) {
throw new Error('Invalid issue or pull request');
if (!isGitConfigEntityIdentifiers(decoded)) {
debugger;
Logger.error('Invalid entity identifiers in git config');
return [];
}

if (
decoded.provider === EntityIdentifierProviderType.Jira &&
(decoded.resourceId == null || decoded.projectId == null)
) {
throw new Error('Invalid Jira issue');
for (const decodedEntity of decoded) {
if (
decodedEntity.provider === EntityIdentifierProviderType.Jira &&
(decodedEntity.resourceId == null || decodedEntity.projectId == null)
) {
debugger;
Logger.error('Invalid Jira issue in git config');
continue;
}
}

return decoded;
Expand Down Expand Up @@ -199,3 +217,15 @@ export async function getIssueFromGitConfigEntityIdentifier(
identifier.metadata.id,
);
}

export function getIssueOwner(issue: IssueShape): RepositoryDescriptor | IssueResourceDescriptor | undefined {
return issue.repository
? {
key: `${issue.repository.owner}/${issue.repository.repo}`,
owner: issue.repository.owner,
name: issue.repository.repo,
}
: issue.project
? { key: issue.project.resourceId, id: issue.project.resourceId, name: issue.project.resourceId }
: undefined;
}
2 changes: 1 addition & 1 deletion src/plus/startWork/startWork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export class StartWorkCommand extends QuickCommand<State> {
suggestNameOnly: true,
suggestRepoOnly: true,
confirmOptions: ['--switch', '--worktree'],
withIssue: issue,
associateWithIssue: issue,
},
},
this.pickedVia,
Expand Down

0 comments on commit 0eeb7a1

Please sign in to comment.