dsh-git-branch-manage 1.2.0 → 1.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/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  本文件记录 `dsh-git-branch-manage` 的重要变更。版本号遵循语义化版本。
4
4
 
5
+ ## [1.3.0] - 2026-11-04
6
+
7
+ ### 新增
8
+
9
+ - **网络备用通道与代理配置**(决策 0042、0043):直连发生可重试网络故障时,对 GitHub 地址使用运行时 DoH 查询和真实 Git 请求竞速选择可用 IP;支持显式配置 HTTP(S)/SOCKS 代理,并提供只读系统代理探测建议与「检测网络」诊断。
10
+ - **网络操作状态诊断**(决策 0041):按探测、读取、写入和超时核验分别设置预算;推送前探测远端,推送超时后核验远端 ref,并按失败类型给出建议。
11
+
12
+ ### 变更
13
+
14
+ - **镜像仅作为读取加速**(决策 0044):镜像 fetch/update 失败后自动恢复原始 URL 回直连,再按备用通道链尝试;推送不走镜像,避免将写入交给第三方。
15
+ - SSH 远端不再进行无效的 HTTP 代理/IP 通道探测,错误信息改为提示可执行的 HTTPS 方案。
16
+ - 加速前缀改为拼接完整 GitHub URL(决策 0040),并用真实 Git URL 解析与提取覆盖回归场景。
17
+
18
+ ### 修复
19
+
20
+ - **CI 在非 Windows 上失败**(决策 0046):系统代理探测的用例此前无条件假设 Windows 注册表,在 ubuntu-latest 上必然失败;现按平台分派,Windows 侧保留注册表行为覆盖,非 Windows 侧断言不读取注册表。
21
+
5
22
  ## [1.2.0] - 2026-11-02
6
23
 
7
24
  ### 新增
