finch-git-branch 0.3.5 → 0.3.7
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/dist/index.js +1 -0
- package/dist/scm-panel.html +39 -21
- package/dist/scm-panel.js +71 -18
- package/i18n/en-US.json +3 -0
- package/i18n/zh-CN.json +3 -0
- package/package.json +6 -1
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
|
@@ -30,9 +30,7 @@
|
|
|
30
30
|
}
|
|
31
31
|
@media (prefers-color-scheme: dark) {
|
|
32
32
|
:root {
|
|
33
|
-
--
|
|
34
|
-
--elev: #262629;
|
|
35
|
-
--hover: #303034;
|
|
33
|
+
--elev: var(--finch-bg-elevated, #262629);
|
|
36
34
|
--text: #e8e8e9;
|
|
37
35
|
--muted: #a3a3aa;
|
|
38
36
|
--border: #38383d;
|
|
@@ -766,8 +764,17 @@
|
|
|
766
764
|
},
|
|
767
765
|
};
|
|
768
766
|
let words = t.en;
|
|
767
|
+
// Attach the latest env-derived scope to every backend action. This is
|
|
768
|
+
// both restoration metadata and a safety boundary: if a Session/Space
|
|
769
|
+
// switch races with an action, the backend can reject stale repo state
|
|
770
|
+
// instead of executing against the previous cwd.
|
|
769
771
|
const post = (type, data = {}) =>
|
|
770
|
-
window.finch?.postMessage({
|
|
772
|
+
window.finch?.postMessage({
|
|
773
|
+
type,
|
|
774
|
+
cwd: state.cwd,
|
|
775
|
+
scopeKey: state.scopeKey,
|
|
776
|
+
...data,
|
|
777
|
+
});
|
|
771
778
|
const esc = (s) =>
|
|
772
779
|
String(s).replace(
|
|
773
780
|
/[&<>'"]/g,
|
|
@@ -884,28 +891,39 @@
|
|
|
884
891
|
document.querySelectorAll(".file").forEach((row) => {
|
|
885
892
|
const path = row.dataset.path,
|
|
886
893
|
file = r.files.find((f) => f.path === path);
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
894
|
+
const bindFileAction = (selector, handler) => {
|
|
895
|
+
const button = row.querySelector(selector);
|
|
896
|
+
if (!button) return;
|
|
897
|
+
// The row itself opens Diff on pointerdown. Handle child actions in
|
|
898
|
+
// the same event phase so the parent never consumes the first tap.
|
|
899
|
+
button.onpointerdown = (event) => {
|
|
900
|
+
event.preventDefault();
|
|
901
|
+
event.stopPropagation();
|
|
902
|
+
void handler();
|
|
903
|
+
};
|
|
904
|
+
};
|
|
905
|
+
bindFileAction(".open", () =>
|
|
906
|
+
post("openFile", { repoPath: state.path, filePath: path }),
|
|
907
|
+
);
|
|
908
|
+
bindFileAction(".stage", () =>
|
|
890
909
|
post(file.staged ? "unstage" : "stage", {
|
|
891
910
|
repoPath: state.path,
|
|
892
911
|
filePath: path,
|
|
912
|
+
}),
|
|
913
|
+
);
|
|
914
|
+
bindFileAction(".undo", async () => {
|
|
915
|
+
const ok = await window.finch.ui.confirm({
|
|
916
|
+
title: words.discard,
|
|
917
|
+
message: path,
|
|
918
|
+
variant: "danger",
|
|
893
919
|
});
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
message: path,
|
|
900
|
-
variant: "danger",
|
|
920
|
+
if (ok.confirmed)
|
|
921
|
+
post("discard", {
|
|
922
|
+
repoPath: state.path,
|
|
923
|
+
filePath: path,
|
|
924
|
+
status: file.status,
|
|
901
925
|
});
|
|
902
|
-
|
|
903
|
-
post("discard", {
|
|
904
|
-
repoPath: state.path,
|
|
905
|
-
filePath: path,
|
|
906
|
-
status: file.status,
|
|
907
|
-
});
|
|
908
|
-
};
|
|
926
|
+
});
|
|
909
927
|
row.onpointerdown = async (event) => {
|
|
910
928
|
if (event.target.closest(".file-actions")) return;
|
|
911
929
|
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,17 +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
|
-
|
|
158
|
-
// true. Treat the live handle as available so that init is never dropped;
|
|
159
|
-
// visibility still triggers an explicit refresh below.
|
|
160
|
-
const active = () => !unavailable;
|
|
186
|
+
let disposed = false;
|
|
187
|
+
const active = () => !disposed;
|
|
161
188
|
const post = async (message) => {
|
|
162
189
|
if (!active())
|
|
163
190
|
return false;
|
|
@@ -165,9 +192,11 @@ export function activateScmPanel(ctx) {
|
|
|
165
192
|
await panel.postMessage(message);
|
|
166
193
|
return true;
|
|
167
194
|
}
|
|
168
|
-
catch {
|
|
169
|
-
//
|
|
170
|
-
|
|
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);
|
|
171
200
|
return false;
|
|
172
201
|
}
|
|
173
202
|
};
|
|
@@ -177,8 +206,8 @@ export function activateScmPanel(ctx) {
|
|
|
177
206
|
try {
|
|
178
207
|
await panel.updateToolbarItem('scm-title', { label, icon });
|
|
179
208
|
}
|
|
180
|
-
catch {
|
|
181
|
-
|
|
209
|
+
catch (error) {
|
|
210
|
+
ctx.logger.debug('SCM toolbar update deferred until next refresh', error);
|
|
182
211
|
}
|
|
183
212
|
};
|
|
184
213
|
const sendConfig = async () => {
|
|
@@ -243,18 +272,17 @@ export function activateScmPanel(ctx) {
|
|
|
243
272
|
try {
|
|
244
273
|
await ctx.ui.showToast({ title, variant, position: 'TC' });
|
|
245
274
|
}
|
|
246
|
-
catch {
|
|
247
|
-
|
|
275
|
+
catch (error) {
|
|
276
|
+
ctx.logger.debug('SCM toast skipped during scope transition', error);
|
|
248
277
|
}
|
|
249
278
|
};
|
|
250
279
|
ctx.subscriptions.push(panel.onDidChangeVisibility((visible) => {
|
|
251
|
-
if (!visible)
|
|
280
|
+
if (!visible || !active())
|
|
252
281
|
return;
|
|
253
|
-
unavailable = false;
|
|
254
282
|
void sendConfig().then(() => refresh()).catch(() => undefined);
|
|
255
283
|
}));
|
|
256
284
|
ctx.subscriptions.push(panel.onDidDispose(() => {
|
|
257
|
-
|
|
285
|
+
disposed = true;
|
|
258
286
|
generation += 1;
|
|
259
287
|
repos = [];
|
|
260
288
|
}));
|
|
@@ -282,6 +310,15 @@ export function activateScmPanel(ctx) {
|
|
|
282
310
|
await refresh();
|
|
283
311
|
return;
|
|
284
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
|
+
}
|
|
285
322
|
const repoPath = allowedRepo(repos, message.repoPath);
|
|
286
323
|
if (!repoPath)
|
|
287
324
|
return;
|
|
@@ -313,6 +350,17 @@ export function activateScmPanel(ctx) {
|
|
|
313
350
|
await runGit(repoPath, ['fetch', '--prune'], 60_000);
|
|
314
351
|
await toast(ctx.i18n.t('git.scm.fetch.success'));
|
|
315
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
|
+
}
|
|
316
364
|
if (message.type === 'requestCommit') {
|
|
317
365
|
const result = await ctx.ui.showModalDialog({
|
|
318
366
|
title: ctx.i18n.t('git.scm.commit.title'),
|
|
@@ -350,8 +398,13 @@ export function activateScmPanel(ctx) {
|
|
|
350
398
|
}
|
|
351
399
|
if (message.type === 'openFile') {
|
|
352
400
|
const file = allowedFile(repoPath, message.filePath);
|
|
353
|
-
if (file && existsSync(file))
|
|
354
|
-
|
|
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
|
+
}
|
|
355
408
|
return;
|
|
356
409
|
}
|
|
357
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.7",
|
|
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",
|