authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-11 18:51:04-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-11 18:51:04-07:00
logf9019cd63f3bc409344418a662d73a8ef065f439
treeefa80504c28b8cf0af8de9c69cce18a90935d8d0
parentf9372a4c3d7ecb33120922397c26d5cf0c143970
signature Signed by PGP key B5690EEEBB952194

Coalesce status refreshes by remote (#1940)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 93 insertions(+), 79 deletions(-)

apps/server/src/git/Layers/GitCore.test.ts+82-59
...@@ -826,7 +826,7 @@ it.layer(TestLayer)("git integration", (it) => {...@@ -826,7 +826,7 @@ it.layer(TestLayer)("git integration", (it) => {
826 }),826 }),
827 );827 );
828828
829 it.effect("shares upstream refreshes across worktrees that use the same git common dir", () =>829 it.effect("coalesces upstream refreshes across sibling worktrees on the same remote", () =>
830 Effect.gen(function* () {830 Effect.gen(function* () {
831 const ok = (stdout = "") =>831 const ok = (stdout = "") =>
832 Effect.succeed({832 Effect.succeed({
...@@ -845,7 +845,9 @@ it.layer(TestLayer)("git integration", (it) => {...@@ -845,7 +845,9 @@ it.layer(TestLayer)("git integration", (it) => {
845 input.args[2] === "--symbolic-full-name" &&845 input.args[2] === "--symbolic-full-name" &&
846 input.args[3] === "@{upstream}"846 input.args[3] === "@{upstream}"
847 ) {847 ) {
848 return ok("origin/main\n");848 return ok(
849 input.cwd === "/repo/worktrees/pr-123" ? "origin/feature/pr-123\n" : "origin/main\n",
850 );
849 }851 }
850 if (input.args[0] === "remote") {852 if (input.args[0] === "remote") {
851 return ok("origin\n");853 return ok("origin\n");
...@@ -856,10 +858,22 @@ it.layer(TestLayer)("git integration", (it) => {...@@ -856,10 +858,22 @@ it.layer(TestLayer)("git integration", (it) => {
856 if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {858 if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {
857 fetchCount += 1;859 fetchCount += 1;
858 expect(input.cwd).toBe("/repo");860 expect(input.cwd).toBe("/repo");
861 expect(input.args).toEqual([
862 "--git-dir",
863 "/repo/.git",
864 "fetch",
865 "--quiet",
866 "--no-tags",
867 "origin",
868 ]);
859 return ok();869 return ok();
860 }870 }
861 if (input.operation === "GitCore.statusDetails.status") {871 if (input.operation === "GitCore.statusDetails.status") {
862 return ok("# branch.head main\n# branch.upstream origin/main\n# branch.ab +0 -0\n");872 return ok(
873 input.cwd === "/repo/worktrees/pr-123"
874 ? "# branch.head feature/pr-123\n# branch.upstream origin/feature/pr-123\n# branch.ab +0 -0\n"
875 : "# branch.head main\n# branch.upstream origin/main\n# branch.ab +0 -0\n",
876 );
863 }877 }
864 if (878 if (
865 input.operation === "GitCore.statusDetails.unstagedNumstat" ||879 input.operation === "GitCore.statusDetails.unstagedNumstat" ||
...@@ -886,70 +900,80 @@ it.layer(TestLayer)("git integration", (it) => {...@@ -886,70 +900,80 @@ it.layer(TestLayer)("git integration", (it) => {
886 }),900 }),
887 );901 );
888902
889 it.effect("briefly backs off failed upstream refreshes across sibling worktrees", () =>903 it.effect(
890 Effect.gen(function* () {904 "briefly backs off failed upstream refreshes across sibling worktrees on one remote",
891 const ok = (stdout = "") =>905 () =>
892 Effect.succeed({906 Effect.gen(function* () {
893 code: 0,907 const ok = (stdout = "") =>
894 stdout,908 Effect.succeed({
895 stderr: "",909 code: 0,
896 stdoutTruncated: false,910 stdout,
897 stderrTruncated: false,911 stderr: "",
898 });912 stdoutTruncated: false,
913 stderrTruncated: false,
914 });
899915
900 let fetchCount = 0;916 let fetchCount = 0;
901 const core = yield* makeIsolatedGitCore((input) => {917 const core = yield* makeIsolatedGitCore((input) => {
902 if (918 if (
903 input.args[0] === "rev-parse" &&919 input.args[0] === "rev-parse" &&
904 input.args[1] === "--abbrev-ref" &&920 input.args[1] === "--abbrev-ref" &&
905 input.args[2] === "--symbolic-full-name" &&921 input.args[2] === "--symbolic-full-name" &&
906 input.args[3] === "@{upstream}"922 input.args[3] === "@{upstream}"
907 ) {923 ) {
908 return ok("origin/main\n");924 return ok(
909 }925 input.cwd === "/repo/worktrees/pr-123"
910 if (input.args[0] === "remote") {926 ? "origin/feature/pr-123\n"
911 return ok("origin\n");927 : "origin/main\n",
912 }928 );
913 if (input.args[0] === "rev-parse" && input.args[1] === "--git-common-dir") {929 }
914 return ok("/repo/.git\n");930 if (input.args[0] === "remote") {
915 }931 return ok("origin\n");
916 if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {932 }
917 fetchCount += 1;933 if (input.args[0] === "rev-parse" && input.args[1] === "--git-common-dir") {
934 return ok("/repo/.git\n");
935 }
936 if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {
937 fetchCount += 1;
938 return Effect.fail(
939 new GitCommandError({
940 operation: input.operation,
941 command: `git ${input.args.join(" ")}`,
942 cwd: input.cwd,
943 detail: "simulated fetch timeout",
944 }),
945 );
946 }
947 if (input.operation === "GitCore.statusDetails.status") {
948 return ok(
949 input.cwd === "/repo/worktrees/pr-123"
950 ? "# branch.head feature/pr-123\n# branch.upstream origin/feature/pr-123\n# branch.ab +0 -0\n"
951 : "# branch.head main\n# branch.upstream origin/main\n# branch.ab +0 -0\n",
952 );
953 }
954 if (
955 input.operation === "GitCore.statusDetails.unstagedNumstat" ||
956 input.operation === "GitCore.statusDetails.stagedNumstat"
957 ) {
958 return ok();
959 }
960 if (input.operation === "GitCore.statusDetails.defaultRef") {
961 return ok("refs/remotes/origin/main\n");
962 }
918 return Effect.fail(963 return Effect.fail(
919 new GitCommandError({964 new GitCommandError({
920 operation: input.operation,965 operation: input.operation,
921 command: `git ${input.args.join(" ")}`,966 command: `git ${input.args.join(" ")}`,
922 cwd: input.cwd,967 cwd: input.cwd,
923 detail: "simulated fetch timeout",968 detail: "Unexpected git command in refresh failure cooldown test.",
924 }),969 }),
925 );970 );
926 }971 });
927 if (input.operation === "GitCore.statusDetails.status") {
928 return ok("# branch.head main\n# branch.upstream origin/main\n# branch.ab +0 -0\n");
929 }
930 if (
931 input.operation === "GitCore.statusDetails.unstagedNumstat" ||
932 input.operation === "GitCore.statusDetails.stagedNumstat"
933 ) {
934 return ok();
935 }
936 if (input.operation === "GitCore.statusDetails.defaultRef") {
937 return ok("refs/remotes/origin/main\n");
938 }
939 return Effect.fail(
940 new GitCommandError({
941 operation: input.operation,
942 command: `git ${input.args.join(" ")}`,
943 cwd: input.cwd,
944 detail: "Unexpected git command in refresh failure cooldown test.",
945 }),
946 );
947 });
948972
949 yield* core.statusDetails("/repo/worktrees/main");973 yield* core.statusDetails("/repo/worktrees/main");
950 yield* core.statusDetails("/repo/worktrees/pr-123");974 yield* core.statusDetails("/repo/worktrees/pr-123");
951 expect(fetchCount).toBe(1);975 expect(fetchCount).toBe(1);
952 }),976 }),
953 );977 );
954978
955 it.effect("throws when branch does not exist", () =>979 it.effect("throws when branch does not exist", () =>
...@@ -1047,7 +1071,6 @@ it.layer(TestLayer)("git integration", (it) => {...@@ -1047,7 +1071,6 @@ it.layer(TestLayer)("git integration", (it) => {
1047 "--quiet",1071 "--quiet",
1048 "--no-tags",1072 "--no-tags",
1049 remoteName,1073 remoteName,
1050 `+refs/heads/${featureBranch}:refs/remotes/${remoteName}/${featureBranch}`,
1051 ]);1074 ]);
1052 }),1075 }),
1053 );1076 );
apps/server/src/git/Layers/GitCore.ts+11-20
...@@ -78,11 +78,9 @@ type TraceTailState = {...@@ -78,11 +78,9 @@ type TraceTailState = {
78 remainder: string;78 remainder: string;
79};79};
8080
81class StatusUpstreamRefreshCacheKey extends Data.Class<{81class StatusRemoteRefreshCacheKey extends Data.Class<{
82 gitCommonDir: string;82 gitCommonDir: string;
83 upstreamRef: string;
84 remoteName: string;83 remoteName: string;
85 upstreamBranch: string;
86}> {}84}> {}
8785
88interface ExecuteGitOptions {86interface ExecuteGitOptions {
...@@ -919,17 +917,16 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {...@@ -919,17 +917,16 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
919 );917 );
920 });918 });
921919
922 const fetchUpstreamRefForStatus = (920 const fetchRemoteForStatus = (
923 gitCommonDir: string,921 gitCommonDir: string,
924 upstream: { upstreamRef: string; remoteName: string; upstreamBranch: string },922 remoteName: string,
925 ): Effect.Effect<void, GitCommandError> => {923 ): Effect.Effect<void, GitCommandError> => {
926 const refspec = `+refs/heads/${upstream.upstreamBranch}:refs/remotes/${upstream.upstreamRef}`;
927 const fetchCwd =924 const fetchCwd =
928 path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;925 path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;
929 return executeGit(926 return executeGit(
930 "GitCore.fetchUpstreamRefForStatus",927 "GitCore.fetchRemoteForStatus",
931 fetchCwd,928 fetchCwd,
932 ["--git-dir", gitCommonDir, "fetch", "--quiet", "--no-tags", upstream.remoteName, refspec],929 ["--git-dir", gitCommonDir, "fetch", "--quiet", "--no-tags", remoteName],
933 {930 {
934 allowNonZeroExit: true,931 allowNonZeroExit: true,
935 timeoutMs: Duration.toMillis(STATUS_UPSTREAM_REFRESH_TIMEOUT),932 timeoutMs: Duration.toMillis(STATUS_UPSTREAM_REFRESH_TIMEOUT),
...@@ -945,18 +942,14 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {...@@ -945,18 +942,14 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
945 return path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir);942 return path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir);
946 });943 });
947944
948 const refreshStatusUpstreamCacheEntry = Effect.fn("refreshStatusUpstreamCacheEntry")(function* (945 const refreshStatusRemoteCacheEntry = Effect.fn("refreshStatusRemoteCacheEntry")(function* (
949 cacheKey: StatusUpstreamRefreshCacheKey,946 cacheKey: StatusRemoteRefreshCacheKey,
950 ) {947 ) {
951 yield* fetchUpstreamRefForStatus(cacheKey.gitCommonDir, {948 yield* fetchRemoteForStatus(cacheKey.gitCommonDir, cacheKey.remoteName);
952 upstreamRef: cacheKey.upstreamRef,
953 remoteName: cacheKey.remoteName,
954 upstreamBranch: cacheKey.upstreamBranch,
955 });
956 return true as const;949 return true as const;
957 });950 });
958951
959 const statusUpstreamRefreshCache = yield* Cache.makeWith(refreshStatusUpstreamCacheEntry, {952 const statusRemoteRefreshCache = yield* Cache.makeWith(refreshStatusRemoteCacheEntry, {
960 capacity: STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY,953 capacity: STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY,
961 // Keep successful refreshes warm and briefly back off failed refreshes to avoid retry storms.954 // Keep successful refreshes warm and briefly back off failed refreshes to avoid retry storms.
962 timeToLive: (exit) =>955 timeToLive: (exit) =>
...@@ -972,12 +965,10 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {...@@ -972,12 +965,10 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
972 if (!upstream) return;965 if (!upstream) return;
973 const gitCommonDir = yield* resolveGitCommonDir(cwd);966 const gitCommonDir = yield* resolveGitCommonDir(cwd);
974 yield* Cache.get(967 yield* Cache.get(
975 statusUpstreamRefreshCache,968 statusRemoteRefreshCache,
976 new StatusUpstreamRefreshCacheKey({969 new StatusRemoteRefreshCacheKey({
977 gitCommonDir,970 gitCommonDir,
978 upstreamRef: upstream.upstreamRef,
979 remoteName: upstream.remoteName,971 remoteName: upstream.remoteName,
980 upstreamBranch: upstream.upstreamBranch,
981 }),972 }),
982 );973 );
983 });974 });