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
2 changes: 1 addition & 1 deletion apps/mobile/src/features/review/reviewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as Arr from "effect/Array";
import { pipe } from "effect/Function";
import * as Order from "effect/Order";

export type ReviewSectionKind = "turn" | "working-tree" | "branch-range";
export type ReviewSectionKind = "turn" | ReviewDiffPreviewSource["kind"];

const DIRTY_WORKTREE_SECTION_ID = "git:working-tree";
const DIRTY_WORKTREE_TITLE = "Dirty worktree";
Expand Down
16 changes: 16 additions & 0 deletions apps/server/src/git/GitWorkflowService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type GitPreparePullRequestThreadResult,
type GitPullRequestRefInput,
type VcsPullResult,
type VcsPathsInput,
type VcsRemoveWorktreeInput,
type GitResolvePullRequestResult,
type GitRunStackedActionInput,
Expand Down Expand Up @@ -49,6 +50,9 @@ export class GitWorkflowService extends Context.Service<
readonly invalidateRemoteStatus: (cwd: string) => Effect.Effect<void, never>;
readonly invalidateStatus: (cwd: string) => Effect.Effect<void, never>;
readonly pullCurrentBranch: (cwd: string) => Effect.Effect<VcsPullResult, GitCommandError>;
readonly stagePaths: (input: VcsPathsInput) => Effect.Effect<void, GitCommandError>;
readonly unstagePaths: (input: VcsPathsInput) => Effect.Effect<void, GitCommandError>;
readonly discardPaths: (input: VcsPathsInput) => Effect.Effect<void, GitCommandError>;
readonly runStackedAction: (
input: GitRunStackedActionInput,
options?: GitManager.GitRunStackedActionOptions,
Expand Down Expand Up @@ -277,6 +281,18 @@ export const make = Effect.gen(function* () {
ensureGitCommand("GitWorkflowService.pullCurrentBranch", cwd).pipe(
Effect.andThen(git.pullCurrentBranch(cwd)),
),
stagePaths: (input) =>
ensureGitCommand("GitWorkflowService.stagePaths", input.cwd).pipe(
Effect.andThen(git.stagePaths(input)),
),
unstagePaths: (input) =>
ensureGitCommand("GitWorkflowService.unstagePaths", input.cwd).pipe(
Effect.andThen(git.unstagePaths(input)),
),
discardPaths: (input) =>
ensureGitCommand("GitWorkflowService.discardPaths", input.cwd).pipe(
Effect.andThen(git.discardPaths(input)),
),
runStackedAction: (input, options) =>
ensureGit("GitWorkflowService.runStackedAction", input.cwd).pipe(
Effect.andThen(gitManager.runStackedAction(input, options)),
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
type VcsListRefsInput,
type VcsListRefsResult,
type VcsPullResult,
type VcsPathsInput,
type VcsRemoveWorktreeInput,
type VcsStatusInput,
type VcsStatusResult,
Expand Down Expand Up @@ -245,6 +246,9 @@ export class GitVcsDriver extends Context.Service<
input: VcsListRefsInput,
) => Effect.Effect<VcsListRefsResult, GitCommandError>;
readonly pullCurrentBranch: (cwd: string) => Effect.Effect<VcsPullResult, GitCommandError>;
readonly stagePaths: (input: VcsPathsInput) => Effect.Effect<void, GitCommandError>;
readonly unstagePaths: (input: VcsPathsInput) => Effect.Effect<void, GitCommandError>;
readonly discardPaths: (input: VcsPathsInput) => Effect.Effect<void, GitCommandError>;
readonly createWorktree: (
input: VcsCreateWorktreeInput,
) => Effect.Effect<VcsCreateWorktreeResult, GitCommandError>;
Expand Down
94 changes: 93 additions & 1 deletion apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ it.effect("uses stable diagnostics for every parsed non-repository command", ()
yield* driver.listRefs({ cwd });

assert.deepStrictEqual(commands, [
{ args: ["status", "--porcelain=2", "--branch"], lcAll: "C" },
{
args: ["status", "--porcelain=2", "--branch", "--untracked-files=all"],
lcAll: "C",
},
{ args: ["rev-parse", "--abbrev-ref", "HEAD"], lcAll: "C" },
{ args: ["branch", "--no-color", "--no-column"], lcAll: "C" },
]);
Expand Down Expand Up @@ -386,6 +389,30 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
}),
);

it.effect("separates staged and unstaged review patches", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* writeTextFile(cwd, "README.md", "# staged\n");
yield* driver.stagePaths({ cwd, paths: ["README.md"] });
yield* writeTextFile(cwd, "README.md", "# unstaged\n");

const preview = yield* driver.getReviewDiffPreview({
cwd,
ignoreWhitespace: false,
includeIndexSections: true,
});
const staged = preview.sources.find((source) => source.kind === "staged")?.diff ?? "";
const unstaged = preview.sources.find((source) => source.kind === "unstaged")?.diff ?? "";

assert.include(staged, "+# staged");
assert.notInclude(staged, "+# unstaged");
assert.include(unstaged, "-# staged");
assert.include(unstaged, "+# unstaged");
}),
);

it.effect("honors whitespace filtering for worktree and branch previews", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
Expand Down Expand Up @@ -586,6 +613,71 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
status.workingTree.files.map((file) => file.path),
"feature.ts",
);
assert.deepInclude(
status.workingTree.files.find((file) => file.path === "feature.ts") ?? {},
{ indexStatus: ".", worktreeStatus: "?" },
);
}),
);

it.effect("reports a staged rename once under its destination path", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
const fileSystem = yield* FileSystem.FileSystem;
const pathService = yield* Path.Path;
yield* writeTextFile(cwd, "old/name.ts", "export const value = 1;\n");
yield* git(cwd, ["add", "old/name.ts"]);
yield* git(cwd, ["commit", "-m", "add nested source"]);
yield* fileSystem.makeDirectory(pathService.join(cwd, "new"));
yield* git(cwd, ["mv", "old/name.ts", "new/name.ts"]);

const status = yield* driver.statusDetailsLocal(cwd);

assert.deepStrictEqual(
status.workingTree.files.map((file) => file.path),
["new/name.ts"],
);
assert.deepInclude(status.workingTree.files[0] ?? {}, {
indexStatus: "R",
worktreeStatus: ".",
});
}),
);

it.effect("stages, unstages, and discards explicit worktree paths", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
const fileSystem = yield* FileSystem.FileSystem;
const pathService = yield* Path.Path;
yield* writeTextFile(cwd, "README.md", "# changed\n");

yield* driver.stagePaths({ cwd, paths: ["README.md"] });
const staged = yield* driver.statusDetailsLocal(cwd);
assert.deepInclude(
staged.workingTree.files.find((file) => file.path === "README.md") ?? {},
{ indexStatus: "M", worktreeStatus: "." },
);

yield* driver.unstagePaths({ cwd, paths: ["README.md"] });
const unstaged = yield* driver.statusDetailsLocal(cwd);
assert.deepInclude(
unstaged.workingTree.files.find((file) => file.path === "README.md") ?? {},
{ indexStatus: ".", worktreeStatus: "M" },
);

yield* driver.discardPaths({ cwd, paths: ["README.md"] });
assert.equal(
yield* fileSystem.readFileString(pathService.join(cwd, "README.md")),
"# test\n",
);

yield* writeTextFile(cwd, "scratch.txt", "temporary\n");
yield* driver.discardPaths({ cwd, paths: ["scratch.txt"] });
assert.equal(yield* fileSystem.exists(pathService.join(cwd, "scratch.txt")), false);
}),
);

Expand Down
Loading
Loading