Skip to content

Comments

Implement squash-commit on the git graph history#297071

Open
timheuer wants to merge 1 commit intomicrosoft:mainfrom
timheuer:squash-commit-214809
Open

Implement squash-commit on the git graph history#297071
timheuer wants to merge 1 commit intomicrosoft:mainfrom
timheuer:squash-commit-214809

Conversation

@timheuer
Copy link
Member

@timheuer timheuer commented Feb 23, 2026

Initial draft of implementing a squash commit capability fixiing #214809

2026-02-23_10-36-21.mp4

Copilot AI review requested due to automatic review settings February 23, 2026 19:04
@vs-code-engineering
Copy link

📬 CODENOTIFY

The following users are being notified based on files changed in this PR:

@lszomoru

Matched files:

  • extensions/git/package.json
  • extensions/git/package.nls.json
  • extensions/git/src/commands.ts
  • extensions/git/src/git.ts
  • extensions/git/src/repository.ts
  • src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an initial “Squash Commits” capability to the SCM history graph context menu by enabling multi-selection in the SCM History view and wiring a new Git extension command that rewrites commit history into a single commit with an editable message.

Changes:

  • Enable multi-selection in the SCM History tree and pass selected history items to context-menu actions.
  • Add git.graph.squashCommits command implementation (currently limited to squashing the latest contiguous commits including HEAD) with an editor-based commit message flow.
  • Introduce a low-level commitTree() Git API to support squashing when the selected range includes the root commit.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts Enables multi-selection and adds a context-menu action runner to forward selected history items.
extensions/git/src/repository.ts Exposes commitTree() on the higher-level Repository wrapper.
extensions/git/src/git.ts Implements git commit-tree plumbing via commitTree().
extensions/git/src/commands.ts Adds the squash command, selection validation, confirmation UI, and commit message editor flow.
extensions/git/package.nls.json Adds localized title string for “Squash Commits”.
extensions/git/package.json Registers the command and contributes it to the SCM History item context menu.

Comment on lines +4214 to +4235
window.showInformationMessage(l10n.t('Edit the commit message and save to continue squash. Close the document to cancel.'));

const message = await new Promise<string | undefined>(resolve => {
const disposables: Disposable[] = [];

const complete = (value: string | undefined) => {
dispose(disposables);
resolve(value);
};

disposables.push(workspace.onDidSaveTextDocument(savedDocument => {
if (savedDocument.uri.toString() === squashMessageUri.toString()) {
complete(savedDocument.getText());
}
}));

disposables.push(workspace.onDidCloseTextDocument(closedDocument => {
if (closedDocument.uri.toString() === squashMessageUri.toString()) {
complete(undefined);
}
}));
});
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openSquashCommitMessageEditor proceeds as soon as the document is saved. With Auto Save enabled (especially afterDelay), the file can be saved while the user is still editing, which can trigger the squash operation prematurely with a partial message.

Consider requiring an explicit user action to continue (e.g., a modal confirmation/command) or waiting until the document is closed and then reading its final contents, rather than treating the first save event as completion.