package/lib/client.js CHANGED
@@ -65,8 +65,8 @@ window.__ModuleLoader__.load({
65
65
  }
66
66
  /**
67
67
  * Normalize an accepted accelerate URL: trim, and append the trailing `/`
68
- * the prefix concatenation requires (`https://gh-proxy.com/` + `o/r.git`
69
- * must stay one valid URL). Callers pass the result to `url.<base>.insteadOf`.
68
+ * the prefix concatenation requires (`https://gh-proxy.com/` + the complete
69
+ * `https://github.com/o/r.git` URL). accelArgs builds the Git rewrite base.
70
70
  * @param value - an accepted (non-empty, http(s), whitespace-free) URL.
71
71
  * @returns the prefix ending in `/`.
72
72
  */
@@ -74,6 +74,37 @@ window.__ModuleLoader__.load({
74
74
  const trimmed = value.trim();
75
75
  return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
76
76
  }
77
+ /** Schemes git's libcurl accepts for an HTTP(S) remote. */
78
+ const PROXY_SCHEMES = [
79
+ "http",
80
+ "https",
81
+ "socks5",
82
+ "socks5h"
83
+ ];
84
+ /**
85
+ * Why this proxy value cannot be used, or undefined when it can.
86
+ *
87
+ * Credentials are rejected on purpose: the value is stored in the plugin
88
+ * config and passed as one `-c` argument, so `user:pass@` would be readable
89
+ * in the settings UI and in any diagnostics (0043).
90
+ * @param value - candidate proxy URL.
91
+ * @returns the user-facing reason, or undefined.
92
+ */
93
+ function proxyUrlError(value) {
94
+ let url;
95
+ try {
96
+ url = new URL(value);
97
+ } catch {
98
+ return "must be an absolute proxy URL such as http://127.0.0.1:7890";
99
+ }
100
+ if (!PROXY_SCHEMES.includes(url.protocol.replace(":", ""))) return `scheme must be one of ${PROXY_SCHEMES.join(", ")}`;
101
+ if (url.hostname === "") return "must name a proxy host";
102
+ if (url.port === "") return "must name a proxy port";
103
+ const port = Number(url.port);
104
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return "proxy port must be 1-65535";
105
+ if (url.username !== "" || url.password !== "") return "credentials in the proxy URL are not supported";
106
+ if (url.pathname !== "" && url.pathname !== "/" || url.search !== "" || url.hash !== "") return "must not carry a path, query, or fragment";
107
+ }
77
108
  //#endregion
78
109
  //#region src/wire.ts
79
110
  /**
@@ -131,6 +162,8 @@ window.__ModuleLoader__.load({
131
162
  const ROUTE_TAG = `${ROUTE_PREFIX}/tag`;
132
163
  /** POST ROUTE_PREFIX/tag-push — push ONE existing local tag to its remote. */
133
164
  const ROUTE_TAG_PUSH = `${ROUTE_PREFIX}/tag-push`;
165
+ /** POST ROUTE_PREFIX/network-probe — read the endpoint through every configured channel (diagnostics; no state change). */
166
+ const ROUTE_NETWORK_PROBE = `${ROUTE_PREFIX}/network-probe`;
134
167
  /** POST ROUTE_PREFIX/credentials — the global per-host credentials, secrets
135
168
  * masked (the stored secret never leaves the host). */
136
169
  const ROUTE_CREDENTIALS = `${ROUTE_PREFIX}/credentials`;
@@ -223,6 +256,15 @@ window.__ModuleLoader__.load({
223
256
  }
224
257
  }
225
258
  /**
259
+ * Probe every network channel (direct, proxy, pinned IP) against the
260
+ * repository's push URL; failures come back per channel rather than as one
261
+ * error, so the popover can show which route works right now (0043).
262
+ * @param repoPath - absolute directory inside the repository.
263
+ */
264
+ function requestNetworkProbe(repoPath) {
265
+ return post(ROUTE_NETWORK_PROBE, { repoPath });
266
+ }
267
+ /**
226
268
  * Create or reuse the worktree for a branch.
227
269
  * @param repoPath - absolute directory inside the repository.
228
270
  * @param branch - branch display name.
@@ -3558,6 +3600,8 @@ window.__ModuleLoader__.load({
3558
3600
  /** The fields this form edits. */
3559
3601
  const ACCEL_ENABLED_FIELD = "cnAccelerate";
3560
3602
  const ACCEL_URL_FIELD = "cnAccelerateUrl";
3603
+ /** The proxy channel's config key (0043); blank = no proxy channel. */
3604
+ const NETWORK_PROXY_FIELD = "networkProxy";
3561
3605
  /**
3562
3606
  * Stages the cn-accelerate edits over the `dsh-git-branch` Host entry.
3563
3607
  *
@@ -3571,6 +3615,7 @@ window.__ModuleLoader__.load({
3571
3615
  listeners = /* @__PURE__ */ new Set();
3572
3616
  enabledDraft;
3573
3617
  urlDraft;
3618
+ proxyDraft;
3574
3619
  saving = false;
3575
3620
  failed = false;
3576
3621
  /**
@@ -3611,6 +3656,11 @@ window.__ModuleLoader__.load({
3611
3656
  this.failed = false;
3612
3657
  this.publish();
3613
3658
  },
3659
+ editNetworkProxy: (text) => {
3660
+ this.proxyDraft = text;
3661
+ this.failed = false;
3662
+ this.publish();
3663
+ },
3614
3664
  restoreAccelDefault: () => {
3615
3665
  this.urlDraft = ACCEL_DEFAULT_URL;
3616
3666
  this.failed = false;
@@ -3618,9 +3668,10 @@ window.__ModuleLoader__.load({
3618
3668
  },
3619
3669
  saveAccel: () => this.save(),
3620
3670
  discardAccel: () => {
3621
- if (this.enabledDraft === void 0 && this.urlDraft === void 0 && !this.failed) return;
3671
+ if (this.enabledDraft === void 0 && this.urlDraft === void 0 && this.proxyDraft === void 0 && !this.failed) return;
3622
3672
  this.enabledDraft = void 0;
3623
3673
  this.urlDraft = void 0;
3674
+ this.proxyDraft = void 0;
3624
3675
  this.failed = false;
3625
3676
  this.publish();
3626
3677
  }
@@ -3632,6 +3683,11 @@ window.__ModuleLoader__.load({
3632
3683
  const value = this.scope.getSnapshot().value?.[ACCEL_URL_FIELD];
3633
3684
  return this.urlDraft ?? value ?? "https://gh-proxy.com/";
3634
3685
  }
3686
+ /** The proxy as the user sees it: the live draft, else the stored value. */
3687
+ effectiveProxyText() {
3688
+ const value = this.scope.getSnapshot().value?.[NETWORK_PROXY_FIELD];
3689
+ return this.proxyDraft ?? (typeof value === "string" ? value : "");
3690
+ }
3635
3691
  /**
3636
3692
  * Write the switch and the normalized URL TOGETHER, then re-seed from what
3637
3693
  * the Host accepted. The URL is validated first — an unusable draft blocks
@@ -3640,11 +3696,14 @@ window.__ModuleLoader__.load({
3640
3696
  * A save that did not land keeps its drafts so the user can correct them.
3641
3697
  */
3642
3698
  async save() {
3643
- if (this.enabledDraft === void 0 && this.urlDraft === void 0 || this.saving) return;
3699
+ if (this.enabledDraft === void 0 && this.urlDraft === void 0 && this.proxyDraft === void 0 || this.saving) return;
3644
3700
  const urlText = this.effectiveUrlText();
3645
3701
  if (accelerateUrlError(urlText) !== void 0) return;
3702
+ const proxyText = this.effectiveProxyText().trim();
3703
+ if (proxyText !== "" && proxyUrlError(proxyText) !== void 0) return;
3646
3704
  const intendedEnabled = this.enabledDraft ?? this.scope.getSnapshot().value?.["cnAccelerate"] ?? false;
3647
3705
  const intendedUrl = normalizeAccelerateUrl(urlText);
3706
+ const writeProxy = this.proxyDraft !== void 0;
3648
3707
  this.saving = true;
3649
3708
  this.failed = false;
3650
3709
  this.publish();
@@ -3652,14 +3711,16 @@ window.__ModuleLoader__.load({
3652
3711
  try {
3653
3712
  await this.scope.set(ACCEL_ENABLED_FIELD, intendedEnabled);
3654
3713
  await this.scope.set(ACCEL_URL_FIELD, intendedUrl);
3714
+ if (writeProxy) await this.scope.set(NETWORK_PROXY_FIELD, proxyText);
3655
3715
  const user = this.userLayer();
3656
- if (user?.["cnAccelerate"] !== intendedEnabled || user?.["cnAccelerateUrl"] !== intendedUrl) landed = false;
3716
+ if (user?.["cnAccelerate"] !== intendedEnabled || user?.["cnAccelerateUrl"] !== intendedUrl || writeProxy && (user?.["networkProxy"] ?? "") !== proxyText) landed = false;
3657
3717
  } catch (_settingsWriteFailure) {
3658
3718
  landed = false;
3659
3719
  }
3660
3720
  if (landed) {
3661
3721
  this.enabledDraft = void 0;
3662
3722
  this.urlDraft = void 0;
3723
+ this.proxyDraft = void 0;
3663
3724
  }
3664
3725
  this.saving = false;
3665
3726
  this.failed = !landed;
@@ -3675,17 +3736,22 @@ window.__ModuleLoader__.load({
3675
3736
  const storedEnabled = snapshot.value?.["cnAccelerate"] ?? false;
3676
3737
  const storedUrl = snapshot.value?.["cnAccelerateUrl"] ?? "https://gh-proxy.com/";
3677
3738
  const urlText = this.effectiveUrlText();
3739
+ const storedProxy = typeof snapshot.value?.["networkProxy"] === "string" ? snapshot.value[NETWORK_PROXY_FIELD] : "";
3740
+ const proxyText = this.effectiveProxyText();
3678
3741
  const enabledDirty = this.enabledDraft !== void 0 && this.enabledDraft !== storedEnabled;
3679
3742
  const urlDirty = this.urlDraft !== void 0 && this.urlDraft !== storedUrl;
3743
+ const proxyDirty = this.proxyDraft !== void 0 && this.proxyDraft !== storedProxy;
3680
3744
  return {
3681
3745
  available: snapshot.status === "ready",
3682
3746
  writable: snapshot.writable,
3683
3747
  enabled: this.enabledDraft ?? storedEnabled,
3684
3748
  urlText,
3685
- dirty: enabledDirty || urlDirty,
3749
+ dirty: enabledDirty || urlDirty || proxyDirty,
3686
3750
  saving: this.saving,
3687
3751
  failed: this.failed,
3688
- urlError: accelerateUrlError(urlText) ?? null
3752
+ urlError: accelerateUrlError(urlText) ?? null,
3753
+ proxyText,
3754
+ proxyError: proxyText.trim() === "" ? null : proxyUrlError(proxyText.trim()) ?? null
3689
3755
  };
3690
3756
  }
3691
3757
  /** Replace the snapshot reference and notify, only when the fact moved. */
@@ -4620,8 +4686,17 @@ window.__ModuleLoader__.load({
4620
4686
  *
4621
4687
  * @module git-worktree/client/panel/CnAcceleratePopover
4622
4688
  */
4689
+ /** Channel id → its display name; the probe rows stay in host order. */
4690
+ const CHANNEL_LABELS = {
4691
+ direct: (t) => t("accel.channel.direct"),
4692
+ proxy: (t) => t("accel.channel.proxy"),
4693
+ ip: (t) => t("accel.channel.ip")
4694
+ };
4623
4695
  function CnAcceleratePopover(props) {
4624
- const { accel, t, pushToast, onClose } = props;
4696
+ const { accel, t, pushToast, cwd, onClose } = props;
4697
+ const [probe, setProbe] = (0, react.useState)(null);
4698
+ const [probing, setProbing] = (0, react.useState)(false);
4699
+ const [probeError, setProbeError] = (0, react.useState)(null);
4625
4700
  const rootRef = (0, react.useRef)(null);
4626
4701
  const state = (0, react.useSyncExternalStore)(accel.store.subscribe, accel.store.getSnapshot);
4627
4702
  (0, react.useEffect)(() => {
@@ -4640,7 +4715,7 @@ window.__ModuleLoader__.load({
4640
4715
  }, [onClose]);
4641
4716
  const locked = state.saving || !state.writable;
4642
4717
  const onSave = (0, react.useCallback)(() => {
4643
- if (state.saving || state.urlError !== null || !state.writable) return;
4718
+ if (state.saving || state.urlError !== null || state.proxyError !== null || !state.writable) return;
4644
4719
  (async () => {
4645
4720
  await accel.saveAccel();
4646
4721
  const after = accel.store.getSnapshot();
@@ -4660,6 +4735,25 @@ window.__ModuleLoader__.load({
4660
4735
  state.writable,
4661
4736
  t
4662
4737
  ]);
4738
+ const onProbe = (0, react.useCallback)(() => {
4739
+ if (cwd === void 0 || probing) return;
4740
+ setProbing(true);
4741
+ setProbeError(null);
4742
+ (async () => {
4743
+ const result = await requestNetworkProbe(cwd);
4744
+ setProbing(false);
4745
+ if (!result.ok) {
4746
+ setProbe(null);
4747
+ setProbeError(result.error);
4748
+ return;
4749
+ }
4750
+ setProbe({
4751
+ url: result.url,
4752
+ channels: result.channels,
4753
+ ...result.systemProxy === void 0 ? {} : { systemProxy: result.systemProxy }
4754
+ });
4755
+ })();
4756
+ }, [cwd, probing]);
4663
4757
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4664
4758
  className: GitHistoryPanel_module_default.cnPop,
4665
4759
  ref: rootRef,
@@ -4715,6 +4809,46 @@ window.__ModuleLoader__.load({
4715
4809
  className: GitHistoryPanel_module_default.cnError,
4716
4810
  children: state.urlError === "empty" ? t("accel.urlEmpty") : t("accel.urlInvalid")
4717
4811
  }) : null,
4812
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4813
+ className: GitHistoryPanel_module_default.cnFieldHead,
4814
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4815
+ className: GitHistoryPanel_module_default.cnFieldLabel,
4816
+ children: t("accel.proxyLabel")
4817
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4818
+ type: "button",
4819
+ className: GitHistoryPanel_module_default.cnLink,
4820
+ disabled: locked || cwd === void 0 || probing,
4821
+ onClick: onProbe,
4822
+ children: probing ? t("accel.probing") : t("accel.probe")
4823
+ })]
4824
+ }),
4825
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4826
+ type: "text",
4827
+ className: state.proxyError !== null ? GitHistoryPanel_module_default.cnInputBad : GitHistoryPanel_module_default.cnInput,
4828
+ value: state.proxyText,
4829
+ spellCheck: false,
4830
+ disabled: locked,
4831
+ placeholder: "http://127.0.0.1:7890",
4832
+ "aria-invalid": state.proxyError !== null,
4833
+ onChange: (event) => accel.editNetworkProxy(event.target.value)
4834
+ }),
4835
+ state.proxyError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4836
+ className: GitHistoryPanel_module_default.cnError,
4837
+ children: t("accel.proxyInvalid")
4838
+ }) : null,
4839
+ probe !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4840
+ className: GitHistoryPanel_module_default.cnHint,
4841
+ children: probe.channels.map((channel) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
4842
+ channel.ok ? "✓ " : "✗ ",
4843
+ CHANNEL_LABELS[channel.channel](t),
4844
+ ` \u00b7 ${String(channel.elapsedMs)} ms`,
4845
+ channel.detail === void 0 ? null : ` \u00b7 ${channel.detail}`
4846
+ ] }, channel.channel))
4847
+ }) : null,
4848
+ probeError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4849
+ className: GitHistoryPanel_module_default.cnError,
4850
+ children: probeError
4851
+ }) : null,
4718
4852
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4719
4853
  className: GitHistoryPanel_module_default.cnFooter,
