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
49 changes: 49 additions & 0 deletions .github/actions/setup-jj/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Setup Jujutsu
description: Install the jj CLI from the official jj-vcs release artifacts.

inputs:
version:
description: Jujutsu release tag to install.
required: false
default: v0.40.0

runs:
using: composite
steps:
- name: Install jj
shell: bash
env:
JJ_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail

case "${RUNNER_OS}-${RUNNER_ARCH}" in
Linux-X64)
asset="jj-${JJ_VERSION}-x86_64-unknown-linux-musl.tar.gz"
;;
Linux-ARM64)
asset="jj-${JJ_VERSION}-aarch64-unknown-linux-musl.tar.gz"
;;
macOS-X64)
asset="jj-${JJ_VERSION}-x86_64-apple-darwin.tar.gz"
;;
macOS-ARM64)
asset="jj-${JJ_VERSION}-aarch64-apple-darwin.tar.gz"
;;
*)
echo "Unsupported runner for jj install: ${RUNNER_OS}-${RUNNER_ARCH}" >&2
exit 1
;;
esac

install_dir="${RUNNER_TEMP}/jj-${JJ_VERSION}"
mkdir -p "$install_dir"

curl -fsSL \
"https://github.com/jj-vcs/jj/releases/download/${JJ_VERSION}/${asset}" \
-o "${RUNNER_TEMP}/${asset}"
tar -xzf "${RUNNER_TEMP}/${asset}" -C "$install_dir"

chmod +x "${install_dir}/jj"
echo "$install_dir" >> "$GITHUB_PATH"
"${install_dir}/jj" --version
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ jobs:
with:
node-version-file: package.json

- name: Setup Jujutsu
uses: ./.github/actions/setup-jj

- name: Cache Bun and Turbo
uses: actions/cache@v5
with:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ jobs:
with:
node-version-file: package.json

- name: Setup Jujutsu
uses: ./.github/actions/setup-jj

