Skip to content

Commit

Permalink
Associates issues with branches in git config (#3871)
Browse files Browse the repository at this point in the history
  • Loading branch information
axosoft-ramint authored Dec 17, 2024
1 parent 99e9b87 commit a786544
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 22 deletions.
16 changes: 15 additions & 1 deletion src/commands/git/branch.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { QuickInputButtons } from 'vscode';
import type { Container } from '../../container';
import { getNameWithoutRemote } from '../../git/models/branch.utils';
import { addAssociatedIssueToBranch, getNameWithoutRemote } from '../../git/models/branch.utils';
import type { IssueShape } from '../../git/models/issue';
import type { GitBranchReference, GitReference } from '../../git/models/reference';
import { getReferenceLabel, isBranchReference, isRevisionReference } from '../../git/models/reference.utils';
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 @@ -61,6 +63,7 @@ interface CreateState {
suggestNameOnly?: boolean;
suggestRepoOnly?: boolean;
confirmOptions?: CreateFlags[];
associateWithIssue?: IssueShape;
}

function isCreateState(state: Partial<State> | undefined): state is Partial<CreateState> {
Expand Down Expand Up @@ -415,6 +418,7 @@ export class BranchGitCommand extends QuickCommand {
}

endSteps(state);

if (state.flags.includes('--switch')) {
await state.repo.switch(state.reference.ref, { createBranch: state.name });
} else {
Expand All @@ -426,6 +430,16 @@ export class BranchGitCommand extends QuickCommand {
return showGenericErrorMessage('Unable to create branch');
}
}

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 owner = getIssueOwner(issue);
if (branch != null && owner != null) {
await addAssociatedIssueToBranch(this.container, branch, { ...issue, type: 'issue' }, owner);
}
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const enum CharCode {
export type GitConfigKeys =
| `branch.${string}.${'gk' | 'vscode'}-merge-base`
| `branch.${string}.gk-target-base`
| `branch.${string}.gk-associated-issues`
| `branch.${string}.github-pr-owner-number`;

export const enum GlyphChars {
Expand Down
3 changes: 2 additions & 1 deletion src/git/models/branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
getBranchTrackingWithoutRemote,
getRemoteNameFromBranchName,
getRemoteNameSlashIndex,
isDetachedHead } from './branch.utils';
isDetachedHead,
} from './branch.utils';
import type { PullRequest, PullRequestState } from './pullRequest';
import type { GitBranchReference } from './reference';
import type { GitRemote } from './remote';
Expand Down
108 changes: 108 additions & 0 deletions src/git/models/branch.utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import type { CancellationToken } from 'vscode';
import type { GitConfigKeys } from '../../constants';
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 { getSettledValue, pauseOnCancelOrTimeout } from '../../system/promise';
import type { GitBranch } from './branch';
import type { Issue } from './issue';
import type { PullRequest } from './pullRequest';
import type { GitBranchReference, GitReference } from './reference';
import type { Repository } from './repository';
Expand Down Expand Up @@ -208,3 +217,102 @@ export function getNameWithoutRemote(ref: GitReference) {
}
return ref.name;
}

export async function getAssociatedIssuesForBranch(
container: Container,
branch: GitBranch,
options?: {
cancellation?: CancellationToken;
timeout?: number;
},
): Promise<MaybePausedResult<Issue[] | undefined>> {
const { encoded } = await getConfigKeyAndEncodedAssociatedIssuesForBranch(container, branch);
if (options?.cancellation?.isCancellationRequested) return { value: undefined, paused: false };

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

if (associatedIssues != null) {
return pauseOnCancelOrTimeout(
(async () => {
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,
);
}
}

return { value: undefined, paused: false };
}

export async function addAssociatedIssueToBranch(
container: Container,
branch: GitBranchReference,
issue: Issue,
owner: RepositoryDescriptor | IssueResourceDescriptor,
options?: {
cancellation?: CancellationToken;
},
) {
const { key, encoded } = await getConfigKeyAndEncodedAssociatedIssuesForBranch(container, branch);
if (options?.cancellation?.isCancellationRequested) return;
try {
const associatedIssues: GitConfigEntityIdentifier[] = encoded
? (JSON.parse(encoded) as GitConfigEntityIdentifier[])
: [];
if (associatedIssues.some(i => i.entityId === issue.nodeId)) {
return;
}
associatedIssues.push(encodeIssueOrPullRequestForGitConfig(issue, owner));
await container.git.setConfig(branch.repoPath, key, JSON.stringify(associatedIssues));
} catch (ex) {
Logger.error(ex, 'addAssociatedIssueToBranch');
}
}

export async function removeAssociatedIssueFromBranch(
container: Container,
branch: GitBranchReference,
id: string,
options?: {
cancellation?: CancellationToken;
},
) {
const { key, encoded } = await getConfigKeyAndEncodedAssociatedIssuesForBranch(container, branch);
if (options?.cancellation?.isCancellationRequested) return;
try {
let associatedIssues: GitConfigEntityIdentifier[] = encoded
? (JSON.parse(encoded) as GitConfigEntityIdentifier[])
: [];
associatedIssues = associatedIssues.filter(i => i.entityId !== id);
if (associatedIssues.length === 0) {
await container.git.setConfig(branch.repoPath, key, undefined);
} else {
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 };
}
9 changes: 9 additions & 0 deletions src/plus/integrations/providers/models.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
Account,
ActionablePullRequest,
AnyEntityIdentifierInput,
AzureDevOps,
AzureOrganization,
AzureProject,
Expand Down Expand Up @@ -891,3 +892,11 @@ export type EnrichablePullRequest = ProviderPullRequest & {
};

export const getActionablePullRequests = GitProviderUtils.getActionablePullRequests;

export type GitConfigEntityIdentifier = AnyEntityIdentifierInput & {
metadata: {
id: string;
owner: { key: string; name: string; id: string | undefined; owner: string | undefined };
createdDate: string;
};
};
Loading

0 comments on commit a786544

Please sign in to comment.