Skip to content

Commit 8dbeffe

Browse files
committed
Shorten git sync success notifications
1 parent 39b7e4a commit 8dbeffe

6 files changed

Lines changed: 188 additions & 11 deletions

File tree

packages/server/src/__tests__/git/cli.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,10 @@ describe("runGitPush", () => {
918918
const result = await runGitPush(testDir);
919919

920920
expect(result.success).toBe(true);
921+
expect(result.message).toBe("Push completed successfully");
922+
expect(result.remote).toBe("origin");
923+
expect(result.branch).toBe("feature/push-test");
924+
expect(result.updated).toBe(true);
921925

922926
const { stdout: upstreamOutput } = await execFileAsync(
923927
"git",
@@ -934,6 +938,23 @@ describe("runGitPush", () => {
934938
expect(remoteOutput).toContain("feature/push-test");
935939
});
936940

941+
it("returns a stable no-op summary when push is already up to date", async () => {
942+
await writeFile(join(testDir, "README.md"), "init\n");
943+
await execFileAsync("git", ["add", "."], { cwd: testDir });
944+
await execFileAsync("git", ["commit", "-m", "initial"], { cwd: testDir });
945+
await execFileAsync("git", ["remote", "add", "origin", remoteDir], { cwd: testDir });
946+
const defaultBranch = await getCurrentBranch(testDir);
947+
await execFileAsync("git", ["push", "-u", "origin", defaultBranch], { cwd: testDir });
948+
949+
const result = await runGitPush(testDir);
950+
951+
expect(result.success).toBe(true);
952+
expect(result.message).toBe("Push completed successfully");
953+
expect(result.remote).toBe("origin");
954+
expect(result.branch).toBe(defaultBranch);
955+
expect(result.updated).toBe(false);
956+
});
957+
937958
it("persists successful HTTP credentials through the configured credential helper", async () => {
938959
const helperLog = join(testDir, "credential-helper.log");
939960
const helperScript = join(testDir, "credential-helper.sh");
@@ -1250,6 +1271,10 @@ describe("runGitPull", () => {
12501271
const result = await runGitPull(primaryDir);
12511272

12521273
expect(result.success).toBe(true);
1274+
expect(result.message).toBe("Pull completed successfully");
1275+
expect(result.remote).toBe("origin");
1276+
expect(result.branch).toBe(contributorBranch);
1277+
expect(result.updated).toBe(true);
12531278

12541279
const { stdout } = await execFileAsync("git", ["rev-parse", "@{upstream}"], {
12551280
cwd: primaryDir,
@@ -1259,4 +1284,14 @@ describe("runGitPull", () => {
12591284
});
12601285
expect(headStdout.trim()).toBe(stdout.trim());
12611286
});
1287+
1288+
it("returns a stable no-op summary when pull is already up to date", async () => {
1289+
const result = await runGitPull(primaryDir);
1290+
1291+
expect(result.success).toBe(true);
1292+
expect(result.message).toBe("Pull completed successfully");
1293+
expect(result.remote).toBe("origin");
1294+
expect(result.branch).toBe(await getCurrentBranch(primaryDir));
1295+
expect(result.updated).toBe(false);
1296+
});
12621297
});

packages/server/src/git/cli.ts

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ export interface GitHttpAuth {
3434
password: string;
3535
}
3636

37+
interface GitSyncSuccessResult {
38+
success: boolean;
39+
message: string;
40+
remote?: string;
41+
branch?: string;
42+
updated?: boolean;
43+
}
44+
3745
export interface GitAuthFailureDetails {
3846
operation: "push" | "pull" | "fetch";
3947
remote?: string;
@@ -306,7 +314,7 @@ export async function runGitPush(
306314
force?: boolean;
307315
auth?: GitHttpAuth;
308316
}
309-
): Promise<{ success: boolean; message: string }> {
317+
): Promise<GitSyncSuccessResult> {
310318
const args = ["push"];
311319
let remote = options?.remote;
312320
let branch = options?.branch;
@@ -335,6 +343,8 @@ export async function runGitPush(
335343
remote = (await getPreferredRemote(cwd)) ?? undefined;
336344
}
337345

346+
const summaryBranch = branch ?? (await getCurrentBranchName(cwd));
347+
338348
if (remote && branch) {
339349
args.push(remote, `HEAD:${branch}`);
340350
} else if (remote) {
@@ -355,10 +365,13 @@ export async function runGitPush(
355365
await persistGitHttpCredentials(cwd, options.auth, remoteMetadata);
356366
}
357367

358-
// Combine output for message
359-
const message = stdout || stderr || "Push completed successfully";
360-
361-
return { success: true, message };
368+
return {
369+
success: true,
370+
message: "Push completed successfully",
371+
remote,
372+
branch: summaryBranch,
373+
updated: !isPushUpToDate(stdout, stderr),
374+
};
362375
} catch (error) {
363376
throw normalizeGitAuthFailure(error, {
364377
operation: "push",
@@ -381,7 +394,7 @@ export async function runGitPull(
381394
branch?: string;
382395
auth?: GitHttpAuth;
383396
}
384-
): Promise<{ success: boolean; message: string; updatedFiles?: string[] }> {
397+
): Promise<GitSyncSuccessResult & { updatedFiles?: string[] }> {
385398
const args = ["pull"];
386399
let remote = options?.remote;
387400
let branch = options?.branch;
@@ -396,6 +409,8 @@ export async function runGitPull(
396409
remote = (await getPreferredRemote(cwd)) ?? "origin";
397410
}
398411

412+
const summaryBranch = branch ?? (await getCurrentBranchName(cwd));
413+
399414
if (remote && branch) {
400415
args.push(remote, branch);
401416
}
@@ -430,9 +445,14 @@ export async function runGitPull(
430445
}
431446
}
432447

433-
const message = stdout || stderr || "Pull completed successfully";
434-
435-
return { success: true, message, updatedFiles };
448+
return {
449+
success: true,
450+
message: "Pull completed successfully",
451+
remote,
452+
branch: summaryBranch,
453+
updated: !isPullUpToDate(stdout, stderr),
454+
updatedFiles,
455+
};
436456
} catch (error) {
437457
throw normalizeGitAuthFailure(error, {
438458
operation: "pull",
@@ -707,6 +727,24 @@ async function resolveRemoteBranchTarget(
707727
}
708728
}
709729

730+
async function getCurrentBranchName(cwd: string): Promise<string | undefined> {
731+
try {
732+
const { stdout } = await runGit(cwd, ["branch", "--show-current"]);
733+
const branch = stdout.trim();
734+
return branch || undefined;
735+
} catch {
736+
return undefined;
737+
}
738+
}
739+
740+
function isPushUpToDate(stdout: string, stderr: string): boolean {
741+
return /Everything up-to-date/i.test(`${stdout}\n${stderr}`);
742+
}
743+
744+
function isPullUpToDate(stdout: string, stderr: string): boolean {
745+
return /Already up[ -]to[ -]date\.?/i.test(`${stdout}\n${stderr}`);
746+
}
747+
710748
async function getPreferredRemote(cwd: string): Promise<string | null> {
711749
try {
712750
const { stdout } = await runGit(cwd, ["remote"]);

packages/web/src/features/workspace/actions/use-git-actions.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ interface GitCheckoutResult {
4343
interface GitSyncResult {
4444
success: boolean;
4545
message: string;
46+
remote?: string;
47+
branch?: string;
48+
updated?: boolean;
4649
updatedFiles?: string[];
4750
}
4851

@@ -76,6 +79,13 @@ const GIT_OPERATION_LABELS: Record<"push" | "pull" | "fetch", string> = {
7679
fetch: "git.operation_fetch",
7780
};
7881

82+
function formatGitSyncTarget(remote?: string, branch?: string): string | null {
83+
if (remote && branch) {
84+
return `${remote}/${branch}`;
85+
}
86+
return null;
87+
}
88+
7989
export function useGitSyncActions(workspaceId: string) {
8090
const t = useTranslation();
8191
const dispatch = useAtomValue(dispatchCommandAtom);
@@ -109,6 +119,34 @@ export function useGitSyncActions(workspaceId: string) {
109119
[t]
110120
);
111121

122+
const getSuccessToastBody = useCallback(
123+
(
124+
op: "git.push" | "git.pull" | "git.fetch",
125+
result: GitSyncResult,
126+
fallbackSuccessBody?: string
127+
) => {
128+
if (op === "git.fetch") {
129+
return result.message || fallbackSuccessBody;
130+
}
131+
132+
const target = formatGitSyncTarget(result.remote, result.branch);
133+
if (target) {
134+
if (op === "git.push") {
135+
return result.updated
136+
? t("git.push_success_target", { target })
137+
: t("git.sync_up_to_date", { target });
138+
}
139+
140+
return result.updated
141+
? t("git.pull_success_target", { target })
142+
: t("git.sync_up_to_date", { target });
143+
}
144+
145+
return result.message || fallbackSuccessBody;
146+
},
147+
[t]
148+
);
149+
112150
const refreshBranchState = useCallback(async () => {
113151
if (!workspaceId) {
114152
return false;
@@ -261,7 +299,7 @@ export function useGitSyncActions(workspaceId: string) {
261299
pushToast({
262300
kind: "success",
263301
title: options.successTitle,
264-
body: result.data.message || options.fallbackSuccessBody,
302+
body: getSuccessToastBody(op, result.data, options.fallbackSuccessBody),
265303
});
266304

267305
return true;
@@ -272,6 +310,7 @@ export function useGitSyncActions(workspaceId: string) {
272310
[
273311
authPrompt,
274312
dispatch,
313+
getSuccessToastBody,
275314
pushToast,
276315
refreshBranchState,
277316
setFetchState,

packages/web/src/features/workspace/views/shared/git-status-bar.test.tsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,13 @@ describe("GitStatusBar", () => {
7171
const sendCommand = vi.fn().mockImplementation(async (op: string) => {
7272
if (op === "git.push") {
7373
await pushPromise;
74-
return { success: true, message: "Push completed successfully" };
74+
return {
75+
success: true,
76+
message: "Push completed successfully",
77+
remote: "origin",
78+
branch: "main",
79+
updated: true,
80+
};
7581
}
7682

7783
if (op === "git.branches") {
@@ -126,6 +132,7 @@ describe("GitStatusBar", () => {
126132

127133
expect(store.get(gitStateAtomFamily("ws-test"))?.ahead).toBe(0);
128134
expect(store.get(toastsAtom)[0]?.title).toBe("Push completed");
135+
expect(store.get(toastsAtom)[0]?.body).toBe("Pushed to origin/main");
129136
});
130137

131138
it("shows pull confirmation and does not dispatch when cancelled", async () => {
@@ -151,6 +158,58 @@ describe("GitStatusBar", () => {
151158
expect(sendCommand).not.toHaveBeenCalledWith("git.pull", { workspaceId: "ws-test" });
152159
});
153160

161+
it("formats a short localized no-op pull success body", async () => {
162+
const sendCommand = vi.fn().mockImplementation(async (op: string) => {
163+
if (op === "git.pull") {
164+
return {
165+
success: true,
166+
message: "Pull completed successfully",
167+
remote: "origin",
168+
branch: "main",
169+
updated: false,
170+
};
171+
}
172+
173+
if (op === "git.branches") {
174+
return {
175+
current: "main",
176+
branches: [{ name: "main", isRemote: false, isCurrent: true }],
177+
};
178+
}
179+
180+
if (op === "git.status") {
181+
return {
182+
...baseStatus,
183+
behind: 0,
184+
};
185+
}
186+
187+
throw new Error(`Unexpected command: ${op}`);
188+
});
189+
190+
const { store } = renderStatusBar({
191+
locale: "zh",
192+
status: {
193+
...baseStatus,
194+
ahead: 0,
195+
behind: 1,
196+
},
197+
sendCommand,
198+
});
199+
200+
fireEvent.click(screen.getByRole("button", { name: "拉取" }));
201+
const modal = screen.getByText("拉取更改").closest(".modal-card");
202+
expect(modal).not.toBeNull();
203+
fireEvent.click(within(modal as HTMLElement).getByRole("button", { name: "拉取" }));
204+
205+
await waitFor(() => {
206+
expect(store.get(toastsAtom)[0]).toMatchObject({
207+
title: "拉取完成",
208+
body: "origin/main 已是最新",
209+
});
210+
});
211+
});
212+
154213
it("renders sync dialog text actions with shared button compatibility classes", async () => {
155214
const sendCommand = vi.fn();
156215

packages/web/src/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,11 @@
348348
"sync_confirm_helper": "This action updates your current branch and workspace state.",
349349
"push_success_title": "Push completed",
350350
"push_success_body": "Local commits were pushed to the remote.",
351+
"push_success_target": "Pushed to {target}",
351352
"pull_success_title": "Pull completed",
352353
"pull_success_body": "Latest remote commits were pulled into your local branch.",
354+
"pull_success_target": "Pulled updates from {target}",
355+
"sync_up_to_date": "{target} is already up to date",
353356
"push_in_progress": "Pushing...",
354357
"pull_in_progress": "Pulling...",
355358
"push_failed_title": "Push failed",

packages/web/src/locales/zh.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,11 @@
348348
"sync_confirm_helper": "该操作会更新当前分支和工作区状态。",
349349
"push_success_title": "推送完成",
350350
"push_success_body": "本地提交已推送到远端。",
351+
"push_success_target": "已推送到 {target}",
351352
"pull_success_title": "拉取完成",
352353
"pull_success_body": "已拉取远端最新提交到当前分支。",
354+
"pull_success_target": "已从 {target} 拉取更新",
355+
"sync_up_to_date": "{target} 已是最新",
353356
"push_in_progress": "推送中...",
354357
"pull_in_progress": "拉取中...",
355358
"push_failed_title": "推送失败",

0 commit comments

Comments
 (0)