- name: Install dependencies
run: bun install --frozen-lockfile

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ export const makeOrchestrationIntegrationHarness = (
refreshLocalStatus: () =>
Effect.succeed({
isRepo: true,
kind: "git",
hasPrimaryRemote: false,
isDefaultRef: true,
refName: "main",
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/git/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {

expect(status).toEqual({
isRepo: false,
kind: "unknown",
hasPrimaryRemote: false,
isDefaultRef: false,
refName: null,
Expand Down Expand Up @@ -899,6 +900,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {

expect(status).toEqual({
isRepo: false,
kind: "unknown",
hasPrimaryRemote: false,
isDefaultRef: false,
refName: null,
Expand Down Expand Up @@ -3186,6 +3188,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
expect.objectContaining({
kind: "hook_finished",
hookName: "pre-commit",
exitCode: 0,
}),
expect.objectContaining({
kind: "action_finished",
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/git/GitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
: null;

return {
kind: details.isRepo ? ("git" as const) : ("unknown" as const),
isRepo: details.isRepo,
...(hostingProvider ? { sourceControlProvider: hostingProvider } : {}),
hasPrimaryRemote: details.hasOriginRemote,
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/git/GitWorkflowService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe("GitWorkflowService", () => {
const status = yield* workflow.localStatus({ cwd: "/not-a-repo" });

assert.deepStrictEqual(status, {
kind: "unknown",
isRepo: false,
hasPrimaryRemote: false,
isDefaultRef: false,
Expand All @@ -52,6 +53,7 @@ describe("GitWorkflowService", () => {
const status = yield* workflow.status({ cwd: "/not-a-repo" });

assert.deepStrictEqual(status, {
kind: "unknown",
isRepo: false,
hasPrimaryRemote: false,
isDefaultRef: false,
Expand Down
136 changes: 65 additions & 71 deletions apps/server/src/git/GitWorkflowService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Context, Effect, Layer } from "effect";
import {
GitManagerError,
GitCommandError,
type VcsDriverKind,
type VcsSwitchRefInput,
type VcsSwitchRefResult,
type VcsCreateRefInput,
Expand All @@ -29,6 +30,7 @@ import {
import { GitManager, type GitRunStackedActionOptions } from "./GitManager.ts";
import { GitVcsDriver } from "../vcs/GitVcsDriver.ts";
import { VcsDriverRegistry } from "../vcs/VcsDriverRegistry.ts";
import { mergeGitStatusParts } from "@t3tools/shared/git";

export interface GitWorkflowServiceShape {
readonly status: (
Expand Down Expand Up @@ -91,31 +93,7 @@ const unsupportedGitCommand = (operation: string, cwd: string, detail: string) =
detail,
});

function nonRepositoryLocalStatus(): VcsStatusLocalResult {
return {
isRepo: false,
hasPrimaryRemote: false,
isDefaultRef: false,
refName: null,
hasWorkingTreeChanges: false,
workingTree: {
files: [],
insertions: 0,
deletions: 0,
},
};
}

function nonRepositoryStatus(): VcsStatusResult {
return {
...nonRepositoryLocalStatus(),
hasUpstream: false,
aheadCount: 0,
behindCount: 0,
aheadOfDefaultCount: 0,
pr: null,
};
}
const emptyWorkingTree = { files: [], insertions: 0, deletions: 0 } as const;

function nonRepositoryListRefs(): VcsListRefsResult {
return {
Expand All @@ -127,6 +105,16 @@ function nonRepositoryListRefs(): VcsListRefsResult {
};
}

const nonGitLocalStatus = (kind: VcsDriverKind, isRepo: boolean): VcsStatusLocalResult => ({
kind,
isRepo,
hasPrimaryRemote: false,
isDefaultRef: false,
refName: null,
hasWorkingTreeChanges: false,
workingTree: emptyWorkingTree,
});

export const make = Effect.fn("makeGitWorkflowService")(function* () {
const registry = yield* VcsDriverRegistry;
const git = yield* GitVcsDriver;
Expand Down Expand Up @@ -180,33 +168,6 @@ export const make = Effect.fn("makeGitWorkflowService")(function* () {
}
});

const detectGitRepositoryForStatus = Effect.fn("GitWorkflowService.detectGitRepositoryForStatus")(
function* (operation: string, cwd: string) {
const handle = yield* registry
.detect({ cwd })
.pipe(
Effect.mapError((error) =>
unsupportedGitWorkflow(
operation,
cwd,
error instanceof Error ? error.message : String(error),
),
),
);
if (!handle) {
return false;
}
if (handle.kind !== "git") {
return yield* unsupportedGitWorkflow(
operation,
cwd,
`The ${operation} workflow currently supports Git repositories only; detected ${handle.kind}.`,
);
}
return true;
},
);

const detectGitRepositoryForCommand = Effect.fn(
"GitWorkflowService.detectGitRepositoryForCommand",
)(function* (operation: string, cwd: string) {
Expand Down Expand Up @@ -242,27 +203,60 @@ export const make = Effect.fn("makeGitWorkflowService")(function* () {
(input: Input) =>
ensureGit(operation, input.cwd).pipe(Effect.andThen(run(input)));

return GitWorkflowService.of({
status: (input) =>
detectGitRepositoryForStatus("GitWorkflowService.status", input.cwd).pipe(
Effect.flatMap((isGitRepository) =>
isGitRepository ? gitManager.status(input) : Effect.succeed(nonRepositoryStatus()),
),
),
localStatus: (input) =>
detectGitRepositoryForStatus("GitWorkflowService.localStatus", input.cwd).pipe(
Effect.flatMap((isGitRepository) =>
isGitRepository
? gitManager.localStatus(input)
: Effect.succeed(nonRepositoryLocalStatus()),
const localStatus: GitWorkflowServiceShape["localStatus"] = Effect.fn(
"GitWorkflowService.localStatus",
)(function* (input) {
const handle = yield* registry
.detect({ cwd: input.cwd })
.pipe(
Effect.mapError((error) =>
unsupportedGitWorkflow(
"GitWorkflowService.localStatus",
input.cwd,
error instanceof Error ? error.message : String(error),
),
),
),
remoteStatus: (input) =>
detectGitRepositoryForStatus("GitWorkflowService.remoteStatus", input.cwd).pipe(
Effect.flatMap((isGitRepository) =>
isGitRepository ? gitManager.remoteStatus(input) : Effect.succeed(null),
);
if (!handle) {
return nonGitLocalStatus("unknown", false);
}
if (handle.kind === "git") {
return yield* gitManager.localStatus(input);
}
return nonGitLocalStatus(handle.kind, true);
});

const remoteStatus: GitWorkflowServiceShape["remoteStatus"] = Effect.fn(
"GitWorkflowService.remoteStatus",
)(function* (input) {
const handle = yield* registry
.detect({ cwd: input.cwd })
.pipe(
Effect.mapError((error) =>
unsupportedGitWorkflow(
"GitWorkflowService.remoteStatus",
input.cwd,
error instanceof Error ? error.message : String(error),
),
),
),
);
if (handle?.kind === "git") {
return yield* gitManager.remoteStatus(input);
}
return null;
});

const status: GitWorkflowServiceShape["status"] = Effect.fn("GitWorkflowService.status")(
function* (input) {
const [local, remote] = yield* Effect.all([localStatus(input), remoteStatus(input)]);
return mergeGitStatusParts(local, remote);
},
);

return GitWorkflowService.of({
status,
localStatus,
remoteStatus,
invalidateLocalStatus: gitManager.invalidateLocalStatus,
invalidateRemoteStatus: gitManager.invalidateRemoteStatus,
invalidateStatus: gitManager.invalidateStatus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ describe("CheckpointReactor", () => {
}).pipe(
Effect.as({
isRepo: true,
kind: "git",
hasPrimaryRemote: false,
isDefaultRef: true,
refName: "main",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ describe("ProviderCommandReactor", () => {
const refreshStatus = vi.fn((_: string) =>
Effect.succeed({
isRepo: true,
kind: "git" as const,
hasPrimaryRemote: true,
isDefaultRef: false,
refName: "renamed-branch",
Expand Down
Loading
Loading