finch-git-branch 0.3.3 → 0.3.6
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/index.js +1 -0
- package/dist/scm-panel.html +38 -18
- package/dist/scm-panel.js +71 -15
- package/i18n/en-US.json +3 -0
- package/i18n/zh-CN.json +3 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ The first version intentionally leaves out force-push, rebase, reset, cherry-pic
|
|
|
32
32
|
|
|
33
33
|
## Permissions
|
|
34
34
|
|
|
35
|
-
This tool needs shell permission
|
|
35
|
+
This tool needs shell permission to run local `git` commands and read-only filesystem permission to inspect repository metadata and changed files. No network access needed.
|
|
36
36
|
|
|
37
37
|
## Development
|
|
38
38
|
|
package/README.zh-CN.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -132,6 +132,7 @@ export function activate(ctx) {
|
|
|
132
132
|
handshake: { svg: readIconSvg('handshake'), description: 'Include Finch Code co-author' },
|
|
133
133
|
'git-branch': { svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/></svg>', description: 'Git repository' },
|
|
134
134
|
'refresh-cw': { svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>', description: 'Refresh repository status' },
|
|
135
|
+
'external-link': { svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>', description: 'Open remote repository' },
|
|
135
136
|
ellipsis: { svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>', description: 'More repository actions' },
|
|
136
137
|
}));
|
|
137
138
|
const composerAction = ctx.composerActions.register('git-branch', {
|
package/dist/scm-panel.html
CHANGED
|
@@ -766,8 +766,17 @@
|
|
|
766
766
|
},
|
|
767
767
|
};
|
|
768
768
|
let words = t.en;
|
|
769
|
+
// Attach the latest env-derived scope to every backend action. This is
|
|
770
|
+
// both restoration metadata and a safety boundary: if a Session/Space
|
|
771
|
+
// switch races with an action, the backend can reject stale repo state
|
|
772
|
+
// instead of executing against the previous cwd.
|
|
769
773
|
const post = (type, data = {}) =>
|
|
770
|
-
window.finch?.postMessage({
|
|
774
|
+
window.finch?.postMessage({
|
|
775
|
+
type,
|
|
776
|
+
cwd: state.cwd,
|
|
777
|
+
scopeKey: state.scopeKey,
|
|
778
|
+
...data,
|
|
779
|
+
});
|
|
771
780
|
const esc = (s) =>
|
|
772
781
|
String(s).replace(
|
|
773
782
|
/[&<>'"]/g,
|
|
@@ -884,28 +893,39 @@
|
|
|
884
893
|
document.querySelectorAll(".file").forEach((row) => {
|
|
885
894
|
const path = row.dataset.path,
|
|
886
895
|
file = r.files.find((f) => f.path === path);
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
896
|
+
const bindFileAction = (selector, handler) => {
|
|
897
|
+
const button = row.querySelector(selector);
|
|
898
|
+
if (!button) return;
|
|
899
|
+
// The row itself opens Diff on pointerdown. Handle child actions in
|
|
900
|
+
// the same event phase so the parent never consumes the first tap.
|
|
901
|
+
button.onpointerdown = (event) => {
|
|
902
|
+
event.preventDefault();
|
|
903
|
+
event.stopPropagation();
|
|
904
|
+
void handler();
|
|
905
|
+
};
|
|
906
|
+
};
|
|
907
|
+
bindFileAction(".open", () =>
|
|
908
|
+
post("openFile", { repoPath: state.path, filePath: path }),
|
|
909
|
+
);
|
|
910
|
+
bindFileAction(".stage", () =>
|
|
890
911
|
post(file.staged ? "unstage" : "stage", {
|
|
891
912
|
repoPath: state.path,
|
|
892
913
|
filePath: path,
|
|
914
|
+
}),
|
|
915
|
+
);
|
|
916
|
+
bindFileAction(".undo", async () => {
|
|
917
|
+
const ok = await window.finch.ui.confirm({
|
|
918
|
+
title: words.discard,
|
|
919
|
+
message: path,
|
|
920
|
+
variant: "danger",
|
|
893
921
|
});
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
message: path,
|
|
900
|
-
variant: "danger",
|
|
922
|
+
if (ok.confirmed)
|
|
923
|
+
post("discard", {
|
|
924
|
+
repoPath: state.path,
|
|
925
|
+
filePath: path,
|
|
926
|
+
status: file.status,
|
|
901
927
|
});
|
|
902
|
-
|
|
903
|
-
post("discard", {
|
|
904
|
-
repoPath: state.path,
|
|
905
|
-
filePath: path,
|
|
906
|
-
status: file.status,
|
|
907
|
-
});
|
|
908
|
-
};
|
|
928
|
+
});
|
|
909
929
|
row.onpointerdown = async (event) => {
|
|
910
930
|
if (event.target.closest(".file-actions")) return;
|
|
911
931
|
event.preventDefault();
|
package/dist/scm-panel.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile } from 'node:child_process';
|
|
|
2
2
|
import { existsSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { mkdtemp, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { homedir, tmpdir } from 'node:os';
|
|
5
|
-
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
function repoSignature(repos) {
|
|
@@ -147,14 +147,44 @@ async function fileAtRevision(repoPath, revision, filePath) {
|
|
|
147
147
|
async function hasStagedChanges(repoPath) {
|
|
148
148
|
return Boolean(await runGit(repoPath, ['diff', '--cached', '--name-only']));
|
|
149
149
|
}
|
|
150
|
+
function remoteToBrowserUrl(url) {
|
|
151
|
+
const trimmed = url.trim();
|
|
152
|
+
if (!trimmed)
|
|
153
|
+
return undefined;
|
|
154
|
+
// git@github.com:user/repo.git -> https://github.com/user/repo
|
|
155
|
+
const sshMatch = trimmed.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
|
|
156
|
+
if (sshMatch) {
|
|
157
|
+
const [, host, path] = sshMatch;
|
|
158
|
+
return `https://${host}/${path.replace(/\.git$/i, '')}`;
|
|
159
|
+
}
|
|
160
|
+
// ssh://git@host/path/repo.git
|
|
161
|
+
const sshProtoMatch = trimmed.match(/^ssh:\/\/(?:git@)?([^/]+)\/(.+?)(?:\.git)?$/);
|
|
162
|
+
if (sshProtoMatch) {
|
|
163
|
+
const [, host, path] = sshProtoMatch;
|
|
164
|
+
return `https://${host}/${path.replace(/\.git$/i, '')}`;
|
|
165
|
+
}
|
|
166
|
+
// https://host/path/repo.git or http://...
|
|
167
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
168
|
+
return trimmed.replace(/\.git$/i, '');
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
function openInBrowser(url, platform) {
|
|
173
|
+
if (platform === 'darwin')
|
|
174
|
+
execFile('open', [url]);
|
|
175
|
+
else if (platform === 'win32')
|
|
176
|
+
execFile('cmd', ['/c', 'start', '', url]);
|
|
177
|
+
else
|
|
178
|
+
execFile('xdg-open', [url]);
|
|
179
|
+
}
|
|
150
180
|
export function activateScmPanel(ctx) {
|
|
151
181
|
ctx.subscriptions.push(panelUi(ctx).onDidOpenPanel((panel) => {
|
|
152
182
|
let cwd = '';
|
|
153
183
|
let scopeKey = '';
|
|
154
184
|
let generation = 0;
|
|
155
185
|
let repos = [];
|
|
156
|
-
let
|
|
157
|
-
const active = () => !
|
|
186
|
+
let disposed = false;
|
|
187
|
+
const active = () => !disposed;
|
|
158
188
|
const post = async (message) => {
|
|
159
189
|
if (!active())
|
|
160
190
|
return false;
|
|
@@ -162,9 +192,11 @@ export function activateScmPanel(ctx) {
|
|
|
162
192
|
await panel.postMessage(message);
|
|
163
193
|
return true;
|
|
164
194
|
}
|
|
165
|
-
catch {
|
|
166
|
-
//
|
|
167
|
-
|
|
195
|
+
catch (error) {
|
|
196
|
+
// Session/Space navigation can briefly leave this scope without a
|
|
197
|
+
// viewer. That is retryable: only onDidDispose permanently ends the
|
|
198
|
+
// handle, and the page's next init/refresh handshake must still work.
|
|
199
|
+
ctx.logger.debug('SCM panel post deferred until next handshake', error);
|
|
168
200
|
return false;
|
|
169
201
|
}
|
|
170
202
|
};
|
|
@@ -174,8 +206,8 @@ export function activateScmPanel(ctx) {
|
|
|
174
206
|
try {
|
|
175
207
|
await panel.updateToolbarItem('scm-title', { label, icon });
|
|
176
208
|
}
|
|
177
|
-
catch {
|
|
178
|
-
|
|
209
|
+
catch (error) {
|
|
210
|
+
ctx.logger.debug('SCM toolbar update deferred until next refresh', error);
|
|
179
211
|
}
|
|
180
212
|
};
|
|
181
213
|
const sendConfig = async () => {
|
|
@@ -240,18 +272,17 @@ export function activateScmPanel(ctx) {
|
|
|
240
272
|
try {
|
|
241
273
|
await ctx.ui.showToast({ title, variant, position: 'TC' });
|
|
242
274
|
}
|
|
243
|
-
catch {
|
|
244
|
-
|
|
275
|
+
catch (error) {
|
|
276
|
+
ctx.logger.debug('SCM toast skipped during scope transition', error);
|
|
245
277
|
}
|
|
246
278
|
};
|
|
247
279
|
ctx.subscriptions.push(panel.onDidChangeVisibility((visible) => {
|
|
248
|
-
if (!visible)
|
|
280
|
+
if (!visible || !active())
|
|
249
281
|
return;
|
|
250
|
-
unavailable = false;
|
|
251
282
|
void sendConfig().then(() => refresh()).catch(() => undefined);
|
|
252
283
|
}));
|
|
253
284
|
ctx.subscriptions.push(panel.onDidDispose(() => {
|
|
254
|
-
|
|
285
|
+
disposed = true;
|
|
255
286
|
generation += 1;
|
|
256
287
|
repos = [];
|
|
257
288
|
}));
|
|
@@ -279,6 +310,15 @@ export function activateScmPanel(ctx) {
|
|
|
279
310
|
await refresh();
|
|
280
311
|
return;
|
|
281
312
|
}
|
|
313
|
+
// Every page action carries its current env-derived scope. If an init
|
|
314
|
+
// message was lost during navigation, this prevents a stale repoPath
|
|
315
|
+
// from running against the previous Space; refresh the new scope and
|
|
316
|
+
// require a fresh user action instead.
|
|
317
|
+
if (await setScope(message)) {
|
|
318
|
+
await sendConfig();
|
|
319
|
+
await refresh();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
282
322
|
const repoPath = allowedRepo(repos, message.repoPath);
|
|
283
323
|
if (!repoPath)
|
|
284
324
|
return;
|
|
@@ -310,6 +350,17 @@ export function activateScmPanel(ctx) {
|
|
|
310
350
|
await runGit(repoPath, ['fetch', '--prune'], 60_000);
|
|
311
351
|
await toast(ctx.i18n.t('git.scm.fetch.success'));
|
|
312
352
|
}
|
|
353
|
+
if (message.type === 'openRemote') {
|
|
354
|
+
const remoteUrl = await runGit(repoPath, ['remote', 'get-url', 'origin']).catch(() => '');
|
|
355
|
+
const browserUrl = remoteToBrowserUrl(remoteUrl);
|
|
356
|
+
if (!browserUrl) {
|
|
357
|
+
await toast(ctx.i18n.t('git.scm.openRemote.none'), 'error');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
openInBrowser(browserUrl, process.platform);
|
|
361
|
+
await toast(ctx.i18n.t('git.scm.openRemote.success'));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
313
364
|
if (message.type === 'requestCommit') {
|
|
314
365
|
const result = await ctx.ui.showModalDialog({
|
|
315
366
|
title: ctx.i18n.t('git.scm.commit.title'),
|
|
@@ -347,8 +398,13 @@ export function activateScmPanel(ctx) {
|
|
|
347
398
|
}
|
|
348
399
|
if (message.type === 'openFile') {
|
|
349
400
|
const file = allowedFile(repoPath, message.filePath);
|
|
350
|
-
if (file && existsSync(file))
|
|
351
|
-
|
|
401
|
+
if (file && existsSync(file)) {
|
|
402
|
+
const previewPath = realpathSync(file);
|
|
403
|
+
const htmlOptions = ['.html', '.htm'].includes(extname(previewPath).toLowerCase())
|
|
404
|
+
? { htmlPreview: 'code' }
|
|
405
|
+
: undefined;
|
|
406
|
+
await panelUi(ctx).openFilePreview(previewPath, htmlOptions);
|
|
407
|
+
}
|
|
352
408
|
return;
|
|
353
409
|
}
|
|
354
410
|
if (message.type === 'prepareFileDiff') {
|
package/i18n/en-US.json
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
{ "label": "Source Control" },
|
|
17
17
|
{},
|
|
18
18
|
{ "tooltip": "Refresh repository status" },
|
|
19
|
+
{ "tooltip": "Open remote repository" },
|
|
19
20
|
{ "items": [{ "label": "Fetch" }, { "label": "Pull" }, { "label": "Push" }] }
|
|
20
21
|
]
|
|
21
22
|
},
|
|
@@ -67,6 +68,8 @@
|
|
|
67
68
|
"git.scm.pull.success": "Pulled latest changes",
|
|
68
69
|
"git.scm.push.success": "Pushed to remote",
|
|
69
70
|
"git.scm.fetch.success": "Fetched remote updates",
|
|
71
|
+
"git.scm.openRemote.success": "Opened remote repository",
|
|
72
|
+
"git.scm.openRemote.none": "No remote repository configured",
|
|
70
73
|
"tool.create.title": "Create Git Branch",
|
|
71
74
|
"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."
|
|
72
75
|
}
|
package/i18n/zh-CN.json
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
{ "label": "源代码管理" },
|
|
17
17
|
{},
|
|
18
18
|
{ "tooltip": "刷新存储库状态" },
|
|
19
|
+
{ "tooltip": "打开远程存储库" },
|
|
19
20
|
{ "items": [{ "label": "获取" }, { "label": "拉取" }, { "label": "推送" }] }
|
|
20
21
|
]
|
|
21
22
|
},
|
|
@@ -67,6 +68,8 @@
|
|
|
67
68
|
"git.scm.pull.success": "已拉取最新更改",
|
|
68
69
|
"git.scm.push.success": "已推送到远程",
|
|
69
70
|
"git.scm.fetch.success": "已获取远程更新",
|
|
71
|
+
"git.scm.openRemote.success": "已打开远程存储库",
|
|
72
|
+
"git.scm.openRemote.none": "未配置远程存储库",
|
|
70
73
|
"tool.create.title": "Create Git Branch",
|
|
71
74
|
"tool.create.desc": "创建并检出 Git 分支。已确定分支名时传入 branchName 参数,以静默创建;否则省略该参数以打开表单收集分支名。"
|
|
72
75
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "finch-git-branch",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Git Source Control for Finch Composer toolbar.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Finch Team",
|
|
@@ -74,6 +74,11 @@
|
|
|
74
74
|
"icon": "ext:git-branch/refresh-cw",
|
|
75
75
|
"tooltip": "Refresh repository status"
|
|
76
76
|
},
|
|
77
|
+
{
|
|
78
|
+
"id": "openRemote",
|
|
79
|
+
"icon": "ext:git-branch/external-link",
|
|
80
|
+
"tooltip": "Open remote repository"
|
|
81
|
+
},
|
|
77
82
|
{
|
|
78
83
|
"type": "menu",
|
|
79
84
|
"id": "sync",
|
|
@@ -98,7 +103,7 @@
|
|
|
98
103
|
"skills": false
|
|
99
104
|
},
|
|
100
105
|
"permissions": {
|
|
101
|
-
"filesystem": "
|
|
106
|
+
"filesystem": "read",
|
|
102
107
|
"network": false,
|
|
103
108
|
"shell": true
|
|
104
109
|
}
|