Copilot uses AI. Check for mistakes.
Comment on lines +4189 to +4196
try {
if (!oldestCommitParent) {
const squashedRootCommit = await repository.commitTree(headCommit, message);
await repository.reset(squashedRootCommit, true);
} else {
await repository.reset(oldestCommitParent);
await repository.commit(message, {});
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The squashed commit will get a new author/committer timestamp ("now") because the implementation uses git commit / git commit-tree without setting dates. Issue #214809 calls out that the squashed commit should use the date of the oldest selected commit.

Consider setting the author/committer dates from oldestCommit.authorDate / oldestCommit.commitDate (e.g., git commit --date=... plus GIT_COMMITTER_DATE, and for commit-tree via GIT_AUTHOR_DATE/GIT_COMMITTER_DATE env) so the resulting commit preserves the expected timeline metadata.

Copilot uses AI. Check for mistakes.
horizontalScrolling: false,
multipleSelectionSupport: false
multipleSelectionSupport: true,
expandOnlyOnTwistieClick: false
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting expandOnlyOnTwistieClick: false changes the tree interaction model (the default is true when the option is omitted in AbstractTreeOptions). This makes single-click on a node toggle expansion, which is likely to interfere with the new multi-selection workflow (click-to-select can unexpectedly expand/collapse).

Consider keeping the default behavior (omit the option) or setting it to true so expansion requires the twistie, which is also consistent with other SCM trees (e.g. scmRepositoriesViewPane).

Suggested change
expandOnlyOnTwistieClick: false
expandOnlyOnTwistieClick: true

Copilot uses AI. Check for mistakes.
Comment on lines +812 to +824
if (action.id !== 'git.graph.squashCommits') {
return super.runAction(action, context);
}

const contextHistoryItem = context as ISCMHistoryItem | undefined;
const selectedHistoryItems = this._getSelectedHistoryItems();
const historyItems = selectedHistoryItems.length > 1 && contextHistoryItem &&
selectedHistoryItems.some(historyItem => historyItem.id === contextHistoryItem.id)
? selectedHistoryItems
: contextHistoryItem ? [contextHistoryItem] : [];

return this._progressService.withProgress({ location: HISTORY_VIEW_PANE_ID }, async () => {
await action.run(contextHistoryItem, historyItems);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SCMHistoryContextMenuActionRunner hard-codes the Git-specific command id ('git.graph.squashCommits') in the generic SCM history view. This creates a brittle coupling between the workbench SCM UI and the Git extension (e.g., renames/alternate providers), and makes it hard to extend multi-selection behavior for other providers/commands.

Consider introducing a provider-agnostic way to pass multi-selection to commands (for example, a dedicated SCM history context object/argument that includes both the context item and the selection, or a generic mechanism for opt-in multi-selection arguments) so the SCM view doesn’t need to special-case a Git command id.

Suggested change
if (action.id !== 'git.graph.squashCommits') {
return super.runAction(action, context);
}
const contextHistoryItem = context as ISCMHistoryItem | undefined;
const selectedHistoryItems = this._getSelectedHistoryItems();
const historyItems = selectedHistoryItems.length > 1 && contextHistoryItem &&
selectedHistoryItems.some(historyItem => historyItem.id === contextHistoryItem.id)
? selectedHistoryItems
: contextHistoryItem ? [contextHistoryItem] : [];
return this._progressService.withProgress({ location: HISTORY_VIEW_PANE_ID }, async () => {
await action.run(contextHistoryItem, historyItems);
const contextHistoryItem = context as ISCMHistoryItem | undefined;
const selectedHistoryItems = this._getSelectedHistoryItems();
const historyItems = selectedHistoryItems.length > 1 && contextHistoryItem &&
selectedHistoryItems.some(historyItem => historyItem.id === contextHistoryItem.id)
? selectedHistoryItems
: contextHistoryItem ? [contextHistoryItem] : [];
return this._progressService.withProgress({ location: HISTORY_VIEW_PANE_ID }, async () => {
// Always pass the context history item as the first argument (to preserve existing behavior),
// and pass the derived multi-selection as a second argument only for actions that declare it.
const run = (action.run as (...args: unknown[]) => unknown).bind(action);
const args: unknown[] = [contextHistoryItem];
if (run.length >= 2) {
args.push(historyItems);
}
await run(...args);

Copilot uses AI. Check for mistakes.
this._scmProviderCtx = ContextKeys.SCMProvider.bindTo(this.scopedContextKeyService);
this._scmCurrentHistoryItemRefHasRemote = ContextKeys.SCMCurrentHistoryItemRefHasRemote.bindTo(this.scopedContextKeyService);
this._scmCurrentHistoryItemRefInFilter = ContextKeys.SCMCurrentHistoryItemRefInFilter.bindTo(this.scopedContextKeyService);
this._scmHistoryItemMultiSelection = this.scopedContextKeyService.createKey<boolean>('scmHistoryItemMultiSelection', false);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This context key is created ad-hoc via scopedContextKeyService.createKey(). In this area, SCM context keys are typically centralized as RawContextKeys on the exported ContextKeys object (see src/vs/workbench/contrib/scm/browser/scmViewPane.ts ContextKeys), then bound via .bindTo().

To stay consistent and avoid stringly-typed keys, consider adding a ContextKeys.SCMHistoryItemMultiSelection RawContextKey and binding it here instead of creating the key inline.

Suggested change
this._scmHistoryItemMultiSelection = this.scopedContextKeyService.createKey<boolean>('scmHistoryItemMultiSelection', false);
this._scmHistoryItemMultiSelection = ContextKeys.SCMHistoryItemMultiSelection.bindTo(this.scopedContextKeyService);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants