Skip to content

Commit 43b0a5d

Browse files
committed
feat: commit stacking
This feature introduces a useful way to checkout commits when performing code review or browsing large repository, called 'commit stacking'. The idea is to add a stack of Commit. The user can push and pop commits to this stack as they jump around the code. When the user pops the entire stack Gitlens restores the repository to the ref that was checked out before the first push of a commit. To accomplish this three new commands were added: 'switchToCommitStacked': accessible from the right click context menu in the commit's view. this pushes the selected commit onto the stack and checks the repository out to it. 'switchToCommitPop' - accessible from the navigation menu in the commits view. this pops the top commit from the stack and checks out the new head of the stack. 'commitStackEmpty' - accessible from the command pallete. this is a shortcut to immediately clear the stack and check out the original ref before the first commit was pushed to the stack. Additionally, a status bar item is created when the stack has items on it. This format is `commit stack: ${curNode.ref.name} ${this.commitStack.length}` where curNode is a ViewRefNode. The reason this feature is rather useful is because LSP tools do not work when viewing diffs spawned from commits. Instead, VSCode opens read only editors where you can no longer navigate code with the LSP. Even if the diff editor is not read only, some LSP's simply won't work in this view. By supporting `commit stacking` a user can quickly jump to a commit, navigate it with their LSP tools, and hop right back to where they were. Additionally, the user can make arbitrary jumps between commits without having to keep a list of which commits they jumped between. Signed-off-by: Louis DeLosSantos <louis.delos@gmail.com>
1 parent f596c5c commit 43b0a5d

File tree

4 files changed

+154
-1
lines changed

4 files changed

+154
-1
lines changed

package.json

+32-1
Original file line numberDiff line numberDiff line change
@@ -5716,6 +5716,27 @@
57165716
"icon": "$(gitlens-switch)",
57175717
"enablement": "!operationInProgress"
57185718
},
5719+
{
5720+
"command": "gitlens.views.switchToCommitStacked",
5721+
"title": "Switch to Commit (Stacked)...",
5722+
"category": "GitLens",
5723+
"icon": "$(gitlens-pop)",
5724+
"enablement": "!operationInProgress"
5725+
},
5726+
{
5727+
"command": "gitlens.views.switchToCommitPop",
5728+
"title": "Pop Commit from Stack...",
5729+
"category": "GitLens",
5730+
"icon": "$(versions)",
5731+
"enablement": "!operationInProgress"
5732+
},
5733+
{
5734+
"command": "gitlens.views.commitStackEmpty",
5735+
"title": "Empty Commit Stack",
5736+
"category": "GitLens",
5737+
"icon": "$()",
5738+
"enablement": "!operationInProgress"
5739+
},
57195740
{
57205741
"command": "gitlens.views.switchToTag",
57215742
"title": "Switch to Tag...",
@@ -10614,6 +10635,11 @@
1061410635
"when": "view =~ /^gitlens\\.views\\.commits/ && gitlens:plus:enabled",
1061510636
"group": "navigation@11"
1061610637
},
10638+
{
10639+
"command": "gitlens.views.switchToCommitPop",
10640+
"when": "view =~ /^gitlens\\.views\\.commits/",
10641+
"group": "navigation@12"
10642+
},
1061710643
{
1061810644
"command": "gitlens.views.commitDetails.refresh",
1061910645
"when": "view =~ /^gitlens\\.views\\.commitDetails/",
@@ -11680,6 +11706,11 @@
1168011706
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b/",
1168111707
"group": "1_gitlens_actions@7"
1168211708
},
11709+
{
11710+
"command": "gitlens.views.switchToCommitStacked",
11711+
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b/",
11712+
"group": "1_gitlens_actions@8"
11713+
},
1168311714
{
1168411715
"command": "gitlens.views.createBranch",
1168511716
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b/",
@@ -14823,7 +14854,7 @@
1482314854
"typescript": "5.2.1-rc",
1482414855
"webpack": "5.88.2",
1482514856
"webpack-bundle-analyzer": "4.9.0",
14826-
"webpack-cli": "5.1.4",
14857+
"webpack-cli": "^5.1.4",
1482714858
"webpack-node-externals": "3.0.0",
1482814859
"webpack-require-from": "1.8.6"
1482914860
},

src/container.ts

+3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { GitLineTracker } from './trackers/gitLineTracker';
5050
import { DeepLinkService } from './uris/deepLinks/deepLinkService';
5151
import { UriService } from './uris/uriService';
5252
import { BranchesView } from './views/branchesView';
53+
import { CommitStack } from './views/commitStack';
5354
import { CommitsView } from './views/commitsView';
5455
import { ContributorsView } from './views/contributorsView';
5556
import { FileHistoryView } from './views/fileHistoryView';
@@ -166,6 +167,8 @@ export class Container {
166167
},
167168
};
168169

170+
readonly CommitStack = new CommitStack(this);
171+
169172
private _disposables: Disposable[];
170173
private _terminalLinks: GitTerminalLinkProvider | undefined;
171174
private _webviews: WebviewsController;