4720
4854
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -4725,7 +4859,7 @@ window.__ModuleLoader__.load({
4725
4859
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4726
4860
  type: "button",
4727
4861
  className: GitHistoryPanel_module_default.cnSave,
4728
- disabled: state.saving || state.urlError !== null || !state.writable,
4862
+ disabled: state.saving || state.urlError !== null || state.proxyError !== null || !state.writable,
4729
4863
  onClick: onSave,
4730
4864
  children: t("cardSave")
4731
4865
  })]
@@ -7125,6 +7259,7 @@ window.__ModuleLoader__.load({
7125
7259
  accel,
7126
7260
  t,
7127
7261
  pushToast,
7262
+ cwd,
7128
7263
  onClose: closeAccel
7129
7264
  }) : null]
7130
7265
  }),
@@ -7946,7 +8081,7 @@ window.__ModuleLoader__.load({
7946
8081
  children: t("cardDescription")
7947
8082
  });
7948
8083
  const lockInput = !state.writable;
7949
- const lockActions = !state.dirty && !accelState.dirty || state.saving || accelState.saving || !state.keepWorktreesValid || accelState.urlError !== null;
8084
+ const lockActions = !state.dirty && !accelState.dirty || state.saving || accelState.saving || !state.keepWorktreesValid || accelState.urlError !== null || accelState.proxyError !== null;
7950
8085
  const legacyRootText = storage?.legacyRoot ?? (state.rootDir === "" ? "—" : state.rootDir);
