Skip to content

Commit 592c5de

Browse files
lambdalisueclaude
andcommitted
feat(source): add git status source for listing modified files
Implements a new source that lists files from git status, showing their modification status (staged/unstaged/untracked). Useful for quickly navigating to modified files in a git repository. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 5d15993 commit 592c5de

File tree

2 files changed

+208
-0
lines changed

2 files changed

+208
-0
lines changed

builtin/source/git_status.ts

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import * as fn from "@denops/std/function";
2+
import { join } from "@std/path/join";
3+
import { relative } from "@std/path/relative";
4+
5+
import { defineSource, type Source } from "../../source.ts";
6+
7+
type Detail = {
8+
/**
9+
* File path relative to git root
10+
*/
11+
path: string;
12+
13+
/**
14+
* Absolute file path
15+
*/
16+
absolutePath: string;
17+
18+
/**
19+
* Git status code (e.g., "M", "A", "D", "??")
20+
*/
21+
status: string;
22+
23+
/**
24+
* Human-readable status description
25+
*/
26+
statusDescription: string;
27+
28+
/**
29+
* Whether the file is staged
30+
*/
31+
staged: boolean;
32+
33+
/**
34+
* Whether the file is unstaged
35+
*/
36+
unstaged: boolean;
37+
};
38+
39+
export type GitStatusOptions = {
40+
/**
41+
* Whether to include untracked files.
42+
* @default true
43+
*/
44+
includeUntracked?: boolean;
45+
46+
/**
47+
* Whether to include ignored files.
48+
* @default false
49+
*/
50+
includeIgnored?: boolean;
51+
52+
/**
53+
* Whether to show status in submodules.
54+
* @default false
55+
*/
56+
includeSubmodules?: boolean;
57+
};
58+
59+
/**
60+
* Creates a Source that generates items from git status.
61+
*
62+
* This Source runs `git status` and generates items for each modified,
63+
* staged, or untracked file in the repository.
64+
*
65+
* @param options - Options to customize git status listing.
66+
* @returns A Source that generates items representing git status files.
67+
*/
68+
export function gitStatus(
69+
options: Readonly<GitStatusOptions> = {},
70+
): Source<Detail> {
71+
const includeUntracked = options.includeUntracked ?? true;
72+
const includeIgnored = options.includeIgnored ?? false;
73+
const includeSubmodules = options.includeSubmodules ?? false;
74+
75+
return defineSource(async function* (denops, _params, { signal }) {
76+
// Get current working directory
77+
const cwd = await fn.getcwd(denops);
78+
signal?.throwIfAborted();
79+
80+
// Build git status command
81+
const args = ["status", "--porcelain=v1"];
82+
if (includeUntracked) {
83+
args.push("-u");
84+
} else {
85+
args.push("-uno");
86+
}
87+
if (includeIgnored) {
88+
args.push("--ignored");
89+
}
90+
if (!includeSubmodules) {
91+
args.push("--ignore-submodules");
92+
}
93+
94+
try {
95+
// Run git status
96+
const cmd = new Deno.Command("git", {
97+
args,
98+
cwd,
99+
stdout: "piped",
100+
stderr: "piped",
101+
signal,
102+
});
103+
104+
const { stdout, stderr, success } = await cmd.output();
105+
106+
if (!success) {
107+
// Not a git repository or git command failed
108+
const errorText = new TextDecoder().decode(stderr);
109+
if (errorText.includes("not a git repository")) {
110+
// Silently return empty - not an error condition
111+
return;
112+
}
113+
throw new Error(`git status failed: ${errorText}`);
114+
}
115+
116+
// Parse git status output
117+
const output = new TextDecoder().decode(stdout);
118+
const lines = output.trim().split("\n").filter((line) => line);
119+
120+
const items = lines.map((line, index) => {
121+
// Git status format: XY filename
122+
// X = staged status, Y = unstaged status
123+
const staged = line[0];
124+
const unstaged = line[1];
125+
const filename = line.substring(3);
126+
127+
// Determine status code and description
128+
let status = `${staged}${unstaged}`;
129+
let statusDescription = "";
130+
let isStaged = false;
131+
let isUnstaged = false;
132+
133+
// Parse status codes
134+
if (staged === "?" && unstaged === "?") {
135+
statusDescription = "untracked";
136+
isUnstaged = true;
137+
} else if (staged === "!" && unstaged === "!") {
138+
statusDescription = "ignored";
139+
} else {
140+
// Handle staged status
141+
if (staged === "M") {
142+
statusDescription = "modified";
143+
isStaged = true;
144+
} else if (staged === "A") {
145+
statusDescription = "added";
146+
isStaged = true;
147+
} else if (staged === "D") {
148+
statusDescription = "deleted";
149+
isStaged = true;
150+
} else if (staged === "R") {
151+
statusDescription = "renamed";
152+
isStaged = true;
153+
} else if (staged === "C") {
154+
statusDescription = "copied";
155+
isStaged = true;
156+
}
157+
158+
// Handle unstaged status
159+
if (unstaged === "M") {
160+
statusDescription += isStaged ? ", modified" : "modified";
161+
isUnstaged = true;
162+
} else if (unstaged === "D") {
163+
statusDescription += isStaged ? ", deleted" : "deleted";
164+
isUnstaged = true;
165+
}
166+
}
167+
168+
// Create status indicator
169+
let indicator = "";
170+
if (status === "??") {
171+
indicator = "[?]";
172+
} else if (status === "!!") {
173+
indicator = "[!]";
174+
} else {
175+
indicator = `[${status}]`;
176+
}
177+
178+
// Format display value
179+
const absolutePath = join(cwd, filename);
180+
const displayPath = filename;
181+
const value = `${indicator.padEnd(5)} ${displayPath}`;
182+
183+
return {
184+
id: index,
185+
value,
186+
detail: {
187+
path: filename,
188+
absolutePath,
189+
status,
190+
statusDescription,
191+
staged: isStaged,
192+
unstaged: isUnstaged,
193+
},
194+
};
195+
});
196+
197+
yield* items;
198+
} catch (err) {
199+
// Handle errors gracefully
200+
if (err.name === "NotFound") {
201+
// Git not installed - silently return empty
202+
return;
203+
}
204+
throw err;
205+
}
206+
});
207+
}

builtin/source/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from "./buffer.ts";
33
export * from "./colorscheme.ts";
44
export * from "./command.ts";
55
export * from "./file.ts";
6+
export * from "./git_status.ts";
67
export * from "./helptag.ts";
78
export * from "./highlight.ts";
89
export * from "./history.ts";

0 commit comments

Comments
 (0)