Skip to content

Commit 91531f1

Browse files
committed
perf(git-worktree): 优化启动阶段 Git 状态探测
- 将 Git 仓库探测收敛为一次 rev-parse 调用,减少启动阶段的 Git 进程开销 - 将项目列表的 Git 状态刷新改为按最近活跃项目优先的分批增量加载 - 在刷新过程中清理失效缓存并逐批回填状态,降低启动时的等待感与界面抖动 本次调整不改变 worktree 的创建、删除与回收业务逻辑,只优化启动阶段最近项目更快显示分支与 worktree 入口的体验。 Signed-off-by: LULU/52628 <526284266@qq.com>
1 parent f220cfe commit 91531f1

File tree

2 files changed

+102
-29
lines changed

2 files changed

+102
-29
lines changed

electron/git/status.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,32 +99,32 @@ export async function getGitDirInfoAsync(args: {
9999
const timeoutMs = Math.max(200, Math.min(30_000, Number(args.timeoutMs ?? 2500)));
100100
const gitPath = args.gitPath;
101101

102-
// 1) 是否位于 work tree 内
103-
const inside = await execGitAsync({
102+
// 1) 一次性读取“是否在 work tree 内”与仓库顶层,减少启动阶段的 git 进程次数
103+
const top = await execGitAsync({
104104
gitPath,
105-
argv: ["-C", abs, "rev-parse", "--is-inside-work-tree"],
105+
argv: ["-C", abs, "rev-parse", "--is-inside-work-tree", "--show-toplevel"],
106106
timeoutMs,
107107
});
108-
if (!inside.ok || inside.stdout.trim() !== "true") {
108+
const topLines = String(top.stdout || "")
109+
.replace(/\r\n/g, "\n")
110+
.replace(/\r/g, "\n")
111+
.split("\n");
112+
const insideLine = String(topLines[0] || "").trim();
113+
if (!top.ok || insideLine !== "true") {
109114
base.isInsideWorkTree = false;
110-
base.error = inside.error || inside.stderr.trim() || base.error;
115+
base.error = top.error || top.stderr.trim() || base.error;
111116
cache.set(key, { at: now, value: base });
112117
return base;
113118
}
114119
base.isInsideWorkTree = true;
115120

116121
// 2) 仓库顶层
117-
const top = await execGitAsync({
118-
gitPath,
119-
argv: ["-C", abs, "rev-parse", "--show-toplevel"],
120-
timeoutMs,
121-
});
122-
if (!top.ok) {
123-
base.error = top.error || top.stderr.trim();
122+
const repoRoot = topLines.slice(1).map((line) => String(line || "").trim()).find(Boolean) || "";
123+
if (!repoRoot) {
124+
base.error = top.error || top.stderr.trim() || "missing repo root";
124125
cache.set(key, { at: now, value: base });
125126
return base;
126127
}
127-
const repoRoot = String(top.stdout || "").trim();
128128
base.repoRoot = repoRoot;
129129
base.isRepoRoot = toFsPathKey(repoRoot) === toFsPathKey(abs);
130130

web/src/App.tsx

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,8 @@ type CloseWorkingTabConfirmState = {
339339
tabName: string;
340340
};
341341

342+
const STARTUP_GIT_STATUS_BATCH_SIZE = 8;
343+
342344
/**
343345
* 中文说明:将耗时(毫秒)格式化为带单位文本(如 `2s`、`1m 05s`、`1h 02m 05s`)。
344346
*/
@@ -435,6 +437,43 @@ function resolveNextWorktreeRemarkIndex(args: {
435437
return maxIndex + 1;
436438
}
437439

440+
/**
441+
* 中文说明:提取项目的“最近活跃时间戳”,用于启动阶段优先刷新更可能被立刻操作的项目。
442+
*/
443+
function resolveProjectGitStatusPriorityTimestamp(project: Project | null | undefined): number {
444+
const lastOpenedAt = Number(project?.lastOpenedAt);
445+
if (Number.isFinite(lastOpenedAt) && lastOpenedAt > 0) return lastOpenedAt;
446+
const createdAt = Number(project?.createdAt);
447+
if (Number.isFinite(createdAt) && createdAt > 0) return createdAt;
448+
return 0;
449+
}
450+
451+
/**
452+
* 中文说明:构建启动阶段的 Git 状态探测队列。
453+
* - 仅保留有效项目 id 与目录路径;
454+
* - 按最近活跃时间优先,尽快回填用户更可能先点开的项目;
455+
* - 同优先级下按路径稳定排序,避免刷新顺序抖动。
456+
*/
457+
function buildStartupGitStatusQueue(projects: Project[]): Array<{ id: string; dir: string }> {
458+
const seen = new Set<string>();
459+
return [...projects]
460+
.sort((left, right) => {
461+
const diff = resolveProjectGitStatusPriorityTimestamp(right) - resolveProjectGitStatusPriorityTimestamp(left);
462+
if (diff !== 0) return diff;
463+
return String(left?.winPath || "").localeCompare(String(right?.winPath || ""), undefined, {
464+
sensitivity: "base",
465+
numeric: true,
466+
});
467+
})
468+
.flatMap((project) => {
469+
const id = String(project?.id || "").trim();
470+
const dir = String(project?.winPath || "").trim();
471+
if (!id || !dir || seen.has(id)) return [];
472+
seen.add(id);
473+
return [{ id, dir }];
474+
});
475+
}
476+
438477
type IdeOpenPrefs = {
439478
mode: "auto" | "builtin" | "custom";
440479
builtinId: BuiltinIdeId;
@@ -1101,28 +1140,62 @@ export default function CodexFlowManagerUI() {
11011140
return () => { try { window.clearTimeout(timer); } catch {} };
11021141
}, [dirTreeStore, projectsHydrated]);
11031142

1104-
// Git 状态:批量刷新(用于分支/工作树识别与“目录缺失”判定)
1143+
// Git 状态:项目列表一到位就开始分批刷新,优先回填最近项目,避免启动时长时间看不到 worktree 入口
11051144
useEffect(() => {
1106-
if (!projectsHydrated) return;
11071145
let cancelled = false;
1146+
const orderedPairs = buildStartupGitStatusQueue(projects);
1147+
const activeIds = new Set(orderedPairs.map((item) => item.id));
1148+
1149+
setGitInfoByProjectId((prev) => {
1150+
const next: Record<string, GitDirInfo> = {};
1151+
let changed = false;
1152+
for (const [projectId, info] of Object.entries(prev || {})) {
1153+
if (!activeIds.has(projectId)) {
1154+
changed = true;
1155+
continue;
1156+
}
1157+
next[projectId] = info;
1158+
}
1159+
return changed ? next : prev;
1160+
});
1161+
1162+
if (orderedPairs.length === 0) {
1163+
return () => { cancelled = true; };
1164+
}
1165+
1166+
/**
1167+
* 中文说明:按批次读取 Git 状态,并将结果增量合并到当前缓存。
1168+
*/
1169+
const loadGitInfoBatch = async (batch: Array<{ id: string; dir: string }>): Promise<void> => {
1170+
if (batch.length === 0) return;
1171+
const res: any = await (window as any).host?.gitWorktree?.statusBatch?.(batch.map((item) => item.dir));
1172+
if (cancelled) return;
1173+
if (!(res && res.ok && Array.isArray(res.items))) return;
1174+
const items = res.items as GitDirInfo[];
1175+
setGitInfoByProjectId((prev) => {
1176+
const next = { ...prev };
1177+
let changed = false;
1178+
for (let i = 0; i < batch.length; i++) {
1179+
const projectId = batch[i]?.id;
1180+
const info = items[i];
1181+
if (!projectId || !info) continue;
1182+
next[projectId] = info;
1183+
changed = true;
1184+
}
1185+
return changed ? next : prev;
1186+
});
1187+
};
1188+
11081189
(async () => {
11091190
try {
1110-
const dirs = projects.map((p) => p.winPath).filter(Boolean);
1111-
const res: any = await (window as any).host?.gitWorktree?.statusBatch?.(dirs);
1112-
if (cancelled) return;
1113-
if (res && res.ok && Array.isArray(res.items)) {
1114-
const next: Record<string, GitDirInfo> = {};
1115-
for (let i = 0; i < projects.length; i++) {
1116-
const p = projects[i];
1117-
const info = (res.items[i] || null) as GitDirInfo | null;
1118-
if (p?.id && info) next[p.id] = info;
1119-
}
1120-
setGitInfoByProjectId(next);
1121-
}
1122-
} catch {}
1191+
for (let start = 0; start < orderedPairs.length; start += STARTUP_GIT_STATUS_BATCH_SIZE) {
1192+
await loadGitInfoBatch(orderedPairs.slice(start, start + STARTUP_GIT_STATUS_BATCH_SIZE));
1193+
if (cancelled) return;
1194+
}
1195+
} catch {}
11231196
})();
11241197
return () => { cancelled = true; };
1125-
}, [projectsHydrated, projects]);
1198+
}, [projects]);
11261199

11271200
/**
11281201
* 中文说明:解析当前节点所属 worktree 组的“管理父节点”。

0 commit comments

Comments
 (0)