Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/mobile/src/features/threads/git/GitConfirmSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export function GitConfirmSheet(props: GitConfirmSheetProps) {
gitState.selectedThreadBranches.length > 0
? gitState.selectedThreadBranches
: await gitActions.refreshSelectedThreadBranches();
if (branches === null) return;
const newBranchName = resolveAutoFeatureBranchName(
Arr.filterMap(branches, (branch) =>
branch.isRemote ? Result.failVoid : Result.succeed(branch.name),
Expand Down
75 changes: 74 additions & 1 deletion apps/mobile/src/state/use-selected-thread-git-actions.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { AuthOrchestrationOperateScope, AuthSourceControlWriteScope } from "@t3tools/contracts";
import type { VcsListRefsResult } from "@t3tools/contracts";
import { resolveAutoFeatureBranchName } from "@t3tools/shared/git";
import * as Cause from "effect/Cause";
import { AsyncResult } from "effect/reactivity";
import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
Expand All @@ -20,6 +22,8 @@ const state = vi.hoisted(() => ({
results: [] as { type: string; description?: string }[],
afterGitAction: undefined as (() => void) | undefined,
statusRefreshes: 0,
readRefs: vi.fn(),
cachedRefs: [] as VcsListRefsResult["refs"],
}));

vi.mock("react", () => ({
Expand All @@ -46,7 +50,7 @@ vi.mock("./use-selected-thread-worktree", () => ({
}),
}));
vi.mock("./queries", () => ({
useBranches: () => ({ data: { refs: [] }, refresh: () => {} }),
useBranches: () => ({ data: { refs: state.cachedRefs }, refresh: () => {} }),
}));
vi.mock("./use-atom-command", () => ({ useAtomCommand: (command: unknown) => command }));
vi.mock("./atom-registry", () => ({ appAtomRegistry: {} }));
Expand Down Expand Up @@ -79,6 +83,7 @@ vi.mock("./threads", () => ({
}));
vi.mock("./vcs", () => ({
vcsEnvironment: {
readRefs: state.readRefs,
refreshStatus: async () => {
state.statusRefreshes += 1;
return AsyncResult.success({ refName: state.branch });
Expand Down Expand Up @@ -231,3 +236,71 @@ describe("thread Git mutation permissions", () => {
},
);
});

describe("automatic feature branch refs", () => {
const empty: VcsListRefsResult = {
refs: [],
isRepo: true,
hasPrimaryRemote: true,
nextCursor: null,
totalCount: 0,
};

it.each([false, true])("awaits fresh refs before naming, cached: %s", async (cached) => {
const request = Promise.withResolvers<VcsListRefsResult>();
state.cachedRefs = cached
? [
{
name: "old-branch",
isRemote: false,
current: false,
isDefault: false,
worktreePath: null,
},
]
: [];
state.readRefs.mockReturnValue(request.promise.then(AsyncResult.success));
let settled = false;
const refresh = useSelectedThreadGitActions()
.refreshSelectedThreadBranches()
.then((refs) => {
settled = true;
return refs;
});
await Promise.resolve();
expect(settled).toBe(false);
const local = {
name: "feature/update",
isRemote: false,
current: false,
isDefault: false,
worktreePath: null,
};
request.resolve({
...empty,
refs: [
local,
{ ...local, name: "origin/feature/update", isRemote: true, remoteName: "origin" },
],
totalCount: 2,
});
const refs = await refresh;
expect(refs).toEqual([local]);
expect(resolveAutoFeatureBranchName(refs!.map((ref) => ref.name))).toBe("feature/update-2");
expect(state.readRefs).toHaveBeenCalledWith({
environmentId: "environment",
input: { cwd: "/repo", limit: 100, refresh: true },
});
});

it("reports lookup failure instead of returning empty refs", async () => {
state.results = [];
state.readRefs.mockResolvedValue(
AsyncResult.failure(Cause.fail(new Error("Cannot list refs"))),
);
expect(await useSelectedThreadGitActions().refreshSelectedThreadBranches()).toBeNull();
expect(state.results).toEqual([
{ type: "error", title: "Git action failed", description: "Cannot list refs" },
]);
});
});
19 changes: 16 additions & 3 deletions apps/mobile/src/state/use-selected-thread-git-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export function useSelectedThreadGitActions() {
const updateThreadMetadata = useAtomCommand(threadEnvironment.updateMetadata, {
reportFailure: false,
});
const readRefs = useAtomCommand(vcsEnvironment.readRefs, { reportFailure: false });
const refreshStatus = useAtomCommand(vcsEnvironment.refreshStatus, { reportFailure: false });
const switchRef = useAtomCommand(vcsEnvironment.switchRef, { reportFailure: false });
const createRef = useAtomCommand(vcsEnvironment.createRef, { reportFailure: false });
Expand Down Expand Up @@ -206,9 +207,21 @@ export function useSelectedThreadGitActions() {
return result.value;
};

const refreshSelectedThreadBranches = async (): Promise<ReadonlyArray<VcsRef>> => {
branchState.refresh();
return dedupeRemoteBranchesWithLocalMatches(branchState.data?.refs ?? []).filter(
const refreshSelectedThreadBranches = async (): Promise<ReadonlyArray<VcsRef> | null> => {
if (branchTarget.environmentId === null || branchTarget.cwd === null) return null;
const result = await readRefs({
environmentId: branchTarget.environmentId,
input: { cwd: branchTarget.cwd, limit: 100, refresh: true },
});
if (AsyncResult.isFailure(result)) {
const error = Cause.squash(result.cause);
const message = error instanceof Error ? error.message : "Failed to refresh branches.";
setPendingConnectionError(message);
showGitActionResult({ type: "error", title: "Git action failed", description: message });
return null;
}
setPendingConnectionError(null);
return dedupeRemoteBranchesWithLocalMatches(result.value.refs).filter(
(branch) => !branch.isRemote,
);
};
Expand Down
Loading