Skip to content
Open
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
100 changes: 100 additions & 0 deletions src/app/repository-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { isDeepStrictEqual } from "util";
import {
mergeSettingsInputs,
normalizeSettings,
type WtSettings,
type WtSettingsInput,
} from "../domain/settings.js";
import { listGitWorktreePaths } from "../infra/git/worktree-repository.js";
import { loadSettingsInputs } from "../infra/storage/settings-store.js";

export interface RepositorySettings {
settings: WtSettings;
worktreeDirRoot: string;
}

interface SettingsInputSource {
input?: WtSettingsInput;
root: string;
}

async function resolveMainWorktreePath(
repoRoot: string
): Promise<string | undefined> {
const worktrees = await listGitWorktreePaths(repoRoot);
return worktrees.find((worktree) => worktree.isMain)?.path;
}

function selectWorktreeDirRoot(
sources: SettingsInputSource[],
fallbackRoot: string
): string {
return (
sources.find((source) => source.input?.worktreeDir !== undefined)?.root ??
fallbackRoot
);
}

function selectSharedSettingsSource(
currentSource: SettingsInputSource,
mainSource: SettingsInputSource
): SettingsInputSource {
if (!currentSource.input) {
return mainSource;
}

if (!mainSource.input || !isDeepStrictEqual(currentSource.input, mainSource.input)) {
return currentSource;
}

return mainSource;
}

export async function loadRepositorySettings(
repoRoot: string
): Promise<RepositorySettings> {
const mainWorktreePath = await resolveMainWorktreePath(repoRoot);
const settingsRoot = mainWorktreePath ?? repoRoot;
const currentInputs = await loadSettingsInputs(repoRoot);
const mainInputs =
settingsRoot === repoRoot
? currentInputs
: await loadSettingsInputs(settingsRoot);
const currentSharedSource: SettingsInputSource = {
input: currentInputs.shared,
root: repoRoot,
};
const mainSharedSource: SettingsInputSource = {
input: mainInputs.shared,
root: settingsRoot,
};
const sharedSource = selectSharedSettingsSource(
currentSharedSource,
mainSharedSource
);
const mainLocalSource: SettingsInputSource | undefined =
settingsRoot === repoRoot
? undefined
: { input: mainInputs.local, root: settingsRoot };
const currentLocalSource: SettingsInputSource = {
input: currentInputs.local,
root: repoRoot,
};
const localSettings = mergeSettingsInputs(
mainLocalSource?.input,
currentLocalSource.input
);
const settings = normalizeSettings(
mergeSettingsInputs(sharedSource.input, localSettings)
);
const worktreeDirRoot = selectWorktreeDirRoot(
[
currentLocalSource,
...(mainLocalSource ? [mainLocalSource] : []),
sharedSource,
],
sharedSource.root
);

return { settings, worktreeDirRoot };
}
8 changes: 5 additions & 3 deletions src/app/use-cases/create-branch-worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
} from "../worktree-creation.js";
import { copyConfiguredPaths } from "../worktree-copy.js";
import { requireRepositoryContext } from "../repository-context.js";
import { loadRepositorySettings } from "../repository-settings.js";
import { loadWorktreeInfos } from "../worktree-catalog.js";
import {
loadSettings,
resolveWorktreeDir,
} from "../../infra/storage/settings-store.js";
import {
Expand Down Expand Up @@ -62,15 +62,17 @@ export async function createBranchWorktree(
);
}

const settings = await loadSettings(context.repoRoot);
const { settings, worktreeDirRoot } = await loadRepositorySettings(
context.repoRoot
);
const { id, idAdjustedFrom } = resolveUniqueWorktreeId(
normalizedBranchName,
worktrees.map((worktree) => worktree.id)
);
const fullId = `${context.repoName}-${id}`;
const worktreeBaseDir = resolveWorktreeDir(
settings.worktreeDir,
context.repoRoot
worktreeDirRoot
);

