Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false, // set this to true to hide the "out" folder with the compiled JS files
"dist": false // set this to true to hide the "dist" folder with the compiled JS files
"out": true,
"dist": true
},
"search.exclude": {
"out": true, // set this to false to include "out" folder in search results
"dist": true // set this to false to include "dist" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off",
"js/ts.tsc.autoDetect": "off",
"snyk.advanced.autoSelectOrganization": true
}
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,21 @@ Git Operations - Visual Git Toolkit for VS Code
### Tree View

- Toolbar with following options
- Refresh treeview
- Push
- Commit
- Create Branch from Current
- Pull
- Push
- Refresh treeview
- Local Branches
- Checkout
- Delete
- New Branch
- Local and Remote branches
- Ability to interact and perform the following operations
- Checkout branch
- Create tag
- Delete branch
- Rename branch
- Rename Branch
- Remote Branches
- Local Changes
- Staged Changes
- Tags
- Stash

## Requirements

Expand Down
25 changes: 24 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,17 @@
{
"command": "gops.commit",
"title": "Commit",
"icon": "$(save)"
"icon": "$(git-commit)"
},
{
"command": "gops.unstageAllFiles",
"title": "Unstage All Files",
"icon": "$(collapse-all)"
},
{
"command": "gops.stageAllFiles",
"title": "Stage All Files",
"icon": "$(expand-all)"
}
],
"viewsContainers": {
Expand Down Expand Up @@ -195,6 +200,24 @@
"title": "Unstage All Files",
"when": "view == gitOpsTreeview && viewItem == stagedChangesSection",
"group": "inline"
},
{
"command": "gops.stageAllFiles",
"title": "Stage All Files",
"when": "view == gitOpsTreeview && viewItem == changesSection",
"group": "inline"
},
{
"command": "gops.deleteBranch",
"title": "Delete Branch",
"when": "view == gitOpsTreeview && viewItem == localBranches",
"group": "navigation"
},
{
"command": "gops.renameBranch",
"title": "Rename Branch",
"when": "view == gitOpsTreeview && viewItem == localBranches",
"group": "navigation"
}
]
}
Expand Down
3 changes: 3 additions & 0 deletions src/commands/CommandRegistrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export class CommandRegistrar {
this.register(COMMANDS.UNSTAGE_ALL_FILES, () =>
this.delegate.unstageAllFiles(),
);
this.register(COMMANDS.STAGE_ALL_FILES, () =>
this.delegate.stageAllFiles(),
);
this.register(COMMANDS.COMMIT, () => this.delegate.commit());
this.register(COMMANDS.CREATE_TAG, () => this.delegate.createTag());
}
Expand Down
1 change: 1 addition & 0 deletions src/commands/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const COMMANDS = {
CREATE_TAG: "gops.tag",
SHOW_DIFF: "gops.showDiff",
STAGE_FILE: "gops.stageFile",
STAGE_ALL_FILES: "gops.stageAllFiles",
UNSTAGE_FILE: "gops.unstageFile",
UNSTAGE_ALL_FILES: "gops.unstageAllFiles",
COMMIT: "gops.commit",
Expand Down
35 changes: 33 additions & 2 deletions src/commands/GitOperationsDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,39 @@ export class GitOperationsDelegate {
if (!node || !("branchName" in node)) {
return;
}
// TODO: implement

const confirm = await vscode.window.showWarningMessage(
`Are you sure you want to delete branch "${node.branchName}"?`,
{ modal: true },
"Delete",
);

if (confirm !== "Delete") {
return;
}

await this.gitService.deleteBranch(node.branchName);
await this.treeDataProvider.refreshLocalBranchesNode();
}

async renameBranch(node: GitTreeNode): Promise<void> {
if (!node || !("branchName" in node)) {
return;
}
// TODO: implement

const newName = await vscode.window.showInputBox({
prompt: `Enter new name for branch "${node.branchName}"`,
placeHolder: "feature/my-new-name",
value: node.branchName,
ignoreFocusOut: true,
});

if (!newName || newName === node.branchName) {
return;
}

await this.gitService.renameBranch(node.branchName, newName);
await this.treeDataProvider.refreshLocalBranchesNode();
}

async push(): Promise<void> {
Expand Down Expand Up @@ -124,6 +149,12 @@ export class GitOperationsDelegate {
await this.treeDataProvider.refreshStagedNode();
}

async stageAllFiles(): Promise<void> {
await this.gitService.stageAllFiles();
await this.treeDataProvider.refreshChangesNode();
await this.treeDataProvider.refreshStagedNode();
}

async commit(): Promise<void> {
const message = await vscode.window.showInputBox({
prompt: "Enter commit message",
Expand Down
4 changes: 4 additions & 0 deletions src/constants/ContextKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const CONTEXT_KEYS = {
HAS_STAGED_FILES: "gops.hasStagedFiles",
HAS_CHANGED_FILES: "gops.hasChangedFiles",
} as const;
1 change: 1 addition & 0 deletions src/gopstree/ContextValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum ContextValue {
LocalBranchesSection = "localBranchesSection",
RemoteBranchesSection = "remoteBranchesSection",
ChangesSection = "changesSection",
ChangesSectionEmpty = "changesSectionEmpty",
StagedChangesSection = "stagedChangesSection",
StagedChangesSectionEmpty = "stagedChangesSectionEmpty",
TagsSection = "tagsSection",
Expand Down
41 changes: 29 additions & 12 deletions src/gopstree/TreeDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { NodeType } from "./nodes/NodeType";
import { RepositoryNode } from "./nodes/RepositoryNode";
import { LocalBranchNode } from "./nodes/LocalBranchNode";
import { RemoteBranchNode } from "./nodes/RemoteBranchNode";
import { Constants } from "../constants/Constants";
import { GitTreeNode } from "./types";
import { Notifications } from "../notifications/Notifications";
import { ChangedFileNode } from "./nodes/ChangedFileNode";
Expand All @@ -17,6 +16,7 @@ import { StagedChangesSection } from "./nodes/StagedChangesSection";
import { TagsSection } from "./nodes/TagsSection";
import { StashSection } from "./nodes/StashSection";
import { ContextValue } from "./ContextValue";
import { CONTEXT_KEYS } from "../constants/ContextKeys";

export class TreeDataProvider implements vscode.TreeDataProvider<GitTreeNode> {
private _onDidChangeTreeData = new vscode.EventEmitter<
Expand Down Expand Up @@ -135,13 +135,18 @@ export class TreeDataProvider implements vscode.TreeDataProvider<GitTreeNode> {
}

private async getRepositoryChildren(): Promise<TreeItemModel[]> {
const stagedFiles = await this.gitService.getStagedFiles();
const [stagedFiles, changedFiles] = await Promise.all([
this.gitService.getStagedFiles(),
this.gitService.getChangedFiles(),
]);

const hasStagedFiles = stagedFiles.length > 0;
await vscode.commands.executeCommand(
"setContext",
"gops.hasStagedFiles",
hasStagedFiles,
);
const hasChangedFiles = changedFiles.length > 0;

await Promise.all([
this.setContext(CONTEXT_KEYS.HAS_STAGED_FILES, hasStagedFiles),
this.setContext(CONTEXT_KEYS.HAS_CHANGED_FILES, hasChangedFiles),
]);

const localBranchesItem = new LocalBranchesSection(
this.localBranchesNode?.collapsibleState ||
Expand All @@ -159,12 +164,18 @@ export class TreeDataProvider implements vscode.TreeDataProvider<GitTreeNode> {
this.changesNode?.collapsibleState ||
vscode.TreeItemCollapsibleState.Collapsed,
);
changesItem.contextValue = hasChangedFiles
? ContextValue.ChangesSection
: ContextValue.ChangesSectionEmpty;
this.changesNode = changesItem;

const stagedItem = new StagedChangesSection(
this.stagedNode?.collapsibleState ||
vscode.TreeItemCollapsibleState.Collapsed,
);
stagedItem.contextValue = hasStagedFiles
? ContextValue.StagedChangesSection
: ContextValue.StagedChangesSectionEmpty;
this.stagedNode = stagedItem;

const tagsItem = new TagsSection(
Expand All @@ -189,6 +200,10 @@ export class TreeDataProvider implements vscode.TreeDataProvider<GitTreeNode> {
];
}

private async setContext(key: string, value: boolean): Promise<void> {
await vscode.commands.executeCommand("setContext", key, value);
}

refresh(node?: GitTreeNode): void {
this._onDidChangeTreeData.fire(node);

Expand Down Expand Up @@ -228,6 +243,12 @@ export class TreeDataProvider implements vscode.TreeDataProvider<GitTreeNode> {
if (!this.changesNode) {
return;
}
const changedFiles = await this.gitService.getChangedFiles();
const hasChangedFiles = changedFiles.length > 0;
await this.setContext(CONTEXT_KEYS.HAS_CHANGED_FILES, hasChangedFiles);
this.changesNode.contextValue = hasChangedFiles
? ContextValue.ChangesSection
: ContextValue.ChangesSectionEmpty;
this._onDidChangeTreeData.fire(this.changesNode);
}

Expand All @@ -237,11 +258,7 @@ export class TreeDataProvider implements vscode.TreeDataProvider<GitTreeNode> {
}
const stagedFiles = await this.gitService.getStagedFiles();
const hasStagedFiles = stagedFiles.length > 0;
await vscode.commands.executeCommand(
"setContext",
"gops.hasStagedFiles",
hasStagedFiles,
);
await this.setContext(CONTEXT_KEYS.HAS_STAGED_FILES, hasStagedFiles);
this.stagedNode.contextValue = hasStagedFiles
? ContextValue.StagedChangesSection
: ContextValue.StagedChangesSectionEmpty;
Expand Down
19 changes: 9 additions & 10 deletions src/gopstree/nodes/ChangedFileNode.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { ContextValue } from "../ContextValue";
import { NodeType } from "./NodeType";
import { TreeItemModel } from "../TreeItemModel";
import * as vscode from 'vscode';
import { createChangedFileTooltip, formatChangedFileLabel } from "./utils/nodeUtils";
import * as vscode from "vscode";
import {
createChangedFileTooltip,
formatChangedFileLabel,
} from "./utils/nodeUtils";
import { COMMANDS } from "../../commands/Commands";

export class ChangedFileNode extends TreeItemModel<NodeType.Changes> {
public override command?: vscode.Command;
constructor(
public readonly fileName: string,
) {
constructor(public readonly fileName: string) {
const fomatted = formatChangedFileLabel(fileName);
super(
{
Expand All @@ -25,13 +26,11 @@ export class ChangedFileNode extends TreeItemModel<NodeType.Changes> {
command: COMMANDS.SHOW_DIFF,
arguments: [this],
};

this.tooltip = createChangedFileTooltip(
fileName,
);

this.tooltip = createChangedFileTooltip(fileName);
}

public toString(): string {
return `ChangedFileNode(${this.fileName}, contextValue=${this.contextValue})`;
}
}
}
24 changes: 24 additions & 0 deletions src/services/GitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,30 @@ export class GitService {
);
}

async stageAllFiles(): Promise<void> {
await this.executeGitAction(
() => this.git.add("."),
"Staged all files successfully",
"Failed to stage all files",
);
}

async deleteBranch(branchName: string): Promise<void> {
await this.executeGitAction(
() => this.git.deleteLocalBranch(branchName),
`Branch ${branchName} deleted successfully`,
`Failed to delete branch ${branchName}`,
);
}

async renameBranch(oldName: string, newName: string): Promise<void> {
await this.executeGitAction(
() => this.git.raw(["branch", "-m", oldName, newName]),
`Branch renamed to ${newName} successfully`,
`Failed to rename branch ${oldName}`,
);
}

async getBranches(): Promise<BranchSummary> {
return this.git.branch();
}
Expand Down
25 changes: 13 additions & 12 deletions test/integration/branch.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as assert from "node:assert";
import * as vscode from "vscode";
import { COMMANDS } from "../../src/commands/Commands";

suite("Branch", function () {
this.timeout(30000);
Expand All @@ -11,29 +12,29 @@ suite("Branch", function () {
}
});

test("gops.branch.current should execute without error", async function () {
test(`${COMMANDS.CREATE_BRANCH_FROM_CURRENT} should execute without error`, async function () {
// Mock input box to simulate cancel
const stub = vscode.window.showInputBox;
(vscode.window as any).showInputBox = async () => undefined;

await vscode.commands.executeCommand("gops.branch.current");
await vscode.commands.executeCommand(COMMANDS.CREATE_BRANCH_FROM_CURRENT);

(vscode.window as any).showInputBox = stub;
assert.ok(true, "gops.branch.current completed without error");
assert.ok(true, `${COMMANDS.CREATE_BRANCH_FROM_CURRENT} completed without error`);
});

test("gops.deleteBranch should execute without error", async function () {
await vscode.commands.executeCommand("gops.deleteBranch");
assert.ok(true, "gops.deleteBranch completed without error");
test(`${COMMANDS.DELETE_BRANCH} should execute without error`, async function () {
await vscode.commands.executeCommand(COMMANDS.DELETE_BRANCH);
assert.ok(true, `${COMMANDS.DELETE_BRANCH} completed without error`);
});

test("gops.renameBranch should execute without error", async function () {
await vscode.commands.executeCommand("gops.renameBranch");
assert.ok(true, "gops.renameBranch completed without error");
test(`${COMMANDS.RENAME_BRANCH} should execute without error`, async function () {
await vscode.commands.executeCommand(COMMANDS.RENAME_BRANCH);
assert.ok(true, `${COMMANDS.RENAME_BRANCH} completed without error`);
});

test("gops.checkout should execute without error", async function () {
await vscode.commands.executeCommand("gops.checkout");
assert.ok(true, "gops.checkout completed without error");
test(`${COMMANDS.CHECKOUT_BRANCH} should execute without error`, async function () {
await vscode.commands.executeCommand(COMMANDS.CHECKOUT_BRANCH);
assert.ok(true, `${COMMANDS.CHECKOUT_BRANCH} completed without error`);
});
});
Loading
Loading