src/views/commitStack.ts

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import type { StatusBarItem } from 'vscode';
2+
import { MarkdownString , StatusBarAlignment , window} from 'vscode';
3+
import type { Container } from '../container';
4+
import type { GitBranch } from '../git/models/branch';
5+
import type {
6+
ViewRefNode,
7+
} from './nodes/viewNode';
8+
9+
export class CommitStack {
10+
private container: Container;
11+
// The stack which is pushed to and popped from.
12+
// We push and pop ViewRefNode types for convenience since these nodes
13+
// coorespond to commit refs in the Commit view.
14+
private stack: ViewRefNode[] = [];
15+
// A StatusBarItem is created and displayed when the stack is not empty.
16+
private statusBarItem?: StatusBarItem;
17+
// The git ref that was checked out before any commit was pushed to the stack.
18+
private originalRef?: GitBranch;
19+
20+
constructor(container: Container) {
21+
this.container = container;
22+
}
23+
24+
private renderStatusBarTooltip = (): MarkdownString => {
25+
const tooltip = new MarkdownString();
26+
if (this.originalRef) {
27+
tooltip.appendMarkdown(`**original ref**: ${this.originalRef.name}\n\n`);
28+
}
29+
this.stack.forEach((n: ViewRefNode, i: number) => {
30+
tooltip.appendMarkdown(`**${i}**. **commit**: ${n.ref.name}\n\n`);
31+
});
32+
return tooltip;
33+
};
34+
35+
async push(commit: ViewRefNode): Promise<void> {
36+
if (this.stack.length == 0) {
37+
// track the 'ref' the branh was on before we start adding to the
38+
// stack, we'll restore to this ref after the stack is emptied.
39+
this.originalRef = await this.container.git.getBranch(commit.repoPath);
40+
this.statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 100);
41+
this.statusBarItem.show();
42+
}
43+
this.stack.push(commit);
44+
if (this.statusBarItem) {
45+
this.statusBarItem.text = `commit stack: ${commit.ref.name} ${this.stack.length}`;
46+
this.statusBarItem.tooltip = this.renderStatusBarTooltip();
47+
}
48+
void window.showInformationMessage(`Pushed ${commit.ref.name} onto stack`);
49+
return Promise.resolve();
50+
}
51+
52+
async pop(): Promise<ViewRefNode|void>{
53+
if (this.stack.length == 0) {
54+
void window.showErrorMessage("Stack is empty.\nUse 'Switch to Commit (Stacked) command to push a commit to the stack.");
55+
return;
56+
}
57+
const node = this.stack.pop();
58+
// this just shuts the compiler up, it doesn't understand that pop()
59+
// won't return an undefined since we check length above.
60+
if (!node) {
61+
return;
62+
}
63+
void window.showInformationMessage(`Popped ${node.ref.name} from stack`);
64+
if (this.stack.length == 0) {
65+
await this.empty();
66+
return;
67+
}
68+
const curNode = this.stack[this.stack.length-1];
69+
if (this.statusBarItem) {
70+
this.statusBarItem.text = `commit stack: ${curNode.ref.name} ${this.stack.length}`;
71+
this.statusBarItem.tooltip = this.renderStatusBarTooltip();
72+
}
73+
return curNode;
74+
}
75+
76+
async empty(): Promise<void> {
77+
this.stack = [];
78+
this.statusBarItem?.dispose();
79+
this.statusBarItem = undefined;
80+
void window.showInformationMessage('Stack is now empty.');
81+
if (this.originalRef) {
82+
// if we stored a original 'ref' before pushing to the stack,
83+
// restore it.
84+
await this.container.git.checkout(this.originalRef.repoPath, this.originalRef.ref);
85+
void window.showInformationMessage(`Restored original ref to ${this.originalRef.name}`);
86+
this.originalRef = undefined;
87+
}
88+
return Promise.resolve();
89+
}
90+
91+
}

src/views/viewCommands.ts

+28
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,9 @@ export class ViewCommands {
225225
registerViewCommand('gitlens.views.switchToAnotherBranch', this.switch, this);
226226
registerViewCommand('gitlens.views.switchToBranch', this.switchTo, this);
227227
registerViewCommand('gitlens.views.switchToCommit', this.switchTo, this);
228+
registerViewCommand('gitlens.views.switchToCommitStacked', this.switchToPush, this);
229+
registerViewCommand('gitlens.views.switchToCommitPop', this.switchToPop, this);
230+
registerViewCommand('gitlens.views.commitStackEmpty', this.commitStackEmpty, this);
228231
registerViewCommand('gitlens.views.switchToTag', this.switchTo, this);
229232
registerViewCommand('gitlens.views.addRemote', this.addRemote, this);
230233
registerViewCommand('gitlens.views.pruneRemote', this.pruneRemote, this);
@@ -834,6 +837,31 @@ export class ViewCommands {
834837
return RepoActions.switchTo(getNodeRepoPath(node));
835838
}
836839

840+
@debug()
841+
private async switchToPush(node?: ViewNode) {
842+
if (!(node instanceof ViewRefNode)) {
843+
return;
844+
}
845+
await this.container.CommitStack.push(node);
846+
return RepoActions.switchTo(
847+
node.repoPath,
848+
node instanceof BranchNode && node.branch.current ? undefined : node.ref,
849+
);
850+
}
851+
852+
@debug()
853+
private async switchToPop() {
854+
const nextCommit = await this.container.CommitStack.pop();
855+
if (nextCommit !== undefined) {
856+
return this.switchTo(nextCommit);
857+
}
858+
}
859+
860+
@debug()
861+
private async commitStackEmpty() {
862+
await this.container.CommitStack.empty();
863+
}
864+
837865
@debug()
838866
private async undoCommit(node: CommitNode | FileRevisionAsCommitNode) {
839867
if (!(node instanceof CommitNode) && !(node instanceof FileRevisionAsCommitNode)) return;

0 commit comments

Comments
 (0)