Skip to content

Commit 0aa1371

Browse files
committed
Saves a merge base branch defined by user
(#4224, #4258)
1 parent 07a01fe commit 0aa1371

17 files changed

+181
-19
lines changed

contributions.json

+4
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@
7171
"enablement": "gitlens:enabled && resourceScheme =~ /^(gitlens|pr)$/",
7272
"commandPalette": "!gitlens:hasVirtualFolders && gitlens:enabled && resourceScheme =~ /^(gitlens|git|pr)$/"
7373
},
74+
"gitlens.changeBranchMergeTarget": {
75+
"label": "Change Branch Merge Target",
76+
"commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders"
77+
},
7478
"gitlens.clearFileAnnotations": {
7579
"label": "Clear File Annotations",
7680
"icon": "$(gitlens-gitlens-filled)",

docs/telemetry-events.md

+8
Original file line numberDiff line numberDiff line change
@@ -1160,6 +1160,14 @@ or
11601160
}
11611161
```
11621162

1163+
### home/changeBranchMergeTarget
1164+
1165+
> Sent when the user starts defining a user-specific merge target branch
1166+
1167+
```typescript
1168+
void
1169+
```
1170+
11631171
### home/command
11641172

11651173
> Sent when a Home command is executed

package.json

+9
Original file line numberDiff line numberDiff line change
@@ -6098,6 +6098,11 @@
60986098
"icon": "$(folder-opened)",
60996099
"enablement": "gitlens:enabled && resourceScheme =~ /^(gitlens|pr)$/"
61006100
},
6101+
{
6102+
"command": "gitlens.changeBranchMergeTarget",
6103+
"title": "Change Branch Merge Target",
6104+
"category": "GitLens"
6105+
},
61016106
{
61026107
"command": "gitlens.clearFileAnnotations",
61036108
"title": "Clear File Annotations",
@@ -10383,6 +10388,10 @@
1038310388
"command": "gitlens.browseRepoBeforeRevisionInNewWindow",
1038410389
"when": "!gitlens:hasVirtualFolders && gitlens:enabled && resourceScheme =~ /^(gitlens|git|pr)$/"
1038510390
},
10391+
{
10392+
"command": "gitlens.changeBranchMergeTarget",
10393+
"when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders"
10394+
},
1038610395
{
1038710396
"command": "gitlens.clearFileAnnotations",
1038810397
"when": "resource in gitlens:tabs:blameable && (gitlens:window:annotated || resource in gitlens:tabs:annotated)"
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { Container } from '../container';
2+
import type { GitBranch } from '../git/models/branch';
3+
import type { Repository } from '../git/models/repository';
4+
import type { PartialStepState, StepGenerator } from './quickCommand';
5+
import { QuickCommand, StepResultBreak } from './quickCommand';
6+
import { pickBranchOrTagStep } from './quickCommand.steps';
7+
8+
interface Context {
9+
repos: Repository[];
10+
title: string;
11+
}
12+
13+
type State = {
14+
repo: string | Repository;
15+
branch: string;
16+
mergeBranch: string | undefined;
17+
};
18+
19+
export interface ChangeBranchMergeTargetCommandArgs {
20+
readonly command: 'changeBranchMergeTarget';
21+
state?: Partial<State>;
22+
}
23+
24+
export class ChangeBranchMergeTargetCommand extends QuickCommand {
25+
constructor(container: Container, args?: ChangeBranchMergeTargetCommandArgs) {
26+
super(container, 'changeBranchMergeTarget', 'changeBranchMergeTarget', 'Change Merge Target', {
27+
description: 'Change Merge Target for a branch',
28+
});
29+
this.initialState = {
30+
counter: 0,
31+
...args?.state,
32+
};
33+
}
34+
35+
protected async *steps(state: PartialStepState<State>): StepGenerator {
36+
const context: Context = {
37+
repos: this.container.git.openRepositories,
38+
title: this.title,
39+
};
40+
const repository = typeof state.repo === 'string' ? this.container.git.getRepository(state.repo) : state.repo;
41+
if (repository) {
42+
const result = yield* pickBranchOrTagStep({ counter: 0, repo: repository }, context, {
43+
picked: state.mergeBranch,
44+
placeholder: 'Pick a merge target branch',
45+
value: undefined,
46+
filter: {
47+
branches: (branch: GitBranch) => branch.remote && branch.name !== state.branch,
48+
tags: () => false,
49+
},
50+
});
51+
if (result === StepResultBreak) {
52+
return;
53+
}
54+
const ref = await this.container.git.branches(repository.path).getBranch(state.branch);
55+
if (ref && result && state.branch) {
56+
await this.container.git
57+
.branches(repository.path)
58+
.setUserMergeTargetBranchName?.(state.branch, result.name);
59+
}
60+
}
61+
62+
await Promise.resolve(true);
63+
}
64+
}

src/commands/quickWizard.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,26 @@ import type { Container } from '../container';
22
import type { LaunchpadCommandArgs } from '../plus/launchpad/launchpad';
33
import type { AssociateIssueWithBranchCommandArgs, StartWorkCommandArgs } from '../plus/startWork/startWork';
44
import { command } from '../system/-webview/command';
5+
import type { ChangeBranchMergeTargetCommandArgs } from './changeBranchMergeTarget';
56
import type { CommandContext } from './commandContext';
67
import type { QuickWizardCommandArgsWithCompletion } from './quickWizard.base';
78
import { QuickWizardCommandBase } from './quickWizard.base';
89

9-
export type QuickWizardCommandArgs = LaunchpadCommandArgs | StartWorkCommandArgs | AssociateIssueWithBranchCommandArgs;
10+
export type QuickWizardCommandArgs =
11+
| LaunchpadCommandArgs
12+
| StartWorkCommandArgs
13+
| AssociateIssueWithBranchCommandArgs
14+
| ChangeBranchMergeTargetCommandArgs;
1015

1116
@command()
1217
export class QuickWizardCommand extends QuickWizardCommandBase {
1318
constructor(container: Container) {
14-
super(container, ['gitlens.showLaunchpad', 'gitlens.startWork', 'gitlens.associateIssueWithBranch']);
19+
super(container, [
20+
'gitlens.showLaunchpad',
21+
'gitlens.startWork',
22+
'gitlens.associateIssueWithBranch',
23+
'gitlens.changeBranchMergeTarget',
24+
]);
1525
}
1626

1727
protected override preExecute(context: CommandContext, args?: QuickWizardCommandArgsWithCompletion): Promise<void> {
@@ -25,6 +35,9 @@ export class QuickWizardCommand extends QuickWizardCommandBase {
2535
case 'gitlens.associateIssueWithBranch':
2636
return this.execute({ command: 'associateIssueWithBranch', ...args });
2737

38+
case 'gitlens.changeBranchMergeTarget':
39+
return this.execute({ command: 'changeBranchMergeTarget', ...args });
40+
2841
default:
2942
return this.execute(args);
3043
}

src/commands/quickWizard.utils.ts

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { LaunchpadCommand } from '../plus/launchpad/launchpad';
55
import { AssociateIssueWithBranchCommand, StartWorkCommand } from '../plus/startWork/startWork';
66
import { configuration } from '../system/-webview/configuration';
77
import { getContext } from '../system/-webview/context';
8+
import { ChangeBranchMergeTargetCommand } from './changeBranchMergeTarget';
89
import { BranchGitCommand } from './git/branch';
910
import { CherryPickGitCommand } from './git/cherry-pick';
1011
import { CoAuthorsGitCommand } from './git/coauthors';
@@ -122,6 +123,10 @@ export class QuickWizardRootStep implements QuickPickStep<QuickCommand> {
122123
if (args?.command === 'associateIssueWithBranch') {
123124
this.hiddenItems.push(new AssociateIssueWithBranchCommand(container, args));
124125
}
126+
127+
if (args?.command === 'changeBranchMergeTarget') {
128+
this.hiddenItems.push(new ChangeBranchMergeTargetCommand(container, args));
129+
}
125130
}
126131

127132
private _command: QuickCommand | undefined;

src/constants.commands.generated.ts

+1
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,7 @@ export type ContributedPaletteCommands =
619619
| 'gitlens.browseRepoAtRevisionInNewWindow'
620620
| 'gitlens.browseRepoBeforeRevision'
621621
| 'gitlens.browseRepoBeforeRevisionInNewWindow'
622+
| 'gitlens.changeBranchMergeTarget'
622623
| 'gitlens.clearFileAnnotations'
623624
| 'gitlens.closeUnchangedFiles'
624625
| 'gitlens.compareHeadWith'

src/constants.commands.ts

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type InternalGraphWebviewCommands =
3333
| 'gitlens.graph.skipPausedOperation';
3434

3535
type InternalHomeWebviewCommands =
36+
| 'gitlens.home.changeBranchMergeTarget'
3637
| 'gitlens.home.deleteBranchOrWorktree'
3738
| 'gitlens.home.pushBranch'
3839
| 'gitlens.home.openMergeTargetComparison'
@@ -103,6 +104,7 @@ type InternalWalkthroughCommands =
103104

104105
type InternalGlCommands =
105106
| `gitlens.action.${string}`
107+
| 'gitlens.changeBranchMergeTarget'
106108
| 'gitlens.diffWith'
107109
| 'gitlens.openOnRemote'
108110
| 'gitlens.openWalkthrough'

src/constants.telemetry.ts

+2
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ export interface TelemetryEvents extends WebviewShowAbortedEvents, WebviewShownE
157157
'home/createBranch': void;
158158
/** Sent when the user chooses to start work on an issue from the home view */
159159
'home/startWork': void;
160+
/** Sent when the user starts defining a user-specific merge target branch */
161+
'home/changeBranchMergeTarget': void;
160162

161163
/** Sent when the user takes an action on the Launchpad title bar */
162164
'launchpad/title/action': LaunchpadTitleActionEvent;

src/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const enum CharCode {
4949

5050
export type GitConfigKeys =
5151
| `branch.${string}.${'gk' | 'vscode'}-merge-base`
52+
| `branch.${string}.gk-user-merge-target`
5253
| `branch.${string}.gk-target-base`
5354
| `branch.${string}.gk-associated-issues`
5455
| `branch.${string}.github-pr-owner-number`;

src/env/node/git/sub-providers/branches.ts

+13
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,19 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
627627
await this.provider.config.setConfig(repoPath, mergeBaseConfigKey, base);
628628
}
629629

630+
@log()
631+
async getUserMergeTargetBranchName(repoPath: string, ref: string): Promise<string | undefined> {
632+
const mergeTargetConfigKey: GitConfigKeys = `branch.${ref}.gk-user-merge-target`;
633+
const target = await this.provider.config.getConfig(repoPath, mergeTargetConfigKey);
634+
return target?.trim() || undefined;
635+
}
636+
637+
@log()
638+
async setUserMergeTargetBranchName(repoPath: string, ref: string, target: string | undefined): Promise<void> {
639+
const mergeTargetConfigKey: GitConfigKeys = `branch.${ref}.gk-user-merge-target`;
640+
await this.provider.config.setConfig(repoPath, mergeTargetConfigKey, target);
641+
}
642+
630643
private async getBaseBranchFromReflog(
631644
repoPath: string,
632645
ref: string,

src/git/gitProvider.ts

+2
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ export interface GitBranchesSubProvider {
258258
setBaseBranchName?(repoPath: string, ref: string, base: string): Promise<void>;
259259
getTargetBranchName?(repoPath: string, ref: string): Promise<string | undefined>;
260260
setTargetBranchName?(repoPath: string, ref: string, target: string): Promise<void>;
261+
getUserMergeTargetBranchName?(repoPath: string, ref: string): Promise<string | undefined>;
262+
setUserMergeTargetBranchName?(repoPath: string, ref: string, target: string): Promise<void>;
261263
renameBranch?(repoPath: string, oldName: string, newName: string): Promise<void>;
262264
}
263265

src/git/models/branch.ts

+1
Original file line numberDiff line numberDiff line change
@@ -227,4 +227,5 @@ export interface BranchTargetInfo {
227227
baseBranch: string | undefined;
228228
defaultBranch: string | undefined;
229229
targetBranch: MaybePausedResult<string | undefined>;
230+
userTargetBranch: string | undefined;
230231
}

src/git/utils/-webview/branch.utils.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,26 @@ export async function getBranchTargetInfo(
1414
timeout?: number;
1515
},
1616
): Promise<BranchTargetInfo> {
17-
const [baseResult, defaultResult, targetResult] = await Promise.allSettled([
17+
const [baseResult, defaultResult, targetResult, userTargetResult] = await Promise.allSettled([
1818
container.git.branches(branch.repoPath).getBaseBranchName?.(branch.name),
1919
getDefaultBranchName(container, branch.repoPath, branch.getRemoteName()),
2020
getTargetBranchName(container, branch, {
2121
cancellation: options?.cancellation,
2222
timeout: options?.timeout,
2323
}),
24+
container.git.branches(branch.repoPath).getUserMergeTargetBranchName?.(branch.name),
2425
]);
2526

2627
const baseBranchName = getSettledValue(baseResult);
2728
const defaultBranchName = getSettledValue(defaultResult);
2829
const targetMaybeResult = getSettledValue(targetResult);
30+
const userTargetBranchName = getSettledValue(userTargetResult);
2931

3032
return {
3133
baseBranch: baseBranchName,
3234
defaultBranch: defaultBranchName,
3335
targetBranch: targetMaybeResult ?? { value: undefined, paused: false },
36+
userTargetBranch: userTargetBranchName,
3437
};
3538
}
3639

src/quickpicks/comparisonPicker.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,14 @@ export async function showComparisonPicker(
8181
const branch = await repo?.git.branches().getBranch(head.name);
8282
if (branch != null) {
8383
const info = await getBranchTargetInfo(container, branch);
84-
const target = info.targetBranch.paused
85-
? info.baseBranch
86-
: info.targetBranch.value ?? info.defaultBranch;
84+
let target;
85+
if (info.userTargetBranch) {
86+
target = info.userTargetBranch;
87+
} else if (!info.targetBranch.paused && info.targetBranch.value) {
88+
target = info.targetBranch.value;
89+
} else {
90+
target = info.baseBranch ?? info.defaultBranch;
91+
}
8792
if (target != null) {
8893
base = createReference(target, repoPath, { refType: 'revision' });
8994
}

src/webviews/apps/plus/home/components/merge-target-status.ts

+24-12
Original file line numberDiff line numberDiff line change
@@ -477,18 +477,30 @@ export class GlMergeTargetStatus extends LitElement {
477477
return html`<span class="header__actions"
478478
>${branchRef && targetRef
479479
? html`<gl-button
480-
href="${createCommandLink<BranchAndTargetRefs>('gitlens.home.openMergeTargetComparison', {
481-
...branchRef,
482-
mergeTargetId: targetRef.branchId,
483-
mergeTargetName: targetRef.branchName,
484-
})}"
485-
appearance="toolbar"
486-
><code-icon icon="git-compare"></code-icon>
487-
<span slot="tooltip"
488-
>Compare Branch with Merge Target<br />${renderBranchName(this.branch.name)}
489-
&leftrightarrow; ${renderBranchName(this.target?.name)}</span
490-
>
491-
</gl-button>`
480+
href="${createCommandLink<BranchAndTargetRefs>('gitlens.home.changeBranchMergeTarget', {
481+
...branchRef,
482+
mergeTargetId: targetRef.branchId,
483+
mergeTargetName: targetRef.branchName,
484+
})}"
485+
appearance="toolbar"
486+
><code-icon icon="pencil"></code-icon
487+
><span slot="tooltip"
488+
>Edit Merge Target<br />${renderBranchName(this.branch.name)} &leftrightarrow;
489+
${renderBranchName(this.target?.name)}</span
490+
></gl-button
491+
><gl-button
492+
href="${createCommandLink<BranchAndTargetRefs>('gitlens.home.openMergeTargetComparison', {
493+
...branchRef,
494+
mergeTargetId: targetRef.branchId,
495+
mergeTargetName: targetRef.branchName,
496+
})}"
497+
appearance="toolbar"
498+
><code-icon icon="git-compare"></code-icon>
499+
<span slot="tooltip"
500+
>Compare Branch with Merge Target<br />${renderBranchName(this.branch.name)}
501+
&leftrightarrow; ${renderBranchName(this.target?.name)}</span
502+
>
503+
</gl-button>`
492504
: nothing}<gl-button
493505
href="${createCommandLink('gitlens.home.fetch', this.targetBranchRef)}"
494506
appearance="toolbar"

src/webviews/home/homeWebview.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ActionRunnerType } from '../../api/actionRunners';
44
import type { CreatePullRequestActionContext } from '../../api/gitlens';
55
import type { EnrichedAutolink } from '../../autolinks/models/autolinks';
66
import { getAvatarUriFromGravatarEmail } from '../../avatars';
7+
import type { ChangeBranchMergeTargetCommandArgs } from '../../commands/changeBranchMergeTarget';
78
import type { BranchGitCommandArgs } from '../../commands/git/branch';
89
import type { OpenPullRequestOnRemoteCommandArgs } from '../../commands/openPullRequestOnRemote';
910
import { GlyphChars, urls } from '../../constants';
@@ -333,6 +334,7 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
333334
(src?: Source) => this.container.subscription.validate({ force: true }, src),
334335
this,
335336
),
337+
registerCommand('gitlens.home.changeBranchMergeTarget', this.changeBranchMergeTarget, this),
336338
registerCommand('gitlens.home.deleteBranchOrWorktree', this.deleteBranchOrWorktree, this),
337339
registerCommand('gitlens.home.pushBranch', this.pushBranch, this),
338340
registerCommand('gitlens.home.openMergeTargetComparison', this.mergeTargetCompare, this),
@@ -490,6 +492,19 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
490492
});
491493
}
492494

495+
@log<HomeWebviewProvider['changeBranchMergeTarget']>()
496+
private changeBranchMergeTarget(ref: BranchAndTargetRefs) {
497+
this.container.telemetry.sendEvent('home/changeBranchMergeTarget');
498+
void executeCommand<ChangeBranchMergeTargetCommandArgs>('gitlens.changeBranchMergeTarget', {
499+
command: 'changeBranchMergeTarget',
500+
state: {
501+
repo: ref.repoPath,
502+
branch: ref.branchName,
503+
mergeBranch: ref.mergeTargetName,
504+
},
505+
});
506+
}
507+
493508
@log<HomeWebviewProvider['mergeIntoCurrent']>({ args: { 0: r => r.branchId } })
494509
private async mergeIntoCurrent(ref: BranchRef) {
495510
const { repo, branch } = await this.getRepoInfoFromRef(ref);
@@ -1682,7 +1697,9 @@ async function getBranchMergeTargetStatusInfo(
16821697
});
16831698

16841699
let targetResult;
1685-
if (!info.targetBranch.paused && info.targetBranch.value) {
1700+
if (info.userTargetBranch) {
1701+
targetResult = info.userTargetBranch;
1702+
} else if (!info.targetBranch.paused && info.targetBranch.value) {
16861703
targetResult = info.targetBranch.value;
16871704
}
16881705

0 commit comments

Comments
 (0)