ensureWorktreeBaseDir(worktreeBaseDir);
Expand Down
8 changes: 5 additions & 3 deletions src/app/use-cases/create-pr-worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
} from "../worktree-creation.js";
import { copyConfiguredPaths } from "../worktree-copy.js";
import { requireRepositoryContext } from "../repository-context.js";
import { loadRepositorySettings } from "../repository-settings.js";
import { loadWorktreeInfos } from "../worktree-catalog.js";
import {
loadSettings,
resolveWorktreeDir,
} from "../../infra/storage/settings-store.js";
import {
Expand Down Expand Up @@ -111,15 +111,17 @@ export async function createPrWorktree(
};
}

const settings = await loadSettings(context.repoRoot);
const { settings, worktreeDirRoot } = await loadRepositorySettings(
context.repoRoot
);
const { id, idAdjustedFrom } = resolveUniqueWorktreeId(
preferredId,
worktrees.map((worktree) => worktree.id)
);
const fullId = `${context.repoName}-${id}`;
const worktreeBaseDir = resolveWorktreeDir(
settings.worktreeDir,
context.repoRoot
worktreeDirRoot
);

ensureWorktreeBaseDir(worktreeBaseDir);
Expand Down
8 changes: 5 additions & 3 deletions src/app/use-cases/create-worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
} from "../worktree-creation.js";
import { copyConfiguredPaths } from "../worktree-copy.js";
import { requireRepositoryContext } from "../repository-context.js";
import { loadRepositorySettings } from "../repository-settings.js";
import {
loadSettings,
resolveWorktreeDir,
} from "../../infra/storage/settings-store.js";
import {
Expand Down Expand Up @@ -89,14 +89,16 @@ export async function createWorktree(
cwd: string = process.cwd()
): Promise<CreateWorktreeResult> {
const context = await requireRepositoryContext(cwd);
const settings = await loadSettings(context.repoRoot);
const { settings, worktreeDirRoot } = await loadRepositorySettings(
context.repoRoot
);
const baseBranch = options.base ?? settings.baseBranch;
const pushRemote = options.push ?? settings.pushRemote;
const id = options.id ?? branchName;
const fullId = `${context.repoName}-${id}`;
const worktreeBaseDir = resolveWorktreeDir(
settings.worktreeDir,
context.repoRoot
worktreeDirRoot
);

ensureWorktreeBaseDir(worktreeBaseDir);
Expand Down
84 changes: 84 additions & 0 deletions src/app/use-cases/get-current-worktree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { WtIssueSettings } from "../../domain/settings.js";
import type { WorktreeInfo } from "../../domain/worktree.js";
import { buildIssueLink } from "../../domain/issue-link.js";
import { findPullRequestLinkForBranch } from "../../infra/github/cli.js";
import { AppError } from "../errors.js";
import { requireRepositoryContext } from "../repository-context.js";
import { loadRepositorySettings } from "../repository-settings.js";
import { loadCurrentWorktreeInfo } from "../worktree-catalog.js";

export interface GetCurrentWorktreeResult {
repoName: string;
worktree: WorktreeInfo;
}

function enrichWorktreeWithIssueLink(
worktree: WorktreeInfo,
issueSettings: WtIssueSettings | undefined
): WorktreeInfo {
if (!issueSettings) {
return worktree;
}

try {
const issueLink = buildIssueLink(worktree.branch, issueSettings);

if (!issueLink) {
return worktree;
}

return {
...worktree,
issueKey: issueLink.key,
issueUrl: issueLink.url,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new AppError(
`Invalid issue.pattern "${issueSettings.pattern}": ${message}`
);
}
}

async function enrichWorktreeWithPullRequest(
repoRoot: string,
worktree: WorktreeInfo
): Promise<WorktreeInfo> {
if (worktree.prUrl || !worktree.branch || worktree.isDetached) {
return worktree;
}

const pullRequest = await findPullRequestLinkForBranch(
repoRoot,
worktree.branch
);

if (!pullRequest) {
return worktree;
}

return {
...worktree,
prNumber: pullRequest.number,
prUrl: pullRequest.url,
};
}

export async function getCurrentWorktree(
cwd: string = process.cwd()
): Promise<GetCurrentWorktreeResult> {
const context = await requireRepositoryContext(cwd);
const { settings } = await loadRepositorySettings(context.repoRoot);
const worktree = enrichWorktreeWithIssueLink(
await enrichWorktreeWithPullRequest(
context.repoRoot,
await loadCurrentWorktreeInfo(context)
),
settings.issue
);

return {
repoName: context.repoName,
worktree,
};
}
4 changes: 2 additions & 2 deletions src/app/use-cases/list-worktrees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { requireRepositoryContext } from "../repository-context.js";
import { AppError } from "../errors.js";
import { buildIssueLinkFromPattern } from "../../domain/issue-link.js";
import { listPullRequestLinks } from "../../infra/github/cli.js";
import { loadSettings } from "../../infra/storage/settings-store.js";
import {
loadWorktreeInfos,
loadWorktreeRemovalInfos,
loadWorktreeStates,
} from "../worktree-catalog.js";
import { loadRepositorySettings } from "../repository-settings.js";
import type { WtIssueSettings } from "../../domain/settings.js";

export interface ListWorktreeInfosOptions {
Expand Down Expand Up @@ -125,7 +125,7 @@ export async function listWorktrees(
cwd: string = process.cwd()
): Promise<ListWorktreesResult> {
const context = await requireRepositoryContext(cwd);
const settings = await loadSettings(context.repoRoot);
const { settings } = await loadRepositorySettings(context.repoRoot);
const worktreesWithIssueLinks = enrichWorktreesWithIssueLinks(
settings.issue,
await loadWorktreeStates(context)
Expand Down
68 changes: 44 additions & 24 deletions src/app/worktree-catalog.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { statSync } from "fs";
import type { RepositoryContext } from "./repository-context.js";
import { AppError } from "./errors.js";
import {
buildWorktreeIdentifiers,
type GitWorktreeRef,
type WorktreeInfo,
type WorktreeMergeStatus,
type WorktreeRemovalInfo,
Expand Down Expand Up @@ -31,37 +33,55 @@ function resolveCreatedAt(
}
}

async function buildWorktreeInfo(
context: RepositoryContext,
worktree: GitWorktreeRef
): Promise<WorktreeInfo> {
const meta = await readWorktreeMeta(worktree.path);
const { id, fullId } = buildWorktreeIdentifiers(
context.repoName,
worktree.path,
meta
);

return {
id,
fullId,
path: worktree.path,
branch: worktree.branch,
isMain: worktree.isMain,
isDetached: worktree.isDetached,
head: worktree.head,
repoName: context.repoName,
createdAt: resolveCreatedAt(worktree.path, meta?.createdAt),
baseBranch: meta?.baseBranch,
baseCommit: meta?.baseCommit,
prNumber: meta?.prNumber,
prUrl: meta?.prUrl,
};
}

export async function loadWorktreeInfos(
context: RepositoryContext
): Promise<WorktreeInfo[]> {
const gitWorktrees = await listGitWorktrees(context.repoRoot);

return Promise.all(
gitWorktrees.map(async (worktree) => {
const meta = await readWorktreeMeta(worktree.path);
const { id, fullId } = buildWorktreeIdentifiers(
context.repoName,
worktree.path,
meta
);
return Promise.all(gitWorktrees.map((worktree) => buildWorktreeInfo(context, worktree)));
}

return {
id,
fullId,
path: worktree.path,
branch: worktree.branch,
isMain: worktree.isMain,
isDetached: worktree.isDetached,
head: worktree.head,
repoName: context.repoName,
createdAt: resolveCreatedAt(worktree.path, meta?.createdAt),
baseBranch: meta?.baseBranch,
baseCommit: meta?.baseCommit,
prNumber: meta?.prNumber,
prUrl: meta?.prUrl,
};
})
export async function loadCurrentWorktreeInfo(
context: RepositoryContext
): Promise<WorktreeInfo> {
const gitWorktrees = await listGitWorktrees(context.repoRoot);
const currentWorktree = gitWorktrees.find(
(worktree) => worktree.path === context.repoRoot
);

if (!currentWorktree) {
throw new AppError("Current directory is not inside a git worktree");
}

return buildWorktreeInfo(context, currentWorktree);
}

export async function loadWorktreeStates(
Expand Down
Loading
Loading