Skip to content

feat: commit stacking #2869

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
33 changes: 32 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5716,6 +5716,27 @@
"icon": "$(gitlens-switch)",
"enablement": "!operationInProgress"
},
{
"command": "gitlens.views.switchToCommitStacked",
"title": "Switch to Commit (Stacked)...",
"category": "GitLens",
"icon": "$(gitlens-pop)",
"enablement": "!operationInProgress"
},
{
"command": "gitlens.views.switchToCommitPop",
"title": "Pop Commit from Stack...",
"category": "GitLens",
"icon": "$(versions)",
"enablement": "!operationInProgress"
},
{
"command": "gitlens.views.commitStackEmpty",
"title": "Empty Commit Stack",
"category": "GitLens",
"icon": "$()",
"enablement": "!operationInProgress"
},
{
"command": "gitlens.views.switchToTag",
"title": "Switch to Tag...",
Expand Down Expand Up @@ -10614,6 +10635,11 @@
"when": "view =~ /^gitlens\\.views\\.commits/ && gitlens:plus:enabled",
"group": "navigation@11"
},
{
"command": "gitlens.views.switchToCommitPop",
"when": "view =~ /^gitlens\\.views\\.commits/",
"group": "navigation@12"
},
{
"command": "gitlens.views.commitDetails.refresh",
"when": "view =~ /^gitlens\\.views\\.commitDetails/",
Expand Down Expand Up @@ -11680,6 +11706,11 @@
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b/",
"group": "1_gitlens_actions@7"
},
{
"command": "gitlens.views.switchToCommitStacked",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b/",
"group": "1_gitlens_actions@8"
},
{
"command": "gitlens.views.createBranch",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b/",
Expand Down Expand Up @@ -14823,7 +14854,7 @@
"typescript": "5.2.1-rc",
"webpack": "5.88.2",
"webpack-bundle-analyzer": "4.9.0",
"webpack-cli": "5.1.4",
"webpack-cli": "^5.1.4",
"webpack-node-externals": "3.0.0",
"webpack-require-from": "1.8.6"
},
Expand Down
3 changes: 3 additions & 0 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { GitLineTracker } from './trackers/gitLineTracker';
import { DeepLinkService } from './uris/deepLinks/deepLinkService';
import { UriService } from './uris/uriService';
import { BranchesView } from './views/branchesView';
import { CommitStack } from './views/commitStack';
import { CommitsView } from './views/commitsView';
import { ContributorsView } from './views/contributorsView';
import { FileHistoryView } from './views/fileHistoryView';
Expand Down Expand Up @@ -166,6 +167,8 @@ export class Container {
},
};

readonly CommitStack = new CommitStack(this);

private _disposables: Disposable[];
private _terminalLinks: GitTerminalLinkProvider | undefined;
private _webviews: WebviewsController;
Expand Down
90 changes: 90 additions & 0 deletions src/views/commitStack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { StatusBarItem } from 'vscode';
import { MarkdownString, StatusBarAlignment, window } from 'vscode';
import type { Container } from '../container';
import type { GitBranch } from '../git/models/branch';
import type { ViewRefNode } from './nodes/viewNode';

