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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vite-plus/test";

import {
filterNewTaskBranches,
resolveNewTaskBranchWorktreePath,
resolveNewTaskBranchLabel,
resolveNewTaskLocalWorkspaceSelection,
Expand Down Expand Up @@ -126,3 +127,26 @@ describe("resolveNewTaskBranchLabel", () => {
).toBe("Choose branch");
});
});

describe("filterNewTaskBranches", () => {
const branches = [
{ name: "main", isRemote: false },
{ name: "Feature/Login-Page", isRemote: false },
{ name: "origin/fix/remote-only", isRemote: true },
];
const search = (query: string) =>
filterNewTaskBranches(branches, query).map((branch) => branch.name);

it("ignores case in both the query and the branch name", () => {
expect(search("feature/login")).toEqual(["Feature/Login-Page"]);
expect(search("MAIN")).toEqual(["main"]);
});

it("keeps remote-only branches searchable", () => {
expect(search("remote-only")).toEqual(["origin/fix/remote-only"]);
});

it("matches a typed space against the dash a branch name uses", () => {
expect(search(" login page ")).toEqual(["Feature/Login-Page"]);
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
12 changes: 12 additions & 0 deletions apps/mobile/src/features/threads/new-task-context-presentation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { sanitizeNewRefName } from "@t3tools/shared/git";

type WorkspaceMode = "local" | "worktree";

export function resolveNewTaskWorkspaceLabel(input: {
Expand Down Expand Up @@ -81,3 +83,13 @@ export function shouldCheckoutNewTaskBranch(input: {
}): boolean {
return input.workspaceMode === "local" && !input.branchIsCurrent && !input.branchWorktreePath;
}

export function filterNewTaskBranches<T extends { readonly name: string }>(
branches: ReadonlyArray<T>,
rawQuery: string,
): ReadonlyArray<T> {
const query = sanitizeNewRefName(rawQuery).toLowerCase();
return query.length === 0
? branches
: branches.filter((branch) => branch.name.toLowerCase().includes(query));
}
25 changes: 12 additions & 13 deletions apps/mobile/src/features/threads/new-task-flow-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
T3_PROJECT_FILE_NAME,
ThreadId,
} from "@t3tools/contracts";
import { sanitizeNewRefName } from "@t3tools/shared/git";
import { resolveProjectSettings } from "@t3tools/shared/projectSettings";
import { parseT3ProjectFile } from "@t3tools/shared/t3ProjectFile";
import * as Arr from "effect/Array";
Expand Down Expand Up @@ -92,6 +93,7 @@ import {
} from "../../state/legacy-plan-mode";
import { useLegacyPlanModeState } from "./use-legacy-plan-mode-enabled";
import {
filterNewTaskBranches,
resolveNewTaskBranchWorktreePath,
resolveNewTaskLocalWorkspaceSelection,
} from "./new-task-context-presentation";
Expand Down Expand Up @@ -129,6 +131,9 @@ export function branchBadgeLabel(input: {
if (input.branch.worktreePath && input.branch.worktreePath !== input.project?.workspaceRoot) {
return "worktree";
}
if (input.branch.isRemote) {
return "remote";
}
if (input.branch.isDefault) {
return "default";
}
Expand Down Expand Up @@ -626,7 +631,8 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
}
replaceComposerDraftAttachments(selectedProjectDraftKey, []);
}, [selectedProjectDraftKey]);
const debouncedBranchQuery = useDebouncedValue(branchQuery, BRANCH_SEARCH_DEBOUNCE_MS);
const branchSearchQuery = sanitizeNewRefName(branchQuery);
const debouncedBranchQuery = useDebouncedValue(branchSearchQuery, BRANCH_SEARCH_DEBOUNCE_MS);
const branchTarget = useMemo(
() => ({
environmentId: selectedProject?.environmentId ?? null,
Expand All @@ -637,7 +643,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
[debouncedBranchQuery, selectedProject?.environmentId, selectedProject?.workspaceRoot],
);
const branchState = usePaginatedBranches(branchTarget);
const branchSearchIsDebouncing = branchQuery.trim() !== debouncedBranchQuery.trim();
const branchSearchIsDebouncing = branchSearchQuery !== debouncedBranchQuery;
const branchesLoading =
branchSearchIsDebouncing || (branchState.isPending && branchState.data === null);
const branchesFetchingNextPage = branchState.isFetchingNextPage;
Expand Down Expand Up @@ -669,17 +675,10 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
);
const currentCheckoutBranchName = projectGitStatus.data?.refName ?? null;

const filteredBranches = useMemo(() => {
const query = branchQuery.trim().toLowerCase();
if (query.length === 0) {
return availableBranches;
}

return pipe(
availableBranches,
Arr.filter((branch) => branch.name.toLowerCase().includes(query)),
);
}, [availableBranches, branchQuery]);
const filteredBranches = useMemo(
() => filterNewTaskBranches(allBranchRefs, branchQuery),
[allBranchRefs, branchQuery],
);

// The composer's draft follows the project it will be sent to: switching
// mid-compose keeps the same draft and moves it, so typed text follows the
Expand Down
15 changes: 2 additions & 13 deletions apps/web/src/components/BranchToolbar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import type {
WorktreeSubmodules,
} from "@t3tools/contracts";
import * as Schema from "effect/Schema";
import { sanitizeNewRefName } from "@t3tools/shared/git";
import { toSortableTimestamp } from "../lib/threadSort";
export {
dedupeRemoteBranchesWithLocalMatches,
deriveLocalBranchNameFromRemoteRef,
sanitizeNewRefName,
} from "@t3tools/shared/git";

export interface EnvironmentOption {
Expand Down Expand Up @@ -275,19 +277,6 @@ export function resolveBranchSelectionTarget(input: {
};
}

// Git rejects ASCII space and the ASCII control characters (tab, newline and
// friends) in ref names, so the picker's "Create new ref" entry can only fail
// for a typed name like "new branch". Replacing runs of those with a dash makes
// the name usable without reimplementing check-ref-format: names invalid for
// other reasons still surface the git error. Only the whitespace git actually
// rejects is replaced — git accepts U+00A0 and friends, and rewriting those
// would silently create a ref the user never asked for. Case and existing
// dashes are left alone, since ref names are case sensitive and consecutive
// dashes are valid.
export function sanitizeNewRefName(rawName: string): string {
return rawName.trim().replace(/[ \t\n\r\f\v]+/g, "-");
}

export function shouldIncludeBranchPickerItem(input: {
itemValue: string;
normalizedQuery: string;
Expand Down
13 changes: 13 additions & 0 deletions packages/shared/src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,19 @@ function deriveLocalBranchNameCandidatesFromRemoteRef(
return [...candidates];
}

// Git rejects ASCII space and the ASCII control characters (tab, newline and
// friends) in ref names, so the picker's "Create new ref" entry can only fail
// for a typed name like "new branch". Replacing runs of those with a dash makes
// the name usable without reimplementing check-ref-format: names invalid for
// other reasons still surface the git error. Only the whitespace git actually
// rejects is replaced — git accepts U+00A0 and friends, and rewriting those
// would silently create a ref the user never asked for. Case and existing
// dashes are left alone, since ref names are case sensitive and consecutive
// dashes are valid.
export function sanitizeNewRefName(rawName: string): string {
return rawName.trim().replace(/[ \t\n\r\f\v]+/g, "-");
}

/**
* Hide `origin/*` remote refs when a matching local refName already exists.
*/
Expand Down
Loading