Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions contributions.json
Original file line number Diff line number Diff line change
Expand Up @@ -4265,6 +4265,36 @@
]
}
},
"gitlens.recomposeBranch": {
"label": "Recompose Commits (Preview)...",
"commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
},
"gitlens.recomposeBranch:graph": {
"label": "Recompose Commits (Preview)",
"icon": "$(sparkle)",
"menus": {
"webview/context": [
{
"when": "webviewItem =~ /gitlens:branch\\b(?!.*?\\b\\+remote\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_ai",
"order": 15
}
]
}
},
"gitlens.recomposeBranch:views": {
"label": "Recompose Commits (Preview)",
"icon": "$(sparkle)",
"menus": {
"view/item/context": [
{
"when": "viewItem =~ /gitlens:branch\\b(?!.*?\\b\\+remote\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_ai",
"order": 15
}
]
}
},
"gitlens.regenerateMarkdownDocument": {
"label": "Regenerate",
"icon": "$(refresh)",
Expand Down
37 changes: 37 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7863,6 +7863,21 @@
"command": "gitlens.quickOpenFileHistory:graphDetails",
"title": "Quick Open File History"
},
{
"command": "gitlens.recomposeBranch",
"title": "Recompose Commits (Preview)...",
"category": "GitLens"
},
{
"command": "gitlens.recomposeBranch:graph",
"title": "Recompose Commits (Preview)",
"icon": "$(sparkle)"
},
{
"command": "gitlens.recomposeBranch:views",
"title": "Recompose Commits (Preview)",
"icon": "$(sparkle)"
},
{
"command": "gitlens.regenerateMarkdownDocument",
"title": "Regenerate",
Expand Down Expand Up @@ -12584,6 +12599,18 @@
"command": "gitlens.quickOpenFileHistory:graphDetails",
"when": "false"
},
{
"command": "gitlens.recomposeBranch",
"when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
},
{
"command": "gitlens.recomposeBranch:graph",
"when": "false"
},
{
"command": "gitlens.recomposeBranch:views",
"when": "false"
},
{
"command": "gitlens.regenerateMarkdownDocument",
"when": "false"
Expand Down Expand Up @@ -18164,6 +18191,11 @@
"when": "viewItem =~ /gitlens:branch\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_ai@10"
},
{
"command": "gitlens.recomposeBranch:views",
"when": "viewItem =~ /gitlens:branch\\b(?!.*?\\b\\+remote\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_ai@15"
},
{
"command": "gitlens.views.openBranchOnRemote",
"when": "viewItem =~ /gitlens:branch\\b(?=.*?\\b\\+(tracking|remote)\\b)/ && !listMultiSelection",
Expand Down Expand Up @@ -24031,6 +24063,11 @@
"when": "webviewItem =~ /gitlens:branch\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_ai@10"
},
{
"command": "gitlens.recomposeBranch:graph",
"when": "webviewItem =~ /gitlens:branch\\b(?!.*?\\b\\+remote\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_ai@15"
},
{
"command": "gitlens.graph.openBranchOnRemote",
"when": "webviewItem =~ /gitlens:branch\\b(?=.*?\\b\\+(tracking|remote)\\b)/ && gitlens:repos:withRemotes",
Expand Down
1 change: 1 addition & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import './commands/openWorkingFile';
import './commands/patches';
import './commands/quickWizard';
import './commands/rebaseEditor';
import './commands/recomposeBranch';
import './commands/refreshHover';
import './commands/regenerateMarkdownDocument';
import './commands/remoteProviders';
Expand Down
94 changes: 94 additions & 0 deletions src/commands/recomposeBranch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { window } from 'vscode';
import type { Sources } from '../constants.telemetry';
import type { Container } from '../container';
import { CommandQuickPickItem } from '../quickpicks/items/common';
import { ReferencesQuickPickIncludes, showReferencePicker } from '../quickpicks/referencePicker';
import { getBestRepositoryOrShowPicker } from '../quickpicks/repositoryPicker';
import { command, executeCommand } from '../system/-webview/command';
import { getNodeRepoPath } from '../views/nodes/abstract/viewNode';
import type { WebviewPanelShowCommandArgs } from '../webviews/webviewsController';
import { GlCommandBase } from './commandBase';
import type { CommandContext } from './commandContext';
import { isCommandContextViewNodeHasBranch } from './commandContext.utils';

export interface RecomposeBranchCommandArgs {
repoPath?: string;
branchName?: string;
source?: Sources;
}

@command()
export class RecomposeBranchCommand extends GlCommandBase {
constructor(private readonly container: Container) {
super(['gitlens.recomposeBranch', 'gitlens.recomposeBranch:views']);
}

protected override preExecute(context: CommandContext, args?: RecomposeBranchCommandArgs): Promise<void> {
if (isCommandContextViewNodeHasBranch(context)) {
args = { ...args };
args.repoPath = args.repoPath ?? getNodeRepoPath(context.node);
args.branchName = args.branchName ?? context.node.branch.name;
args.source = args.source ?? 'view';
}

return this.execute(args);
}

async execute(args?: RecomposeBranchCommandArgs): Promise<void> {
try {
// Get repository path using picker fallback
const repoPath =
args?.repoPath ??
(await getBestRepositoryOrShowPicker(this.container, undefined, undefined, 'Recompose Branch'))?.path;
if (!repoPath) return;

args = { ...args };

// Get branch name using picker fallback
let branchName = args.branchName;
if (!branchName) {
const pick = await showReferencePicker(repoPath, 'Recompose Branch', 'Choose a branch to recompose', {
include: ReferencesQuickPickIncludes.Branches,
sort: { branches: { current: true } },
});
if (pick == null || pick instanceof CommandQuickPickItem) return;

if (pick.refType === 'branch') {
branchName = pick.name;
} else {
return;
}
}

// Validate that the repository exists
const repo = this.container.git.getRepository(repoPath);
if (!repo) {
void window.showErrorMessage('Repository not found');
return;
}

// Validate that the branch exists
const branch = await repo.git.branches.getBranch(branchName);
if (!branch) {
void window.showErrorMessage(`Branch '${branchName}' not found`);
return;
}

// Check if branch is remote-only
if (branch.remote && !branch.upstream) {
void window.showErrorMessage(`Cannot recompose remote-only branch '${branchName}'`);
return;
}

// Open the composer with branch mode
await executeCommand<WebviewPanelShowCommandArgs>('gitlens.showComposerPage', undefined, {
repoPath: repoPath,
source: args?.source,
mode: 'preview',
branchName: branchName,
});
} catch (ex) {
void window.showErrorMessage(`Failed to recompose branch: ${ex}`);
}
}
}
3 changes: 3 additions & 0 deletions src/constants.commands.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ export type ContributedCommands =
| 'gitlens.quickOpenFileHistory'
| 'gitlens.quickOpenFileHistory:commitDetails'
| 'gitlens.quickOpenFileHistory:graphDetails'
| 'gitlens.recomposeBranch:graph'
| 'gitlens.recomposeBranch:views'
| 'gitlens.regenerateMarkdownDocument'
| 'gitlens.restore.file:commitDetails'
| 'gitlens.restore.file:graphDetails'
Expand Down Expand Up @@ -909,6 +911,7 @@ export type ContributedPaletteCommands =
| 'gitlens.pullRepositories'
| 'gitlens.pushRepositories'
| 'gitlens.quickOpenFileHistory'
| 'gitlens.recomposeBranch'
| 'gitlens.reset'
| 'gitlens.resetViewsLayout'
| 'gitlens.revealCommitInView'
Expand Down
20 changes: 15 additions & 5 deletions src/env/node/git/sub-providers/patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
WorktreeCreateError,
} from '../../../../git/errors';
import type { GitPatchSubProvider } from '../../../../git/gitProvider';
import type { GitCommit } from '../../../../git/models/commit';
import type { GitCommit, GitCommitIdentityShape } from '../../../../git/models/commit';
import { log } from '../../../../system/decorators/log';
import { Logger } from '../../../../system/logger';
import { getLogScope } from '../../../../system/logger.scope';
Expand Down Expand Up @@ -178,16 +178,16 @@ export class PatchGitSubProvider implements GitPatchSubProvider {
async createUnreachableCommitsFromPatches(
repoPath: string,
base: string | undefined,
patches: { message: string; patch: string }[],
patches: { message: string; patch: string; author?: GitCommitIdentityShape }[],
): Promise<string[]> {
// Create a temporary index file
await using disposableIndex = await this.provider.staging!.createTemporaryIndex(repoPath, base);
const { env } = disposableIndex;

const shas: string[] = [];

for (const { message, patch } of patches) {
const sha = await this.createUnreachableCommitForPatchCore(env, repoPath, base, message, patch);
for (const { message, patch, author } of patches) {
const sha = await this.createUnreachableCommitForPatchCore(env, repoPath, base, message, patch, author);
shas.push(sha);
base = sha;
}
Expand All @@ -201,6 +201,7 @@ export class PatchGitSubProvider implements GitPatchSubProvider {
base: string | undefined,
message: string,
patch: string,
author?: GitCommitIdentityShape,
): Promise<string> {
const scope = getLogScope();

Expand All @@ -221,9 +222,18 @@ export class PatchGitSubProvider implements GitPatchSubProvider {
let result = await this.git.exec({ cwd: repoPath, env: env }, 'write-tree');
const tree = result.stdout.trim();

// Set the author if provided
const commitEnv = author
? {
...env,
GIT_AUTHOR_NAME: author.name,
GIT_AUTHOR_EMAIL: author.email || '',
}
: env;

// Create new commit from the tree
result = await this.git.exec(
{ cwd: repoPath, env: env },
{ cwd: repoPath, env: commitEnv },
'commit-tree',
tree,
...(base ? ['-p', base] : []),
Expand Down
17 changes: 17 additions & 0 deletions src/env/node/git/sub-providers/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,21 @@ export class RefsGitSubProvider implements GitRefsSubProvider {
);
return result.stdout.trim() || undefined;
}

@log()
async updateReference(
repoPath: string,
ref: string,
newRef: string,
cancellation?: CancellationToken,
): Promise<void> {
const scope = getLogScope();

try {
await this.git.exec({ cwd: repoPath, cancellation: cancellation }, 'update-ref', ref, newRef);
} catch (ex) {
Logger.error(ex, scope);
if (isCancellationError(ex)) throw ex;
}
}
}
3 changes: 2 additions & 1 deletion src/env/node/git/sub-providers/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ export class StatusGitSubProvider implements GitStatusSubProvider {
@log()
async hasWorkingChanges(
repoPath: string,
options?: { staged?: boolean; unstaged?: boolean; untracked?: boolean },
options?: { staged?: boolean; unstaged?: boolean; untracked?: boolean; throwOnError?: boolean },
cancellation?: CancellationToken,
): Promise<boolean> {
const scope = getLogScope();
Expand Down Expand Up @@ -664,6 +664,7 @@ export class StatusGitSubProvider implements GitStatusSubProvider {
// Log other errors and return false for graceful degradation
Logger.error(ex, scope);
setLogScopeExit(scope, ' \u2022 error checking for changes');
if (options?.throwOnError) throw ex;
return false;
}
}
Expand Down
7 changes: 5 additions & 2 deletions src/git/gitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { GitUri } from './gitUri';
import type { GitConflictFile } from './models';
import type { GitBlame, GitBlameLine } from './models/blame';
import type { GitBranch } from './models/branch';
import type { GitCommit, GitCommitStats, GitStashCommit } from './models/commit';
import type { GitCommit, GitCommitIdentityShape, GitCommitStats, GitStashCommit } from './models/commit';
import type { GitContributor, GitContributorsStats } from './models/contributor';
import type {
GitDiff,
Expand Down Expand Up @@ -536,7 +536,7 @@ export interface GitPatchSubProvider {
createUnreachableCommitsFromPatches(
repoPath: string,
base: string | undefined,
patches: { message: string; patch: string }[],
patches: { message: string; patch: string; author?: GitCommitIdentityShape }[],
): Promise<string[]>;
createEmptyInitialCommit(repoPath: string): Promise<string>;

Expand Down Expand Up @@ -573,6 +573,7 @@ export interface GitRefsSubProvider {
pathOrUri?: string | Uri,
cancellation?: CancellationToken,
): Promise<boolean>;
updateReference(repoPath: string, ref: string, newRef: string, cancellation?: CancellationToken): Promise<void>;
}

export interface GitRemotesSubProvider {
Expand Down Expand Up @@ -750,6 +751,8 @@ export interface GitStatusSubProvider {
unstaged?: boolean;
/** Check for untracked files (default: true) */
untracked?: boolean;
/** Throw errors rather than returning false */
throwOnError?: boolean;
},
cancellation?: CancellationToken,
): Promise<boolean>;
Expand Down
10 changes: 10 additions & 0 deletions src/plus/integrations/providers/github/sub-providers/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,14 @@ export class RefsGitSubProvider implements GitRefsSubProvider {
): Promise<boolean> {
return Promise.resolve(true);
}

@log()
updateReference(
_repoPath: string,
_ref: string,
_newRef: string,
_cancellation?: CancellationToken,
): Promise<void> {
return Promise.resolve();
}
}
Loading