finch-git-branch 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -5
- package/dist/index.js +16 -6
- package/dist/jetbrains-icons/CMake_auto.svg +1 -0
- package/dist/jetbrains-icons/LICENSE.md +192 -0
- package/dist/jetbrains-icons/actionScript_auto.svg +1 -0
- package/dist/jetbrains-icons/angularJS_auto.svg +1 -0
- package/dist/jetbrains-icons/anyType_auto.svg +1 -0
- package/dist/jetbrains-icons/application_auto.svg +1 -0
- package/dist/jetbrains-icons/archive.svg +1 -0
- package/dist/jetbrains-icons/archive_auto.svg +1 -0
- package/dist/jetbrains-icons/asm.svg +1 -0
- package/dist/jetbrains-icons/bbl.svg +1 -0
- package/dist/jetbrains-icons/beam_auto.svg +1 -0
- package/dist/jetbrains-icons/bib.svg +1 -0
- package/dist/jetbrains-icons/binary.svg +1 -0
- package/dist/jetbrains-icons/biomejs_auto.svg +1 -0
- package/dist/jetbrains-icons/blade.svg +1 -0
- package/dist/jetbrains-icons/bun_auto.svg +1 -0
- package/dist/jetbrains-icons/c.svg +1 -0
- package/dist/jetbrains-icons/c3Interface_auto.svg +1 -0
- package/dist/jetbrains-icons/c3Library_auto.svg +1 -0
- package/dist/jetbrains-icons/c3Test_auto.svg +1 -0
- package/dist/jetbrains-icons/c3_auto.svg +1 -0
- package/dist/jetbrains-icons/c_auto.svg +1 -0
- package/dist/jetbrains-icons/cargoLock_auto.svg +1 -0
- package/dist/jetbrains-icons/cargo_auto.svg +1 -0
- package/dist/jetbrains-icons/clojure_auto.svg +1 -0
- package/dist/jetbrains-icons/cls.svg +1 -0
- package/dist/jetbrains-icons/cmake.svg +1 -0
- package/dist/jetbrains-icons/coffeescript.svg +1 -0
- package/dist/jetbrains-icons/config_auto.svg +1 -0
- package/dist/jetbrains-icons/cpp.svg +1 -0
- package/dist/jetbrains-icons/cpp_auto.svg +1 -0
- package/dist/jetbrains-icons/cs_auto.svg +1 -0
- package/dist/jetbrains-icons/cshtml_auto.svg +1 -0
- package/dist/jetbrains-icons/csproj_auto.svg +1 -0
- package/dist/jetbrains-icons/css.svg +1 -0
- package/dist/jetbrains-icons/css_auto.svg +1 -0
- package/dist/jetbrains-icons/csv.svg +1 -0
- package/dist/jetbrains-icons/csv_auto.svg +1 -0
- package/dist/jetbrains-icons/cu_auto.svg +1 -0
- package/dist/jetbrains-icons/cuda.svg +1 -0
- package/dist/jetbrains-icons/cuda_header.svg +1 -0
- package/dist/jetbrains-icons/cuh_auto.svg +1 -0
- package/dist/jetbrains-icons/dart.svg +1 -0
- package/dist/jetbrains-icons/dart_auto.svg +1 -0
- package/dist/jetbrains-icons/dbms_auto.svg +1 -0
- package/dist/jetbrains-icons/docker_auto.svg +1 -0
- package/dist/jetbrains-icons/dockercompose.svg +1 -0
- package/dist/jetbrains-icons/dune_auto.svg +1 -0
- package/dist/jetbrains-icons/editorConfig_auto.svg +1 -0
- package/dist/jetbrains-icons/editorconfig.svg +1 -0
- package/dist/jetbrains-icons/eex_auto.svg +1 -0
- package/dist/jetbrains-icons/elixir_auto.svg +1 -0
- package/dist/jetbrains-icons/erb.svg +1 -0
- package/dist/jetbrains-icons/erbFile_auto.svg +1 -0
- package/dist/jetbrains-icons/erlang_auto.svg +1 -0
- package/dist/jetbrains-icons/eslint.svg +1 -0
- package/dist/jetbrains-icons/eslint_auto.svg +1 -0
- package/dist/jetbrains-icons/file.svg +1 -0
- package/dist/jetbrains-icons/file_dockerfile.svg +1 -0
- package/dist/jetbrains-icons/folder.svg +1 -0
- package/dist/jetbrains-icons/folderControllers_auto.svg +1 -0
- package/dist/jetbrains-icons/folderGithub_auto.svg +1 -0
- package/dist/jetbrains-icons/folderHelpers_auto.svg +1 -0
- package/dist/jetbrains-icons/folderMailers_auto.svg +1 -0
- package/dist/jetbrains-icons/folderMigrations_auto.svg +1 -0
- package/dist/jetbrains-icons/folderTest_auto.svg +1 -0
- package/dist/jetbrains-icons/folderVendor_auto.svg +1 -0
- package/dist/jetbrains-icons/folder_auto.svg +1 -0
- package/dist/jetbrains-icons/font_auto.svg +1 -0
- package/dist/jetbrains-icons/gitignore_auto.svg +1 -0
- package/dist/jetbrains-icons/gleam_auto.svg +1 -0
- package/dist/jetbrains-icons/go.svg +1 -0
- package/dist/jetbrains-icons/goMod_auto.svg +1 -0
- package/dist/jetbrains-icons/goSum_auto.svg +1 -0
- package/dist/jetbrains-icons/goWork_auto.svg +1 -0
- package/dist/jetbrains-icons/go_auto.svg +1 -0
- package/dist/jetbrains-icons/gradle.svg +1 -0
- package/dist/jetbrains-icons/graphql.svg +1 -0
- package/dist/jetbrains-icons/graphql_auto.svg +1 -0
- package/dist/jetbrains-icons/groovy.svg +1 -0
- package/dist/jetbrains-icons/h_auto.svg +1 -0
- package/dist/jetbrains-icons/handlebarsJson_auto.svg +1 -0
- package/dist/jetbrains-icons/haskell_auto.svg +1 -0
- package/dist/jetbrains-icons/hcl_auto.svg +1 -0
- package/dist/jetbrains-icons/header.svg +1 -0
- package/dist/jetbrains-icons/htaccess.svg +1 -0
- package/dist/jetbrains-icons/html.svg +1 -0
- package/dist/jetbrains-icons/html_auto.svg +1 -0
- package/dist/jetbrains-icons/http_auto.svg +1 -0
- package/dist/jetbrains-icons/ignore_file.svg +1 -0
- package/dist/jetbrains-icons/ignored_auto.svg +1 -0
- package/dist/jetbrains-icons/image.svg +1 -0
- package/dist/jetbrains-icons/image_auto.svg +1 -0
- package/dist/jetbrains-icons/ino.svg +1 -0
- package/dist/jetbrains-icons/ino_auto.svg +1 -0
- package/dist/jetbrains-icons/java.svg +1 -0
- package/dist/jetbrains-icons/javaScript_auto.svg +1 -0
- package/dist/jetbrains-icons/java_auto.svg +1 -0
- package/dist/jetbrains-icons/java_class.svg +1 -0
- package/dist/jetbrains-icons/javascript.svg +1 -0
- package/dist/jetbrains-icons/jsTest_auto.svg +1 -0
- package/dist/jetbrains-icons/json.svg +1 -0
- package/dist/jetbrains-icons/json_auto.svg +1 -0
- package/dist/jetbrains-icons/jsx.svg +1 -0
- package/dist/jetbrains-icons/jsxTest_auto.svg +1 -0
- package/dist/jetbrains-icons/jsx_auto.svg +1 -0
- package/dist/jetbrains-icons/julia.svg +1 -0
- package/dist/jetbrains-icons/jupyter_auto.svg +1 -0
- package/dist/jetbrains-icons/kotlin.svg +1 -0
- package/dist/jetbrains-icons/kotlinScript_auto.svg +1 -0
- package/dist/jetbrains-icons/kotlin_auto.svg +1 -0
- package/dist/jetbrains-icons/latex_aux.svg +1 -0
- package/dist/jetbrains-icons/less.svg +1 -0
- package/dist/jetbrains-icons/less_auto.svg +1 -0
- package/dist/jetbrains-icons/lock.svg +1 -0
- package/dist/jetbrains-icons/lock_auto.svg +1 -0
- package/dist/jetbrains-icons/lua.svg +1 -0
- package/dist/jetbrains-icons/lua_auto.svg +1 -0
- package/dist/jetbrains-icons/makefile.svg +1 -0
- package/dist/jetbrains-icons/makefile_auto.svg +1 -0
- package/dist/jetbrains-icons/markdown.svg +1 -0
- package/dist/jetbrains-icons/markdown_auto.svg +1 -0
- package/dist/jetbrains-icons/mdx.svg +1 -0
- package/dist/jetbrains-icons/mdx_auto.svg +1 -0
- package/dist/jetbrains-icons/ml_auto.svg +1 -0
- package/dist/jetbrains-icons/mli_auto.svg +1 -0
- package/dist/jetbrains-icons/model.svg +1 -0
- package/dist/jetbrains-icons/module_auto.svg +1 -0
- package/dist/jetbrains-icons/nodejs_auto.svg +1 -0
- package/dist/jetbrains-icons/npm_auto.svg +1 -0
- package/dist/jetbrains-icons/opam_auto.svg +1 -0
- package/dist/jetbrains-icons/opentofu_auto.svg +1 -0
- package/dist/jetbrains-icons/pdf.svg +1 -0
- package/dist/jetbrains-icons/php.svg +1 -0
- package/dist/jetbrains-icons/php_auto.svg +1 -0
- package/dist/jetbrains-icons/pnpm_auto.svg +1 -0
- package/dist/jetbrains-icons/pnpm_dark.svg +1 -0
- package/dist/jetbrains-icons/postcss.svg +1 -0
- package/dist/jetbrains-icons/postcss_auto.svg +1 -0
- package/dist/jetbrains-icons/prettier_auto.svg +1 -0
- package/dist/jetbrains-icons/projectProperties_auto.svg +1 -0
- package/dist/jetbrains-icons/properties_auto.svg +1 -0
- package/dist/jetbrains-icons/protoFile_auto.svg +1 -0
- package/dist/jetbrains-icons/protobuf.svg +1 -0
- package/dist/jetbrains-icons/python.svg +1 -0
- package/dist/jetbrains-icons/python_auto.svg +1 -0
- package/dist/jetbrains-icons/rakeTask_auto.svg +1 -0
- package/dist/jetbrains-icons/rego_auto.svg +1 -0
- package/dist/jetbrains-icons/ruby.svg +1 -0
- package/dist/jetbrains-icons/rubyGems_auto.svg +1 -0
- package/dist/jetbrains-icons/ruby_auto.svg +1 -0
- package/dist/jetbrains-icons/rust.svg +1 -0
- package/dist/jetbrains-icons/rustFile_auto.svg +1 -0
- package/dist/jetbrains-icons/sass.svg +1 -0
- package/dist/jetbrains-icons/scala.svg +1 -0
- package/dist/jetbrains-icons/scala_auto.svg +1 -0
- package/dist/jetbrains-icons/scss_auto.svg +1 -0
- package/dist/jetbrains-icons/shell.svg +1 -0
- package/dist/jetbrains-icons/shell_auto.svg +1 -0
- package/dist/jetbrains-icons/slim_auto.svg +1 -0
- package/dist/jetbrains-icons/solution_auto.svg +1 -0
- package/dist/jetbrains-icons/sql.svg +1 -0
- package/dist/jetbrains-icons/sql_auto.svg +1 -0
- package/dist/jetbrains-icons/sty.svg +1 -0
- package/dist/jetbrains-icons/svelte.svg +1 -0
- package/dist/jetbrains-icons/svelte_auto.svg +1 -0
- package/dist/jetbrains-icons/swift.svg +1 -0
- package/dist/jetbrains-icons/swift_auto.svg +1 -0
- package/dist/jetbrains-icons/tailwind_auto.svg +1 -0
- package/dist/jetbrains-icons/terraform_auto.svg +1 -0
- package/dist/jetbrains-icons/test_js.svg +1 -0
- package/dist/jetbrains-icons/test_jsx.svg +1 -0
- package/dist/jetbrains-icons/test_ts.svg +1 -0
- package/dist/jetbrains-icons/test_tsx.svg +12 -0
- package/dist/jetbrains-icons/tex.svg +1 -0
- package/dist/jetbrains-icons/text.svg +1 -0
- package/dist/jetbrains-icons/text_auto.svg +1 -0
- package/dist/jetbrains-icons/theme.json +623 -0
- package/dist/jetbrains-icons/toml.svg +1 -0
- package/dist/jetbrains-icons/toml_auto.svg +1 -0
- package/dist/jetbrains-icons/tsTest_auto.svg +1 -0
- package/dist/jetbrains-icons/tsx.svg +1 -0
- package/dist/jetbrains-icons/tsxTest_auto.svg +1 -0
- package/dist/jetbrains-icons/tsx_auto.svg +1 -0
- package/dist/jetbrains-icons/twig.svg +1 -0
- package/dist/jetbrains-icons/typeScript_auto.svg +1 -0
- package/dist/jetbrains-icons/typescript.svg +1 -0
- package/dist/jetbrains-icons/vercel_auto.svg +1 -0
- package/dist/jetbrains-icons/vite_auto.svg +1 -0
- package/dist/jetbrains-icons/vlang_auto.svg +1 -0
- package/dist/jetbrains-icons/vue.svg +1 -0
- package/dist/jetbrains-icons/vueJs_auto.svg +1 -0
- package/dist/jetbrains-icons/xml.svg +1 -0
- package/dist/jetbrains-icons/xml_auto.svg +1 -0
- package/dist/jetbrains-icons/yaml.svg +1 -0
- package/dist/jetbrains-icons/yaml_auto.svg +1 -0
- package/dist/jetbrains-icons/yarn.svg +1 -0
- package/dist/jetbrains-icons/yarn_auto.svg +1 -0
- package/dist/jetbrains-icons/zig_auto.svg +1 -0
- package/dist/jetbrains-mono-latin.woff2 +0 -0
- package/dist/scm-panel.html +1227 -0
- package/dist/scm-panel.js +393 -0
- package/i18n/en-US.json +24 -0
- package/i18n/zh-CN.json +24 -0
- package/package.json +53 -4
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { mkdtemp, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir, tmpdir } from 'node:os';
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
function repoSignature(repos) {
|
|
9
|
+
return JSON.stringify(repos.map((repo) => ({
|
|
10
|
+
path: repo.path, branch: repo.branch, dirty: repo.dirty, ahead: repo.ahead, behind: repo.behind,
|
|
11
|
+
staged: repo.staged, unstaged: repo.unstaged, untracked: repo.untracked,
|
|
12
|
+
files: repo.files.map(({ path, status, staged, add, del }) => ({ path, status, staged, add, del })),
|
|
13
|
+
graph: repo.graph.map(({ hash, parents, subject, author, date }) => ({ hash, parents, subject, author, date })),
|
|
14
|
+
})));
|
|
15
|
+
}
|
|
16
|
+
function panelUi(ctx) {
|
|
17
|
+
return ctx.ui;
|
|
18
|
+
}
|
|
19
|
+
async function runGit(cwd, args, timeout = 15_000, trimOutput = true) {
|
|
20
|
+
const { stdout } = await execFileAsync('git', ['-C', cwd, ...args], { timeout, maxBuffer: 10 * 1024 * 1024 });
|
|
21
|
+
return trimOutput ? stdout.trim() : stdout;
|
|
22
|
+
}
|
|
23
|
+
async function isRepository(path) {
|
|
24
|
+
try {
|
|
25
|
+
return (await runGit(path, ['rev-parse', '--is-inside-work-tree'])) === 'true';
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function parseStatus(output) {
|
|
32
|
+
const files = [];
|
|
33
|
+
const records = output.split('\0');
|
|
34
|
+
for (let i = 0; i < records.length; i += 1) {
|
|
35
|
+
const record = records[i];
|
|
36
|
+
if (!record)
|
|
37
|
+
continue;
|
|
38
|
+
const index = record[0] ?? ' ';
|
|
39
|
+
const worktree = record[1] ?? ' ';
|
|
40
|
+
const path = record.slice(3);
|
|
41
|
+
// In porcelain -z rename/copy records, the next NUL item is the old path.
|
|
42
|
+
if (index === 'R' || index === 'C')
|
|
43
|
+
i += 1;
|
|
44
|
+
files.push({
|
|
45
|
+
path, basename: basename(path), dirname: dirname(path) === '.' ? '' : dirname(path),
|
|
46
|
+
status: index === '?' ? '?' : (index !== ' ' ? index : worktree),
|
|
47
|
+
staged: index !== ' ' && index !== '?', add: 0, del: 0,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return files;
|
|
51
|
+
}
|
|
52
|
+
function parseGraph(output) {
|
|
53
|
+
return output.split('\x1e').filter(Boolean).map((record) => {
|
|
54
|
+
// `format:` inserts a newline between records; remove it before matching hashes to parents.
|
|
55
|
+
const [hash = '', parents = '', shortHash = '', subject = '', author = '', date = ''] = record.trimStart().split('\x1f');
|
|
56
|
+
return { hash, parents: parents ? parents.split(' ') : [], shortHash, subject, author, date };
|
|
57
|
+
}).filter((commit) => Boolean(commit.hash));
|
|
58
|
+
}
|
|
59
|
+
async function inspectRepo(path) {
|
|
60
|
+
const [branch, statusRaw, upstream, graph] = await Promise.all([
|
|
61
|
+
runGit(path, ['branch', '--show-current']).catch(() => 'HEAD'),
|
|
62
|
+
// Porcelain records begin with meaningful spaces; never trim this output.
|
|
63
|
+
runGit(path, ['status', '--porcelain=v1', '-z'], 15_000, false).catch(() => ''),
|
|
64
|
+
runGit(path, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']).catch(() => ''),
|
|
65
|
+
runGit(path, ['log', '--topo-order', '--date=short', '--pretty=format:%H%x1f%P%x1f%h%x1f%s%x1f%an%x1f%ad%x1e', '-n', '120'], 15_000, false).catch(() => ''),
|
|
66
|
+
]);
|
|
67
|
+
const files = parseStatus(statusRaw);
|
|
68
|
+
const stats = await runGit(path, ['diff', '--numstat', 'HEAD']).catch(() => '');
|
|
69
|
+
for (const line of stats.split('\n').filter(Boolean)) {
|
|
70
|
+
const [add, del, file] = line.split('\t');
|
|
71
|
+
const item = files.find((entry) => entry.path === file);
|
|
72
|
+
if (item) {
|
|
73
|
+
item.add = Number(add) || 0;
|
|
74
|
+
item.del = Number(del) || 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
await Promise.all(files.map(async (file) => {
|
|
78
|
+
const absolute = resolve(path, file.path);
|
|
79
|
+
const rightPath = existsSync(absolute) ? realpathSync(absolute) : await createDiffFile('worktree', file.path, '');
|
|
80
|
+
const compareIndex = file.staged;
|
|
81
|
+
file.diffLeftPath = await createDiffFile('base', file.path, file.status === '?' ? '' : await fileAtRevision(path, compareIndex ? 'HEAD' : '', file.path));
|
|
82
|
+
file.diffRightPath = compareIndex
|
|
83
|
+
? await createDiffFile('index', file.path, await fileAtRevision(path, '', file.path))
|
|
84
|
+
: rightPath;
|
|
85
|
+
}));
|
|
86
|
+
let ahead = 0;
|
|
87
|
+
let behind = 0;
|
|
88
|
+
if (upstream) {
|
|
89
|
+
const counts = await runGit(path, ['rev-list', '--left-right', '--count', `HEAD...${upstream}`]).catch(() => '0\t0');
|
|
90
|
+
[ahead, behind] = counts.split('\t').map((value) => Number(value) || 0);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
path, name: basename(path), branch: branch || 'HEAD', dirty: files.length > 0, ahead, behind,
|
|
94
|
+
staged: files.filter((file) => file.staged).length,
|
|
95
|
+
unstaged: files.filter((file) => !file.staged && file.status !== '?').length,
|
|
96
|
+
untracked: files.filter((file) => file.status === '?').length,
|
|
97
|
+
files, graph: parseGraph(graph),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
async function discoverRepos(cwd) {
|
|
101
|
+
if (!await isRepository(cwd))
|
|
102
|
+
return [];
|
|
103
|
+
const root = await runGit(cwd, ['rev-parse', '--show-toplevel']);
|
|
104
|
+
const paths = [root];
|
|
105
|
+
const submodules = await runGit(root, ['config', '--file', '.gitmodules', '--get-regexp', 'path']).catch(() => '');
|
|
106
|
+
for (const line of submodules.split('\n').filter(Boolean)) {
|
|
107
|
+
const submodulePath = line.replace(/^\S+\s+/, '').trim();
|
|
108
|
+
const absolute = resolve(root, submodulePath);
|
|
109
|
+
if (existsSync(absolute) && await isRepository(absolute))
|
|
110
|
+
paths.push(absolute);
|
|
111
|
+
}
|
|
112
|
+
return Promise.all(paths.map(inspectRepo));
|
|
113
|
+
}
|
|
114
|
+
function toolbarTitle(path) {
|
|
115
|
+
if (!path)
|
|
116
|
+
return 'Source Control';
|
|
117
|
+
const home = homedir();
|
|
118
|
+
const fromHome = relative(home, path);
|
|
119
|
+
if (!fromHome)
|
|
120
|
+
return '~';
|
|
121
|
+
if (!fromHome.startsWith('..') && !isAbsolute(fromHome))
|
|
122
|
+
return `~/${fromHome.replace(/\\/g, '/')}`;
|
|
123
|
+
const normalized = path.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
124
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
125
|
+
return parts.length > 6 ? `…/${parts.slice(-6).join('/')}` : normalized;
|
|
126
|
+
}
|
|
127
|
+
function allowedRepo(repos, repoPath) {
|
|
128
|
+
if (typeof repoPath !== 'string')
|
|
129
|
+
return undefined;
|
|
130
|
+
return repos.find((repo) => repo.path === repoPath)?.path;
|
|
131
|
+
}
|
|
132
|
+
function allowedFile(repoPath, filePath) {
|
|
133
|
+
if (typeof filePath !== 'string')
|
|
134
|
+
return undefined;
|
|
135
|
+
const absolute = resolve(repoPath, filePath);
|
|
136
|
+
return relative(repoPath, absolute).startsWith('..') ? undefined : absolute;
|
|
137
|
+
}
|
|
138
|
+
async function createDiffFile(label, filePath, content) {
|
|
139
|
+
const directory = await mkdtemp(join(tmpdir(), 'finch-git-diff-'));
|
|
140
|
+
const output = join(directory, `${label}-${basename(filePath)}`);
|
|
141
|
+
await writeFile(output, content, 'utf8');
|
|
142
|
+
return output;
|
|
143
|
+
}
|
|
144
|
+
async function fileAtRevision(repoPath, revision, filePath) {
|
|
145
|
+
return runGit(repoPath, ['show', `${revision}:${filePath}`], 15_000, false).catch(() => '');
|
|
146
|
+
}
|
|
147
|
+
async function hasStagedChanges(repoPath) {
|
|
148
|
+
return Boolean(await runGit(repoPath, ['diff', '--cached', '--name-only']));
|
|
149
|
+
}
|
|
150
|
+
export function activateScmPanel(ctx) {
|
|
151
|
+
ctx.subscriptions.push(panelUi(ctx).onDidOpenPanel((panel) => {
|
|
152
|
+
let cwd = '';
|
|
153
|
+
let scopeKey = '';
|
|
154
|
+
let generation = 0;
|
|
155
|
+
let repos = [];
|
|
156
|
+
let unavailable = false;
|
|
157
|
+
const active = () => !unavailable && panel.visible;
|
|
158
|
+
const post = async (message) => {
|
|
159
|
+
if (!active())
|
|
160
|
+
return false;
|
|
161
|
+
try {
|
|
162
|
+
await panel.postMessage(message);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// The Session may have dropped its viewer between the visibility check and send.
|
|
167
|
+
unavailable = true;
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const updateToolbar = async (label, icon) => {
|
|
172
|
+
if (!active())
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
await panel.updateToolbarItem('scm-title', { label, icon });
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
unavailable = true;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const sendConfig = async () => {
|
|
182
|
+
const { assistantName } = await ctx.app.getInfo();
|
|
183
|
+
await post({ type: 'config', assistantName });
|
|
184
|
+
};
|
|
185
|
+
// Also push config immediately: an already-live Webview may not emit finch:env again
|
|
186
|
+
// after the mini tool backend is reloaded.
|
|
187
|
+
void sendConfig().catch(() => undefined);
|
|
188
|
+
const setScope = async (message, useCurrentWorkspace = false) => {
|
|
189
|
+
const webviewCwd = typeof message.cwd === 'string' ? message.cwd : '';
|
|
190
|
+
// Session switches can retain a live Webview without replaying finch:env.
|
|
191
|
+
// The backend workspace follows the active session and is authoritative for heartbeat refreshes.
|
|
192
|
+
const activeCwd = ctx.workspace.directoryPath ?? ctx.workspace.projectPath ?? '';
|
|
193
|
+
const nextCwd = useCurrentWorkspace && activeCwd ? activeCwd : webviewCwd;
|
|
194
|
+
const nextScopeKey = typeof message.scopeKey === 'string' ? message.scopeKey : (scopeKey || nextCwd);
|
|
195
|
+
if (nextScopeKey === scopeKey && nextCwd === cwd)
|
|
196
|
+
return false;
|
|
197
|
+
generation += 1;
|
|
198
|
+
scopeKey = nextScopeKey;
|
|
199
|
+
cwd = nextCwd;
|
|
200
|
+
repos = [];
|
|
201
|
+
await post({ type: 'status', repos: [], scopeKey });
|
|
202
|
+
return true;
|
|
203
|
+
};
|
|
204
|
+
const refresh = async (showLoading = true) => {
|
|
205
|
+
const requestGeneration = generation;
|
|
206
|
+
const requestScope = scopeKey;
|
|
207
|
+
const requestCwd = cwd;
|
|
208
|
+
const previousSignature = repoSignature(repos);
|
|
209
|
+
if (!active())
|
|
210
|
+
return;
|
|
211
|
+
if (!requestCwd) {
|
|
212
|
+
if (repos.length === 0)
|
|
213
|
+
return;
|
|
214
|
+
await updateToolbar('Source Control', 'ext:git-branch/git-branch');
|
|
215
|
+
await post({ type: 'status', repos: [], scopeKey: requestScope, cwd: requestCwd });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (showLoading)
|
|
219
|
+
await post({ type: 'loading', loading: true, scopeKey: requestScope });
|
|
220
|
+
try {
|
|
221
|
+
const nextRepos = await discoverRepos(requestCwd);
|
|
222
|
+
// A newer finch:env/init won while Git was running; never paint stale results.
|
|
223
|
+
if (!active() || requestGeneration !== generation || requestScope !== scopeKey || requestCwd !== cwd)
|
|
224
|
+
return;
|
|
225
|
+
if (repoSignature(nextRepos) === previousSignature)
|
|
226
|
+
return;
|
|
227
|
+
repos = nextRepos;
|
|
228
|
+
await updateToolbar(toolbarTitle(repos[0]?.path), 'ext:git-branch/git-branch');
|
|
229
|
+
await post({ type: 'status', repos, scopeKey: requestScope, cwd: requestCwd });
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
if (showLoading && active() && requestGeneration === generation && requestScope === scopeKey) {
|
|
233
|
+
await post({ type: 'loading', loading: false, scopeKey: requestScope });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const toast = async (title, variant = 'success') => {
|
|
238
|
+
if (!active())
|
|
239
|
+
return;
|
|
240
|
+
try {
|
|
241
|
+
await ctx.ui.showToast({ title, variant, position: 'TC' });
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
unavailable = true;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
ctx.subscriptions.push(panel.onDidChangeVisibility((visible) => {
|
|
248
|
+
if (!visible)
|
|
249
|
+
return;
|
|
250
|
+
unavailable = false;
|
|
251
|
+
void sendConfig().then(() => refresh()).catch(() => undefined);
|
|
252
|
+
}));
|
|
253
|
+
ctx.subscriptions.push(panel.onDidDispose(() => {
|
|
254
|
+
unavailable = true;
|
|
255
|
+
generation += 1;
|
|
256
|
+
repos = [];
|
|
257
|
+
}));
|
|
258
|
+
ctx.subscriptions.push(panel.onDidReceiveMessage((raw) => {
|
|
259
|
+
void (async () => {
|
|
260
|
+
if (!active())
|
|
261
|
+
return;
|
|
262
|
+
const message = raw;
|
|
263
|
+
try {
|
|
264
|
+
if (message.type === 'init') {
|
|
265
|
+
await setScope(message);
|
|
266
|
+
await sendConfig();
|
|
267
|
+
await refresh();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (message.type === 'refresh') {
|
|
271
|
+
await setScope(message, true);
|
|
272
|
+
await refresh(message.silent !== true);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (message.type === 'syncWorkspace') {
|
|
276
|
+
const changed = await setScope(message, true);
|
|
277
|
+
await sendConfig();
|
|
278
|
+
if (changed || repos.length === 0)
|
|
279
|
+
await refresh();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const repoPath = allowedRepo(repos, message.repoPath);
|
|
283
|
+
if (!repoPath)
|
|
284
|
+
return;
|
|
285
|
+
if (message.type === 'stage') {
|
|
286
|
+
await runGit(repoPath, ['add', '--', String(message.filePath)]);
|
|
287
|
+
await toast(ctx.i18n.t('git.scm.stage.success'));
|
|
288
|
+
}
|
|
289
|
+
if (message.type === 'unstage') {
|
|
290
|
+
await runGit(repoPath, ['restore', '--staged', '--', String(message.filePath)]);
|
|
291
|
+
await toast(ctx.i18n.t('git.scm.unstage.success'));
|
|
292
|
+
}
|
|
293
|
+
if (message.type === 'stageAll') {
|
|
294
|
+
await runGit(repoPath, ['add', '-A']);
|
|
295
|
+
await toast(ctx.i18n.t('git.scm.stageAll.success'));
|
|
296
|
+
}
|
|
297
|
+
if (message.type === 'unstageAll') {
|
|
298
|
+
await runGit(repoPath, ['restore', '--staged', '.']);
|
|
299
|
+
await toast(ctx.i18n.t('git.scm.unstageAll.success'));
|
|
300
|
+
}
|
|
301
|
+
if (message.type === 'pull') {
|
|
302
|
+
await runGit(repoPath, ['pull', '--ff-only'], 60_000);
|
|
303
|
+
await toast(ctx.i18n.t('git.scm.pull.success'));
|
|
304
|
+
}
|
|
305
|
+
if (message.type === 'push') {
|
|
306
|
+
await runGit(repoPath, ['push'], 60_000);
|
|
307
|
+
await toast(ctx.i18n.t('git.scm.push.success'));
|
|
308
|
+
}
|
|
309
|
+
if (message.type === 'fetch') {
|
|
310
|
+
await runGit(repoPath, ['fetch', '--prune'], 60_000);
|
|
311
|
+
await toast(ctx.i18n.t('git.scm.fetch.success'));
|
|
312
|
+
}
|
|
313
|
+
if (message.type === 'requestCommit') {
|
|
314
|
+
const result = await ctx.ui.showModalDialog({
|
|
315
|
+
title: ctx.i18n.t('git.scm.commit.title'),
|
|
316
|
+
actions: [
|
|
317
|
+
{ id: 'cancel', label: ctx.i18n.t('git.scm.commit.cancel'), variant: 'secondary' },
|
|
318
|
+
{ id: 'commit', label: ctx.i18n.t('git.scm.commit.submit'), variant: 'primary' },
|
|
319
|
+
],
|
|
320
|
+
fields: [{
|
|
321
|
+
key: 'message', label: ctx.i18n.t('git.scm.commit.field'), type: 'textarea', required: true,
|
|
322
|
+
placeholder: ctx.i18n.t('git.scm.commit.placeholder'),
|
|
323
|
+
}],
|
|
324
|
+
});
|
|
325
|
+
if (result.action !== 'commit')
|
|
326
|
+
return;
|
|
327
|
+
const text = String(result.values?.message ?? '').trim();
|
|
328
|
+
if (!text)
|
|
329
|
+
return;
|
|
330
|
+
await runGit(repoPath, ['add', '-A']);
|
|
331
|
+
if (!await hasStagedChanges(repoPath)) {
|
|
332
|
+
await toast(ctx.i18n.t('git.scm.commit.empty'), 'info');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
await runGit(repoPath, ['commit', '-m', text, '-m', 'Co-authored-by: 帕亚 <noreply@finchwork.app>']);
|
|
336
|
+
await toast(ctx.i18n.t('git.scm.commit.success'));
|
|
337
|
+
}
|
|
338
|
+
if (message.type === 'commit') {
|
|
339
|
+
const text = typeof message.message === 'string' ? message.message.trim() : '';
|
|
340
|
+
if (!text)
|
|
341
|
+
throw new Error('Commit message is required');
|
|
342
|
+
await runGit(repoPath, ['add', '-A']);
|
|
343
|
+
if (!await hasStagedChanges(repoPath)) {
|
|
344
|
+
await toast(ctx.i18n.t('git.scm.commit.empty'), 'info');
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
await runGit(repoPath, ['commit', '-m', text, '-m', 'Co-authored-by: 帕亚 <noreply@finchwork.app>']);
|
|
348
|
+
await toast(ctx.i18n.t('git.scm.commit.success'));
|
|
349
|
+
}
|
|
350
|
+
if (message.type === 'discard') {
|
|
351
|
+
const file = allowedFile(repoPath, message.filePath);
|
|
352
|
+
if (!file)
|
|
353
|
+
throw new Error('Invalid file path');
|
|
354
|
+
if (message.status === '?')
|
|
355
|
+
await runGit(repoPath, ['clean', '-f', '--', String(message.filePath)]);
|
|
356
|
+
else
|
|
357
|
+
await runGit(repoPath, ['restore', '--source=HEAD', '--staged', '--worktree', '--', String(message.filePath)]);
|
|
358
|
+
await toast(ctx.i18n.t('git.scm.discard.success'));
|
|
359
|
+
}
|
|
360
|
+
if (message.type === 'openFile') {
|
|
361
|
+
const file = allowedFile(repoPath, message.filePath);
|
|
362
|
+
if (file && existsSync(file))
|
|
363
|
+
await panelUi(ctx).openFilePreview(realpathSync(file));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (message.type === 'prepareFileDiff') {
|
|
367
|
+
const file = allowedFile(repoPath, message.filePath);
|
|
368
|
+
const relativePath = file ? relative(repoPath, file).replace(/\\/g, '/') : '';
|
|
369
|
+
const entry = repos.find((repo) => repo.path === repoPath)?.files.find((item) => item.path === relativePath);
|
|
370
|
+
if (!file || !entry || !relativePath)
|
|
371
|
+
throw new Error('Invalid file path');
|
|
372
|
+
const rightPath = existsSync(file)
|
|
373
|
+
? realpathSync(file)
|
|
374
|
+
: await createDiffFile('worktree', relativePath, '');
|
|
375
|
+
const compareIndex = entry.staged;
|
|
376
|
+
const leftPath = await createDiffFile('base', relativePath, entry.status === '?' ? '' : await fileAtRevision(repoPath, compareIndex ? 'HEAD' : '', relativePath));
|
|
377
|
+
const resolvedRightPath = compareIndex
|
|
378
|
+
? await createDiffFile('index', relativePath, await fileAtRevision(repoPath, '', relativePath))
|
|
379
|
+
: rightPath;
|
|
380
|
+
await post({ type: 'fileDiff', leftPath, rightPath: resolvedRightPath });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
await refresh();
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
await toast(error instanceof Error ? error.message : 'Git operation failed', 'error');
|
|
387
|
+
if (active())
|
|
388
|
+
await refresh();
|
|
389
|
+
}
|
|
390
|
+
})().catch((error) => ctx.logger.error('SCM panel message failed', error));
|
|
391
|
+
}));
|
|
392
|
+
}));
|
|
393
|
+
}
|
package/i18n/en-US.json
CHANGED
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
"tooltip": "Git Branch Manager"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
|
+
"appPanel": {
|
|
14
|
+
"title": "Source Control",
|
|
15
|
+
"toolbar": [
|
|
16
|
+
{ "label": "Source Control" },
|
|
17
|
+
{},
|
|
18
|
+
{ "tooltip": "Refresh repository status" },
|
|
19
|
+
{ "items": [{ "label": "Fetch" }, { "label": "Pull" }, { "label": "Push" }] }
|
|
20
|
+
]
|
|
21
|
+
},
|
|
13
22
|
|
|
14
23
|
"git.branch.coauthor.label": "Include {finchName} as code co-author",
|
|
15
24
|
"git.branch.coauthor.on": "On",
|
|
@@ -43,6 +52,21 @@
|
|
|
43
52
|
"git.branch.create.nocwd": "No working directory",
|
|
44
53
|
"git.branch.create.success": "✅ Created and switched to branch `{name}`",
|
|
45
54
|
"git.branch.create.checkpoint": "\n\nUncommitted changes were auto-committed as a checkpoint.",
|
|
55
|
+
"git.scm.commit.title": "Commit Changes",
|
|
56
|
+
"git.scm.commit.field": "Commit Message",
|
|
57
|
+
"git.scm.commit.placeholder": "For example: feat: add source control panel",
|
|
58
|
+
"git.scm.commit.submit": "Commit",
|
|
59
|
+
"git.scm.commit.cancel": "Cancel",
|
|
60
|
+
"git.scm.commit.success": "Committed changes",
|
|
61
|
+
"git.scm.commit.empty": "No changes to commit",
|
|
62
|
+
"git.scm.stage.success": "Staged changes",
|
|
63
|
+
"git.scm.unstage.success": "Unstaged changes",
|
|
64
|
+
"git.scm.stageAll.success": "Staged all changes",
|
|
65
|
+
"git.scm.unstageAll.success": "Unstaged all changes",
|
|
66
|
+
"git.scm.discard.success": "Discarded changes",
|
|
67
|
+
"git.scm.pull.success": "Pulled latest changes",
|
|
68
|
+
"git.scm.push.success": "Pushed to remote",
|
|
69
|
+
"git.scm.fetch.success": "Fetched remote updates",
|
|
46
70
|
"tool.create.title": "Create Git Branch",
|
|
47
71
|
"tool.create.desc": "Create and switch to a new Git branch. Pass branchName to create it without a form when the name is known; otherwise omit it to collect the name in a form."
|
|
48
72
|
}
|
package/i18n/zh-CN.json
CHANGED
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
"tooltip": "Git 分支管理"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
|
+
"appPanel": {
|
|
14
|
+
"title": "源代码管理",
|
|
15
|
+
"toolbar": [
|
|
16
|
+
{ "label": "源代码管理" },
|
|
17
|
+
{},
|
|
18
|
+
{ "tooltip": "刷新存储库状态" },
|
|
19
|
+
{ "items": [{ "label": "获取" }, { "label": "拉取" }, { "label": "推送" }] }
|
|
20
|
+
]
|
|
21
|
+
},
|
|
13
22
|
|
|
14
23
|
"git.branch.coauthor.label": "包含 {finchName} 作为代码协作者",
|
|
15
24
|
"git.branch.coauthor.on": "已开启",
|
|
@@ -43,6 +52,21 @@
|
|
|
43
52
|
"git.branch.create.nocwd": "没有工作目录",
|
|
44
53
|
"git.branch.create.success": "✅ 已创建并切换到分支 `{name}`",
|
|
45
54
|
"git.branch.create.checkpoint": "\n\n已自动提交当前工作区的更改作为 checkpoint。",
|
|
55
|
+
"git.scm.commit.title": "提交更改",
|
|
56
|
+
"git.scm.commit.field": "提交信息",
|
|
57
|
+
"git.scm.commit.placeholder": "例如:feat: 添加源代码管理面板",
|
|
58
|
+
"git.scm.commit.submit": "提交",
|
|
59
|
+
"git.scm.commit.cancel": "取消",
|
|
60
|
+
"git.scm.commit.success": "已提交更改",
|
|
61
|
+
"git.scm.commit.empty": "没有需要提交的更改",
|
|
62
|
+
"git.scm.stage.success": "已暂存",
|
|
63
|
+
"git.scm.unstage.success": "已取消暂存",
|
|
64
|
+
"git.scm.stageAll.success": "已暂存全部更改",
|
|
65
|
+
"git.scm.unstageAll.success": "已取消暂存全部更改",
|
|
66
|
+
"git.scm.discard.success": "已放弃更改",
|
|
67
|
+
"git.scm.pull.success": "已拉取最新更改",
|
|
68
|
+
"git.scm.push.success": "已推送到远程",
|
|
69
|
+
"git.scm.fetch.success": "已获取远程更新",
|
|
46
70
|
"tool.create.title": "Create Git Branch",
|
|
47
71
|
"tool.create.desc": "创建并检出 Git 分支。已确定分支名时传入 branchName 参数,以静默创建;否则省略该参数以打开表单收集分支名。"
|
|
48
72
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "finch-git-branch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Git Branch Manager for Finch Composer toolbar.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Finch Team",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"type": "module",
|
|
17
17
|
"main": "dist/index.js",
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "tsc -p tsconfig.json"
|
|
19
|
+
"build": "tsc -p tsconfig.json && cp src/scm-panel.html dist/scm-panel.html && cp node_modules/@fontsource/jetbrains-mono/files/jetbrains-mono-latin-400-normal.woff2 dist/jetbrains-mono-latin.woff2 && mkdir -p dist/jetbrains-icons && cp -R src/jetbrains-icons/. dist/jetbrains-icons/"
|
|
20
20
|
},
|
|
21
21
|
"finch": {
|
|
22
22
|
"manifestVersion": 1,
|
|
@@ -49,6 +49,52 @@
|
|
|
49
49
|
"label": "Git Branch Icons"
|
|
50
50
|
}
|
|
51
51
|
],
|
|
52
|
+
"appPanel": {
|
|
53
|
+
"title": "Source Control",
|
|
54
|
+
"icon": "ext:git-branch/git-branch",
|
|
55
|
+
"viewType": "finch.git-source-control",
|
|
56
|
+
"instanceMode": "single",
|
|
57
|
+
"showInLauncher": true,
|
|
58
|
+
"source": {
|
|
59
|
+
"type": "local",
|
|
60
|
+
"path": "dist/scm-panel.html"
|
|
61
|
+
},
|
|
62
|
+
"toolbar": [
|
|
63
|
+
{
|
|
64
|
+
"type": "title",
|
|
65
|
+
"id": "scm-title",
|
|
66
|
+
"icon": "ext:git-branch/git-branch",
|
|
67
|
+
"label": "Source Control"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "spacer"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "refresh",
|
|
74
|
+
"icon": "ext:git-branch/refresh-cw",
|
|
75
|
+
"tooltip": "Refresh repository status"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"type": "menu",
|
|
79
|
+
"id": "sync",
|
|
80
|
+
"icon": "ext:git-branch/ellipsis",
|
|
81
|
+
"items": [
|
|
82
|
+
{
|
|
83
|
+
"id": "fetch",
|
|
84
|
+
"label": "Fetch"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "pull",
|
|
88
|
+
"label": "Pull"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "push",
|
|
92
|
+
"label": "Push"
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
},
|
|
52
98
|
"skills": false
|
|
53
99
|
},
|
|
54
100
|
"permissions": {
|
|
@@ -58,7 +104,7 @@
|
|
|
58
104
|
}
|
|
59
105
|
},
|
|
60
106
|
"devDependencies": {
|
|
61
|
-
"@finchtoys/minitool-api": "^0.
|
|
107
|
+
"@finchtoys/minitool-api": "^0.2.14",
|
|
62
108
|
"@types/node": "^26.1.0",
|
|
63
109
|
"typescript": "^6.0.3"
|
|
64
110
|
},
|
|
@@ -69,5 +115,8 @@
|
|
|
69
115
|
"icon.png",
|
|
70
116
|
"README.md",
|
|
71
117
|
"README.zh-CN.md"
|
|
72
|
-
]
|
|
118
|
+
],
|
|
119
|
+
"dependencies": {
|
|
120
|
+
"@fontsource/jetbrains-mono": "^5.3.0"
|
|
121
|
+
}
|
|
73
122
|
}
|