7951
8086
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7952
8087
  className: GitWorktreeCard_module_default.page,
@@ -8170,6 +8305,33 @@ window.__ModuleLoader__.load({
8170
8305
  }) : null
8171
8306
  ]
8172
8307
  }),
8308
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
8309
+ className: `${GitWorktreeCard_module_default.field} ${GitWorktreeCard_module_default.copyFilesRow}`,
8310
+ htmlFor: "git-worktree-card-network-proxy",
8311
+ children: [
8312
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8313
+ className: GitWorktreeCard_module_default.fieldLabel,
8314
+ children: t("accel.proxyLabel")
8315
+ }),
8316
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8317
+ id: "git-worktree-card-network-proxy",
8318
+ className: GitWorktreeCard_module_default.accelInput,
8319
+ type: "text",
8320
+ spellCheck: false,
8321
+ placeholder: "http://127.0.0.1:7890",
8322
+ disabled: lockInput || accelState.saving,
8323
+ value: accelState.proxyText,
8324
+ onChange: (event) => {
8325
+ props.editNetworkProxy(event.target.value);
8326
+ }
8327
+ }),
8328
+ accelState.proxyError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8329
+ className: GitWorktreeCard_module_default.keepBad,
8330
+ role: "alert",
8331
+ children: t("accel.proxyInvalid")
8332
+ }) : null
8333
+ ]
8334
+ }),
8173
8335
  history.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8174
