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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ All notable changes to the "gops" extension will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)

## [0.0.12]

### Added

- Added git commit graph
- Added options to stage and unstage all files
- Added option to stage and unstage individual files
- Added commit icon
- Added context menu for renaming and deleting branch

### Fixed

- Improved logic to refresh treeview in various stages

## [0.0.5]

- Added ability to perform diff checks on changed files
47 changes: 29 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,35 @@ Git Operations - Visual Git Toolkit for VS Code

## Features

### Tree View

- Toolbar with following options
- Commit
- Create Branch from Current
- Pull
- Push
- Refresh treeview
- Local Branches
- Checkout
- Delete
- New Branch
- Rename Branch
- Remote Branches
- Local Changes
- Staged Changes
- Tags
- Stash
**Branch Management**
- Create, checkout, delete, and rename branches
- Publish local branches to remote with upstream tracking
- Visual git graph showing commit history per branch
- Ahead/behind tracking for local branches

**File Operations**
- Stage and unstage files individually or all at once
- View diffs for changed files
- Auto-refresh on file save

**Remote Operations**
- Push, pull, and fetch changes
- Fetch with automatic pruning of deleted remote branches

**Commit Workflow**
- Commit staged files with custom messages
- Commit button only appears when files are staged

**User Interface**
- Clean tree view with organized sections (branches, changes, staged, tags, stash)
- Context-aware inline buttons and menus
- Modal dialogs for confirmations
- Syntax highlighting and VSCode theme integration

**Developer Features**
- Comprehensive logging and error reporting
- 80+ unit and integration tests
- Auto-refresh watchers for git state changes

## Requirements

Expand Down
10 changes: 10 additions & 0 deletions media/gitGraph.css
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,13 @@ svg.graph {
display: block;
overflow: visible;
}

.refs {
color: var(--vscode-terminal-ansiGreen);
font-size: 10px;
background: var(--vscode-badge-background);
padding: 2px 6px;
border-radius: 3px;
display: inline-block;
margin-left: 4px;
}
47 changes: 44 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,26 @@
"command": "gops.showGitGraph",
"title": "Show Git Graph",
"icon": "$(graph)"
},
{
"command": "gops.publishBranch",
"title": "Publish Branch",
"icon": "$(cloud-upload)"
},
{
"command": "gops.fetch",
"title": "Fetch",
"icon": "$(cloud-download)"
},
{
"command": "gops.stashChanges",
"title": "Stash Changes",
"icon": "$(save)"
},
{
"command": "gops.popStash",
"title": "Pop Stash",
"icon": "$(remove)"
}
],
"viewsContainers": {
Expand Down Expand Up @@ -169,6 +189,16 @@
"command": "gops.branch.current",
"when": "view == gitOpsTreeview",
"group": "navigation"
},
{
"command": "gops.fetch",
"when": "view == gitOpsTreeview",
"group": "navigation"
},
{
"command": "gops.stashChanges",
"when": "view == gitOpsTreeview && gops.hasChangedFiles == true",
"group": "navigation"
}
],
"view/item/context": [
Expand All @@ -181,7 +211,7 @@
{
"command": "gops.checkout",
"title": "Checkout Branch",
"when": "view == gitOpsTreeview && viewItem == localBranches",
"when": "view == gitOpsTreeview && (viewItem == localBranches || viewItem == localBranches.noUpstream)",
"group": "navigation"
},
{
Expand Down Expand Up @@ -211,14 +241,25 @@
{
"command": "gops.deleteBranch",
"title": "Delete Branch",
"when": "view == gitOpsTreeview && viewItem == localBranches",
"when": "view == gitOpsTreeview && (viewItem == localBranches || viewItem == localBranches.noUpstream)",
"group": "navigation"
},
{
"command": "gops.renameBranch",
"title": "Rename Branch",
"when": "view == gitOpsTreeview && viewItem == localBranches",
"when": "view == gitOpsTreeview && (viewItem == localBranches || viewItem == localBranches.noUpstream)",
"group": "navigation"
},
{
"command": "gops.publishBranch",
"title": "Publish Branch",
"when": "viewItem == localBranches.noUpstream",
"group": "inline"
},
{
"command": "gops.popStash",
"when": "view == gitOpsTreeview && viewItem == stash",
"group": "inline"
}
]
}
Expand Down
9 changes: 8 additions & 1 deletion src/commands/CommandRegistrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,15 @@ export class CommandRegistrar {
);
this.register(COMMANDS.COMMIT, () => this.delegate.commit());
this.register(COMMANDS.CREATE_TAG, () => this.delegate.createTag());
this.register(COMMANDS.SHOW_GIT_GRAPH, (node) => this.delegate.showGitGraph(node)
this.register(COMMANDS.SHOW_GIT_GRAPH, (node) =>
this.delegate.showGitGraph(node),
);
this.register(COMMANDS.PUBLISH_BRANCH, (node) =>
this.delegate.publishBranch(node),
);
this.register(COMMANDS.FETCH, () => this.delegate.fetch());
this.register(COMMANDS.POP_STASH, (node) => this.delegate.popStash(node));
this.register(COMMANDS.STASH_CHANGES, () => this.delegate.stashChanges());
}