export class CommitStack {
private container: Container;
// The stack which is pushed to and popped from.
// We push and pop ViewRefNode types for convenience since these nodes
// coorespond to commit refs in the Commit view.
private stack: ViewRefNode[] = [];
// A StatusBarItem is created and displayed when the stack is not empty.
private statusBarItem?: StatusBarItem;
// The git ref that was checked out before any commit was pushed to the stack.
private originalRef?: GitBranch;

constructor(container: Container) {
this.container = container;
}

private renderStatusBarTooltip = (): MarkdownString => {
const tooltip = new MarkdownString();
if (this.originalRef) {
tooltip.appendMarkdown(`**original ref**: ${this.originalRef.name}\n\n`);
}
this.stack.forEach((n: ViewRefNode, i: number) => {
tooltip.appendMarkdown(`**${i}**. **commit**: ${n.ref.name}\n\n`);
});
return tooltip;
};

async push(commit: ViewRefNode): Promise<void> {
if (this.stack.length == 0) {
// track the 'ref' the branh was on before we start adding to the
// stack, we'll restore to this ref after the stack is emptied.
this.originalRef = await this.container.git.getBranch(commit.repoPath);
this.statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 100);
this.statusBarItem.show();
}
this.stack.push(commit);
if (this.statusBarItem) {
this.statusBarItem.text = `commit stack: ${commit.ref.name} ${this.stack.length}`;
this.statusBarItem.tooltip = this.renderStatusBarTooltip();
}
void window.showInformationMessage(`Pushed ${commit.ref.name} onto stack`);
return Promise.resolve();
}

async pop(): Promise<ViewRefNode | void> {
if (this.stack.length == 0) {
void window.showErrorMessage(
"Stack is empty.\nUse 'Switch to Commit (Stacked) command to push a commit to the stack.",
);
return;
}
const node = this.stack.pop();
// this just shuts the compiler up, it doesn't understand that pop()
// won't return an undefined since we check length above.
if (!node) {
return;
}
void window.showInformationMessage(`Popped ${node.ref.name} from stack`);
if (this.stack.length == 0) {
await this.empty();
return;
}
const curNode = this.stack[this.stack.length - 1];
if (this.statusBarItem) {
this.statusBarItem.text = `commit stack: ${curNode.ref.name} ${this.stack.length}`;
this.statusBarItem.tooltip = this.renderStatusBarTooltip();
}
return curNode;
}

async empty(): Promise<void> {
this.stack = [];
this.statusBarItem?.dispose();
this.statusBarItem = undefined;
void window.showInformationMessage('Stack is now empty.');
if (this.originalRef) {
// if we stored a original 'ref' before pushing to the stack,
// restore it.
await this.container.git.checkout(this.originalRef.repoPath, this.originalRef.ref);
void window.showInformationMessage(`Restored original ref to ${this.originalRef.name}`);
this.originalRef = undefined;
}
return Promise.resolve();
}
}
28 changes: 28 additions & 0 deletions src/views/viewCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ export class ViewCommands {
registerViewCommand('gitlens.views.switchToAnotherBranch', this.switch, this);
registerViewCommand('gitlens.views.switchToBranch', this.switchTo, this);
registerViewCommand('gitlens.views.switchToCommit', this.switchTo, this);
registerViewCommand('gitlens.views.switchToCommitStacked', this.switchToPush, this);
registerViewCommand('gitlens.views.switchToCommitPop', this.switchToPop, this);
registerViewCommand('gitlens.views.commitStackEmpty', this.commitStackEmpty, this);
registerViewCommand('gitlens.views.switchToTag', this.switchTo, this);
registerViewCommand('gitlens.views.addRemote', this.addRemote, this);
registerViewCommand('gitlens.views.pruneRemote', this.pruneRemote, this);
Expand Down Expand Up @@ -834,6 +837,31 @@ export class ViewCommands {
return RepoActions.switchTo(getNodeRepoPath(node));
}

@debug()
private async switchToPush(node?: ViewNode) {
if (!(node instanceof ViewRefNode)) {
return;
}
await this.container.CommitStack.push(node);
return RepoActions.switchTo(
node.repoPath,
node instanceof BranchNode && node.branch.current ? undefined : node.ref,
);
}

@debug()
private async switchToPop() {
const nextCommit = await this.container.CommitStack.pop();
if (nextCommit !== undefined) {
return this.switchTo(nextCommit);
}
}

@debug()
private async commitStackEmpty() {
await this.container.CommitStack.empty();
}

@debug()
private async undoCommit(node: CommitNode | FileRevisionAsCommitNode) {
if (!(node instanceof CommitNode) && !(node instanceof FileRevisionAsCommitNode)) return;
Expand Down