8336
  className: `${GitWorktreeCard_module_default.field} ${GitWorktreeCard_module_default.historyRow}`,
8175
8337
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -8479,11 +8641,18 @@ window.__ModuleLoader__.load({
8479
8641
  "panel.stUnmerged": "conflict",
8480
8642
  "panel.cnAccelerate": "China acceleration",
8481
8643
  "accel.enable": "Enable acceleration",
8482
- "accel.hint": "Accelerates https github.com remotes for fetch/update only; push is never proxied. The mirror sees repository content — do not use it for private or sensitive repositories.",
8644
+ "accel.hint": "Enter a proxy prefix (e.g. https://gh-proxy.com/); fetch/update appends the complete GitHub URL. Push stays direct. The mirror sees repository content — do not use it for private or sensitive repositories.",
8483
8645
  "accel.urlLabel": "Acceleration URL",
8484
8646
  "accel.restoreDefault": "Restore default",
8485
8647
  "accel.urlInvalid": "Enter an http(s):// URL",
8486
8648
  "accel.urlEmpty": "Enter an acceleration URL",
8649
+ "accel.proxyLabel": "Proxy (optional)",
8650
+ "accel.proxyInvalid": "Enter an http(s)/socks5 proxy URL as host:port, without credentials",
8651
+ "accel.probe": "Test network",
8652
+ "accel.probing": "Testing...",
8653
+ "accel.channel.direct": "Direct",
8654
+ "accel.channel.proxy": "Proxy",
8655
+ "accel.channel.ip": "Pinned IP",
8487
8656
  "accel.saveFailed": "The change did not save. Try again.",
8488
8657
  "accel.savedOn": "China acceleration enabled: {url}",
8489
8658
  "accel.savedOff": "China acceleration disabled",
@@ -8769,11 +8938,18 @@ window.__ModuleLoader__.load({
8769
8938
  "panel.stUnmerged": "冲突",
8770
8939
  "panel.cnAccelerate": "国内加速",
8771
8940
  "accel.enable": "启用加速",
8772
- "accel.hint": "仅加速 https 的 github.com 仓库地址,作用于提取/更新;推送不走加速。第三方加速站可看到仓库内容,请勿用于私有/敏感仓库。",
8941
+ "accel.hint": "填写加速站前缀(如 https://gh-proxy.com/),提取/更新时会在其后拼接完整 GitHub 地址;推送不走加速。第三方加速站可看到仓库内容,请勿用于私有/敏感仓库。",
8773
8942
  "accel.urlLabel": "加速地址",
8774
8943
  "accel.restoreDefault": "恢复默认",
8775
8944
  "accel.urlInvalid": "请输入以 http(s):// 开头的地址",
8776
8945
  "accel.urlEmpty": "请输入加速地址",
8946
+ "accel.proxyLabel": "代理(可选)",
8947
+ "accel.proxyInvalid": "请输入 主机:端口 形式的 http(s)/socks5 代理地址,且不带账号密码",
8948
+ "accel.probe": "检测网络",
8949
+ "accel.probing": "检测中...",
8950
+ "accel.channel.direct": "直连",
8951
+ "accel.channel.proxy": "代理",
8952
+ "accel.channel.ip": "固定 IP",
8777
8953
  "accel.saveFailed": "保存未生效,请重试。",
8778
8954
  "accel.savedOn": "已启用国内加速:{url}",
8779
8955
  "accel.savedOff": "已关闭国内加速",
package/lib/git.d.ts CHANGED
@@ -11,6 +11,8 @@ export interface ExecResult {
11
11
  stderr: string;
12
12
  /** Node-level failure text when the process never produced stderr. */
13
13
  errorMessage?: string;
14
+ /** True when the executor killed the process at its timeout budget. */
15
+ timedOut?: boolean;
14
16
  }
15
17
  /**
16
18
  * Process executor seam. Runs `file args` with cwd set, captures output as
@@ -19,14 +21,25 @@ export interface ExecResult {
19
21
  export type Exec = (file: string, args: readonly string[], options: {
20
22
  cwd: string;
21
23
  timeoutMs?: number;
24
+ env?: Record<string, string | undefined>;
22
25
  }) => Promise<ExecResult>;
23
26
  /** Failure of one git invocation: non-zero exit with the stderr text. */
24
27
  export declare class GitError extends Error {
25
28
  readonly args: readonly string[];
26
29
  readonly code: number;
27
30
  readonly stderr: string;
28
- constructor(args: readonly string[], code: number, stderr: string);
31
+ /** True when the executor killed the command at its budget — no verdict. */
32
+ readonly timedOut: boolean;
33
+ constructor(args: readonly string[], code: number, stderr: string,
34
+ /** True when the executor killed the command at its budget — no verdict. */
35
+ timedOut?: boolean);
29
36
  }
37
+ /**
38
+ * Whether an endpoint speaks the SSH transport (`ssh://…`, `git@host:…`, or
39
+ * any `user@host:path` scp-style URL) — none of the network channels apply.
40
+ * @param url - a remote URL as git printed it, or undefined.
41
+ */
42
+ export declare function isSshEndpoint(url: string | undefined): boolean;
30
43
  /** Facts the status route assembles: branches, worktrees, repo identity. */
31
44
  export interface RepoFacts {
32
45
  /** Absolute main worktree path (`dirname` of `--git-common-dir`, not the
@@ -58,15 +71,15 @@ export interface RepoFacts {
58
71
  /** Default hard timeout for one git call: generous for every LOCAL
59
72
  * operation (status/log/commit/worktree) while still bounding hangs. */
60
73
  export declare const GIT_TIMEOUT_MS = 20000;
61
- /** Network operations (fetch/push) get a far wider budget: a slow link with
62
- * a real pack transfer outruns the local default, and a killed push may have
63
- * ALREADY updated the remote refs — an error toast for a push that actually
64
- * landed. 120s still bounds a hung credential prompt. */
65
- export declare const NETWORK_TIMEOUT_MS = 120000;
74
+ /** Network budgets live in `network.ts` (probe/fetch/push/verify): a slow
75
+ * link with a real pack transfer outruns the local default, and a killed
76
+ * push may have ALREADY updated the remote refs — an error toast for a push
77
+ * that actually landed. */
66
78
  /** Real executor over node:child_process with a hard timeout. */
67
79
  export declare function childProcessExec(file: string, args: readonly string[], options: {
68
80
  cwd: string;
69
81
  timeoutMs?: number;
82
+ env?: Record<string, string | undefined>;
70
83
  }): Promise<ExecResult>;
71
84
  /** Directory-existence seam over the parsed worktree registrations. */
72
85
  export type DirExists = (path: string) => boolean;
@@ -256,7 +269,7 @@ export declare function accelArgs(prefix: string | undefined): readonly string[]
256
269
  * @param authArgs - optional per-host credential `-c` pairs (see
257
270
  * `credentials.ts`); prepended before the subcommand like the accel rewrite.
258
271
  */
259
- export declare function fetchAll(exec: Exec, repoRoot: string, accelPrefix?: string, authArgs?: readonly string[]): Promise<void>;
272
+ export declare function fetchAll(exec: Exec, repoRoot: string, accelPrefix?: string, authArgs?: readonly string[], options?: NetworkOptions): Promise<void>;
260
273
  /** Outcome of {@link updateBranch}: the branch touched and whether it moved. */
261
274
  export interface UpdateOutcome {
262
275
  branch: string;
@@ -283,7 +296,62 @@ export declare function updateBranch(exec: Exec, cwd: string, accelPrefix?: stri
283
296
  export interface PushOutcome {
284
297
  branch: string;
285
298
  pushed: boolean;
299
+ /** Set when the push ran out of time but the remote ref already matches
300
+ * the local HEAD — the push landed, the connection just did not say so. */
301
+ note?: 'timed-out-but-landed';
302
+ }
303
+ /** The endpoint a write would use, resolved so it can be probed first. */
304
+ export interface PushTarget {
305
+ /** Remote name taken from the upstream (`origin`). */
306
+ remote: string;
307
+ /** The URL `git push` uses (a separate `pushurl` is respected); absent
308
+ * when git could not name one, in which case the remote name is the
309
+ * endpoint and the probe resolves it the same way push does. */
310
+ url?: string;
311
+ }
312
+ /** Per-command routing for a network command (a proxy's `-c` pairs). */
313
+ export interface NetworkOptions {
314
+ /** Argv placed before the subcommand that selects a network channel;
315
+ * empty/absent = the direct connection. */
316
+ channelArgs?: readonly string[];
317
+ /**
318
+ * Consulted ONLY after the direct channel's probe failed in a way another
319
+ * channel could plausibly fix (unreachable/reset/timeout), in order, until
320
+ * one of them answers argv that ALSO passes its own probe. Each provider
321
+ * answers undefined when it does not apply (a mirror URL, an unconfigured
322
+ * proxy) — that is a skip, not a failure. Empty/absent = direct only.
323
+ */
324
+ fallbackChannels?: readonly ChannelProvider[];
286
325
  }
326
+ /** Answers the argv for one replacement channel, or undefined when it does not apply. */
327
+ export type ChannelProvider = (endpoint: string) => Promise<readonly string[] | undefined>;
328
+ /**
329
+ * Resolve where a write to `@{u}` would go, WITHOUT sending anything.
330
+ * `remote get-url --push` is the one source of truth for the endpoint: a
331
+ * remote may configure a separate `pushurl`, and probing the fetch URL
332
+ * would validate a host the push never talks to.
333
+ * @param exec - executor seam.
334
+ * @param cwd - directory whose checked-out branch supplies the upstream.
335
+ * @param authArgs - per-host credential argv (kept for symmetry; `rev-parse`
336
+ * and `remote` are local and never authenticate).
337
+ * @returns the remote (and its push URL when git names one), or undefined
338
+ * when the branch has no upstream — that case belongs to git's own message.
339
+ */
340
+ export declare function resolvePushTarget(exec: Exec, cwd: string, authArgs?: readonly string[]): Promise<PushTarget | undefined>;
341
+ /**
342
+ * Reachability probe for a write: a READ of the push endpoint. A host that
343
+ * cannot answer `ls-remote` cannot receive a pack either, so failing here
344
+ * costs the probe budget (10s) instead of the push budget (600s), and the
345
+ * caller learns which endpoint was unreachable before any object moved.
346
+ * Terminal prompts are disabled for the probe: a probe must fail, never
347
+ * block on a credential dialog the user cannot see.
348
+ * @param exec - executor seam.
349
+ * @param cwd - any directory inside the repository.
350
+ * @param target - endpoint from {@link resolvePushTarget}.
351
+ * @param options - channel argv (proxy) to route the probe the same way.
352
+ * @param authArgs - per-host credential argv.
353
+ */
354
+ export declare function probePushTarget(exec: Exec, cwd: string, target: PushTarget, options?: NetworkOptions, authArgs?: readonly string[]): Promise<void>;
287
355
  /**
288
356
  * Push the branch checked out by `cwd` to its upstream (`@{u}`) — the
289
357
  * counterpart of {@link updateBranch}. Deliberately a bare `git push`: the
@@ -298,11 +366,13 @@ export interface PushOutcome {
298
366
  * pushed content to a third party.
299
367
  * @param exec - executor seam.
300
368
  * @param cwd - directory whose checked-out branch is pushed.
369
+ * @param authArgs - per-host credential argv.
370
+ * @param options - channel argv (proxy) applied to the probe and the push.
301
371
  * @returns the branch and whether the remote-tracking ref changed. A branch
302
372
  * without an upstream can still be handed to `git push`, so Git's own
303
373
  * push-specific diagnostic is preserved; in that case `pushed` is false.
304
374
  */
305
- export declare function pushBranch(exec: Exec, cwd: string, authArgs?: readonly string[]): Promise<PushOutcome>;
375
+ export declare function pushBranch(exec: Exec, cwd: string, authArgs?: readonly string[], options?: NetworkOptions): Promise<PushOutcome>;
306
376
  /** Every LOCAL tag name of the repository, in git's own sort order. */
307
377
  export declare function listTagNames(exec: Exec, cwd: string): Promise<string[]>;
308
378
  /** Where a one-off push should go, or why no remote could be picked. */
@@ -328,6 +398,9 @@ export interface TagPushOutcome {
328
398
  tag: string;
329
399
  remote: string;
330
400
  pushed: boolean;
401
+ /** Set when the push timed out but the remote tag already names the local
402
+ * object — landed, with the connection gone before it could say so. */
403
+ note?: 'timed-out-but-landed';
331
404
  }
332
405
  /**
333
406
  * Push ONE existing local tag to `remote` (`git push <remote>
@@ -353,7 +426,7 @@ export interface TagPushOutcome {
353
426
  * @param remote - the remote to publish to (see {@link resolvePushRemote}).
354
427
  * @returns the tag, the remote, and whether the push had to run.
355
428
  */
356
- export declare function pushTag(exec: Exec, cwd: string, name: string, remote: string, authArgs?: readonly string[]): Promise<TagPushOutcome>;
429
+ export declare function pushTag(exec: Exec, cwd: string, name: string, remote: string, authArgs?: readonly string[], options?: NetworkOptions): Promise<TagPushOutcome>;
357
430
  /** Guard for route inputs: a non-empty absolute directory path. */
358
431
  export declare function isAbsoluteDir(value: string): boolean;
359
432
  /**