private register(
Expand Down
4 changes: 4 additions & 0 deletions src/commands/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ export const COMMANDS = {
UNSTAGE_ALL_FILES: "gops.unstageAllFiles",
COMMIT: "gops.commit",
SHOW_GIT_GRAPH: "gops.showGitGraph",
PUBLISH_BRANCH: "gops.publishBranch",
FETCH: "gops.fetch",
POP_STASH: "gops.popStash",
STASH_CHANGES: "gops.stashChanges",
} as const;
52 changes: 47 additions & 5 deletions src/commands/GitOperationsDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { GitTreeNode } from "../gopstree/types";
import { ChangedFileNode } from "../gopstree/nodes/ChangedFileNode";
import { StagedFileNode } from "../gopstree/nodes/StagedFileNode";
import { GitGraphPanel } from "../gopswebpanel/GitGraphPanel";
import { LocalBranchNode } from "../gopstree/nodes/LocalBranchNode";
import { Notifications } from "../notifications/Notifications";
import { StashNode } from "../gopstree/nodes/StashNode";

export class GitOperationsDelegate {
constructor(
Expand All @@ -30,14 +33,18 @@ export class GitOperationsDelegate {
}

async deleteBranch(node: GitTreeNode): Promise<void> {
if (!node || !("branchName" in node)) {
if (!node || !(node instanceof LocalBranchNode)) {
return;
}

if (node.isCurrent) {
Notifications.error(`Switch to another branch before deleting.`);
return;
}

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

if (confirm !== "Delete") {
Expand Down Expand Up @@ -112,7 +119,12 @@ export class GitOperationsDelegate {
}

async showDiff(node: GitTreeNode): Promise<void> {
if (!node || !(node instanceof ChangedFileNode) || !node.fileName) {
if (
!node ||
(!(node instanceof ChangedFileNode) &&
!(node instanceof StagedFileNode)) ||
!node.fileName
) {
return;
}

Expand Down Expand Up @@ -178,4 +190,34 @@ export class GitOperationsDelegate {
async showGitGraph(branchName: string): Promise<void> {
await GitGraphPanel.createOrShow(branchName, this.gitService);
}

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

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

async fetch(): Promise<void> {
await this.gitService.fetch();
await this.treeDataProvider.refreshLocalBranchesNode();
this.treeDataProvider.refreshRemoteBranchesNode();
}

async popStash(node: GitTreeNode): Promise<void> {
if (!node || !(node instanceof StashNode)) {
return;
}

await this.gitService.popStash(node.stashRef);
this.treeDataProvider.refresh();
}

async stashChanges(): Promise<void> {
await this.gitService.stashChanges();
this.treeDataProvider.refresh();
}
}
16 changes: 14 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,21 @@ import { GitService } from "./services/GitService";
import { FileService } from "./services/FileService";
import { DiffService } from "./services/DiffService";
import { TreeDataProvider } from "./gopstree/TreeDataProvider";
import { Notifications } from "./notifications/Notifications";

export function activate(context: vscode.ExtensionContext) {
export async function activate(context: vscode.ExtensionContext) {
const gitService = new GitService();

// Check git availability
const isGitAvailable = await GitService.isGitAvailable();
if (!isGitAvailable) {
Notifications.error(
"Gops requires Git to be installed and available in PATH. Please install Git and restart VS Code.",
true,
);
return;
}

const fileService = new FileService(context.globalStorageUri.fsPath);
const diffService = new DiffService(fileService, gitService);
const treeDataProvider = new TreeDataProvider(gitService);
Expand All @@ -34,7 +46,7 @@ export function activate(context: vscode.ExtensionContext) {
treeDataProvider.refreshChangesNode();
treeDataProvider.refreshStagedNode();
});

context.subscriptions.push(treeView, onSave, gitWatcher);
console.log("Gops extension activated.");
}
21 changes: 17 additions & 4 deletions src/gopstree/ContextValue.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
export enum ContextValue {
Repository = "repository",
Changes = "changedFile",
StagedChanges = "stagedFile",

//Context related to Local Branches
LocalBranchesSection = "localBranchesSection",
LocalBranches = "localBranches",
LocalBranchesCurrent = "localBranches.current",
LocalBranchesNoUpstream = "localBranches.noUpstream",

//Context related to Remote Branches
RemoteBranchesSection = "remoteBranchesSection",
RemoteBranches = "remoteBranches",
Branch = "branch",
File = "file",
Stash = "stash",
Commit = "commit",
LocalBranchesSection = "localBranchesSection",
RemoteBranchesSection = "remoteBranchesSection",

//Context related to Changes
ChangesSection = "changesSection",
ChangesSectionEmpty = "changesSectionEmpty",
Changes = "changedFile",

//Context related to Staged Changes
StagedChangesSection = "stagedChangesSection",
StagedChangesSectionEmpty = "stagedChangesSectionEmpty",
StagedChanges = "stagedFile",

//Context related to Tags
TagsSection = "tagsSection",

//Context related to Stash
StashSection = "stashSection",
}
24 changes: 14 additions & 10 deletions src/gopstree/TreeDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { TagsSection } from "./nodes/TagsSection";
import { StashSection } from "./nodes/StashSection";
import { ContextValue } from "./ContextValue";
import { CONTEXT_KEYS } from "../constants/ContextKeys";
import { StashNode } from "./nodes/StashNode";

export class TreeDataProvider implements vscode.TreeDataProvider<GitTreeNode> {
private _onDidChangeTreeData = new vscode.EventEmitter<
Expand Down Expand Up @@ -73,7 +74,13 @@ export class TreeDataProvider implements vscode.TreeDataProvider<GitTreeNode> {
): Promise<TreeItemModel[]> {
const branches = await this.gitService.getLocalBranches();
const allLocalBranches = branches.map((b) => {
const node = new LocalBranchNode(b.name, b.current, b.ahead, b.behind);
const node = new LocalBranchNode(
b.name,
b.current,
b.ahead,
b.behind,
b.hasUpstream,
);
node.parent = parent;
return node;
});
Expand Down Expand Up @@ -124,14 +131,11 @@ export class TreeDataProvider implements vscode.TreeDataProvider<GitTreeNode> {

private async getStash(): Promise<TreeItemModel[]> {
const stash = await this.gitService.getStash();
return stash.map(
(s) =>
new TreeItemModel(
{ label: s },
NodeType.Stash,
vscode.TreeItemCollapsibleState.None,
),
);
return stash.map((s) => {
const node = new StashNode(s.ref, s.message);
console.debug(node.toString());
return node;
});
}

private async getRepositoryChildren(): Promise<TreeItemModel[]> {
Expand Down Expand Up @@ -232,7 +236,7 @@ export class TreeDataProvider implements vscode.TreeDataProvider<GitTreeNode> {
this._onDidChangeTreeData.fire(this.localBranchesNode);
}

refreshRemoteBranchesNode(): void {
async refreshRemoteBranchesNode(): Promise<void> {
if (!this.remoteBranchesNode) {
return;
}
Expand Down
Loading
Loading