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
5 changes: 5 additions & 0 deletions .changeset/fuzzy-base-branch-ref.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@leesangb/wt": patch
---

Allow the Herdr base-branch picker to accept an exact existing Git ref even when fzf has no displayed match.
5 changes: 3 additions & 2 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ wt herdr install
새 worktree 액션은 새 브랜치에 일반 텍스트 입력을, base branch에 `fzf`를
사용합니다. Checkout은 로컬 브랜치를 보여주고, PR은 열린 GitHub PR의 제목,
작성자, head branch, Draft 상태를 보여줍니다. 어느 단계에서든 `Esc`를 누르면
취소됩니다. `fzf`가 `PATH`에 있어야 하며 PR picker에는 인증된 GitHub CLI
(`gh`)가 필요합니다.
취소됩니다. base branch picker에서는 `origin/main`처럼 실제로 존재하는 ref를
정확히 입력하면 표시된 항목과 매칭되지 않아도 선택됩니다. `fzf`가 `PATH`에
있어야 하며 PR picker에는 인증된 GitHub CLI (`gh`)가 필요합니다.

새로 연 worktree Space는 연결된 Pull Request와 CI 상태를 자동으로 감시합니다.
확장된 Space 행에 메타데이터 토큰을 추가하세요:
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ The plugin provides these actions:
The new-worktree action uses a text prompt for the new branch and `fzf` for the
base branch. Checkout lists local branches, while PR lists open GitHub pull
requests with title, author, head branch, and draft status. Press `Esc` at any
step to cancel. The plugin requires `fzf` on `PATH`, and the PR picker requires
an authenticated GitHub CLI (`gh`).
step to cancel. In the base-branch picker, an exact existing ref such as
`origin/main` is accepted even when it does not match the displayed choices.
The plugin requires `fzf` on `PATH`, and the PR picker requires an authenticated
GitHub CLI (`gh`).

Newly opened worktree Spaces automatically watch their linked pull request and
CI status. Add the metadata tokens to the expanded Space rows:
Expand Down
26 changes: 26 additions & 0 deletions src/herdr-plugin/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
pullRequestRefreshIntervalMs,
pullRequestStatusToken,
refreshMetadataTtlMs,
resolveBaseBranchSelection,
streamAndCaptureOutput,
} from "./index.js";

Expand Down Expand Up @@ -73,6 +74,31 @@ describe("wt Herdr plugin", () => {
]);
});

test("accepts an exact existing base branch ref when fzf has no match", () => {
const refs =
"main\nfeature/local\norigin/HEAD\norigin/main\norigin/release/1.0\n";

expect(resolveBaseBranchSelection(refs, "origin/main\n", 1)).toBe(
"origin/main"
);
expect(
resolveBaseBranchSelection(refs, "origin/release/1.0\n", 1)
).toBe("origin/release/1.0");
expect(
resolveBaseBranchSelection(refs, "origin/missing\n", 1)
).toBeUndefined();
expect(
resolveBaseBranchSelection(
refs,
"release/1.0\nrelease/1.0\torigin/release/1.0\n",
0
)
).toBe("origin/release/1.0");
expect(
resolveBaseBranchSelection(refs, "origin/main\n", 130)
).toBeUndefined();
});

test("formats pull requests with searchable metadata and a hidden number", () => {
const [choice] = buildPullRequestChoices(
JSON.stringify([
Expand Down
60 changes: 55 additions & 5 deletions src/herdr-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ interface BranchRef {
local: boolean;
}

interface FzfResult {
output: string;
exitCode: number;
}

interface PullRequest {
number: number;
title: string;
Expand Down Expand Up @@ -379,6 +384,32 @@ export function buildBaseBranchChoices(refs: string): BranchRef[] {
return [...choices.values()].sort((a, b) => a.name.localeCompare(b.name));
}

export function resolveBaseBranchSelection(
refs: string,
output: string,
exitCode: number
): string | undefined {
const lines = output.trimEnd().split(/\r?\n/);

if (exitCode === 0) {
return lines[1]?.split("\t")[1];
}

if (exitCode !== 1) return undefined;

const query = lines[0]?.trim();
if (!query) return undefined;

const existingRefs = new Set(
refs
.split(/\r?\n/)
.map((ref) => ref.trim())
.filter((ref) => ref && !ref.endsWith("/HEAD"))
);

return existingRefs.has(query) ? query : undefined;
}

function fitColumn(value: string, width: number): string {
if (Bun.stringWidth(value) <= width) {
return value + " ".repeat(width - Bun.stringWidth(value));
Expand Down Expand Up @@ -480,11 +511,11 @@ export function closedLinkedWorktrees(json: string): Array<{
.map(({ label, path }) => ({ label, path }));
}

async function fzf(
async function runFzf(
args: string[],
input: string,
cwd: string
): Promise<string | undefined> {
): Promise<FzfResult> {
const proc = spawn(["fzf", ...args], {
cwd,
stdin: "pipe",
Expand All @@ -497,6 +528,16 @@ async function fzf(
const output = await new Response(proc.stdout).text();
const exitCode = await proc.exited;

return { output, exitCode };
}

async function fzf(
args: string[],
input: string,
cwd: string
): Promise<string | undefined> {
const { output, exitCode } = await runFzf(args, input, cwd);

if (exitCode === 130 || exitCode === 1) return undefined;
if (exitCode !== 0) throw new Error(`fzf exited with code ${exitCode}`);
return output.trim();
Expand Down Expand Up @@ -560,18 +601,27 @@ async function pickBaseBranch(cwd: string): Promise<string | undefined> {
cwd
);
const choices = buildBaseBranchChoices(refs);
const selected = await fzf(
const result = await runFzf(
[
"--delimiter=\t",
"--with-nth=1",
"--print-query",
"--prompt=Base branch> ",
"--header=Enter: select · Esc: cancel",
"--header=Enter: select or accept exact ref · Esc: cancel",
],
choices.map(({ name, ref }) => `${name}\t${ref}`).join("\n"),
cwd
);

return selected?.split("\t")[1];
if (
result.exitCode !== 0 &&
result.exitCode !== 1 &&
result.exitCode !== 130
) {
throw new Error(`fzf exited with code ${result.exitCode}`);
}

return resolveBaseBranchSelection(refs, result.output, result.exitCode);
}

async function pickLocalBranch(cwd: string): Promise<string | undefined> {
Expand Down
Loading