Skip to content

Commit 3aca85b

Browse files
skulidropekclaude
andcommitted
fix(state-repo): use main as default branch and set git identity
- git init now uses --initial-branch=main to avoid creating master - gitBaseEnv sets GIT_AUTHOR/COMMITTER_NAME/EMAIL to prevent "Author identity unknown" errors on commit and merge - Extract adoptRemoteHistoryIfOrphan to state-repo/adopt-remote.ts to satisfy max-lines lint rule Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a878bad commit 3aca85b

3 files changed

Lines changed: 69 additions & 58 deletions

File tree

packages/lib/src/usecases/state-repo.ts

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Effect, pipe } from "effect"
66
import { runCommandExitCode } from "../shell/command-runner.js"
77
import { CommandFailedError } from "../shell/errors.js"
88
import { defaultProjectsRoot } from "./menu-helpers.js"
9+
import { adoptRemoteHistoryIfOrphan } from "./state-repo/adopt-remote.js"
910
import { autoSyncEnvKey, autoSyncStrictEnvKey, isAutoSyncEnabled, isTruthyEnv } from "./state-repo/env.js"
1011
import {
1112
git,
@@ -184,7 +185,7 @@ const initRepoIfNeeded = (
184185
return
185186
}
186187

187-
yield* _(git(root, ["init"], gitBaseEnv))
188+
yield* _(git(root, ["init", "--initial-branch=main"], gitBaseEnv))
188189
}).pipe(Effect.asVoid)
189190

190191
const ensureOriginRemote = (
@@ -211,62 +212,6 @@ const checkoutBranchBestEffort = (
211212
yield* _(Effect.logWarning(`git checkout -B ${repoRef} failed (exit ${checkoutExit})`))
212213
})
213214

214-
// CHANGE: align local history with remote when histories have no common ancestor
215-
// WHY: prevents creation of new branches when local repo was git-init'd without cloning (divergent root commits)
216-
// QUOTE(ТЗ): "у нас должна быть единая система облака в виде .docker-git. Новая ветка открывается только тогда когда не возможно исправить конфликт и сделать push в main"
217-
// REF: issue-141
218-
// PURITY: SHELL
219-
// EFFECT: Effect<void, CommandFailedError | PlatformError, CommandExecutor>
220-
// INVARIANT: soft-resets only when merge-base finds no common ancestor; idempotent when histories are already related
221-
// COMPLEXITY: O(1) git operations
222-
const adoptRemoteHistoryIfOrphan = (
223-
root: string,
224-
repoRef: string
225-
): Effect.Effect<void, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> =>
226-
Effect.gen(function*(_) {
227-
// Fetch remote history first — required for merge-base and reset
228-
const fetchExit = yield* _(gitExitCode(root, ["fetch", "origin", repoRef], gitBaseEnv))
229-
if (fetchExit !== successExitCode) {
230-
yield* _(Effect.logWarning(`git fetch origin ${repoRef} failed (exit ${fetchExit}); starting fresh history`))
231-
return
232-
}
233-
const remoteRef = `origin/${repoRef}`
234-
const hasRemoteExit = yield* _(
235-
gitExitCode(root, ["show-ref", "--verify", "--quiet", `refs/remotes/${remoteRef}`], gitBaseEnv)
236-
)
237-
if (hasRemoteExit !== successExitCode) {
238-
return // Remote branch does not exist yet (brand-new repo)
239-
}
240-
241-
// Case 1: orphan branch (no local commits at all)
242-
const revParseExit = yield* _(gitExitCode(root, ["rev-parse", "HEAD"], gitBaseEnv))
243-
if (revParseExit !== successExitCode) {
244-
yield* _(git(root, ["reset", "--soft", remoteRef], gitBaseEnv))
245-
yield* _(Effect.log(`Adopted remote history from ${remoteRef}`))
246-
return
247-
}
248-
249-
// Case 2: local commits exist but histories share no common ancestor
250-
// (e.g. git-init without cloning produced a divergent root commit)
251-
const mergeBaseExit = yield* _(gitExitCode(root, ["merge-base", "HEAD", remoteRef], gitBaseEnv))
252-
if (mergeBaseExit === successExitCode) {
253-
return // Histories are related — normal rebase in stateSync will handle it
254-
}
255-
256-
// Merge unrelated histories so both are preserved; abort on conflict — stateSync will open a PR
257-
yield* _(Effect.logWarning(`Local history has no common ancestor with ${remoteRef}; merging unrelated histories`))
258-
const mergeExit = yield* _(
259-
gitExitCode(root, ["merge", "--allow-unrelated-histories", "--no-edit", remoteRef], gitBaseEnv)
260-
)
261-
if (mergeExit === successExitCode) {
262-
yield* _(Effect.log(`Merged unrelated histories from ${remoteRef}`))
263-
return
264-
}
265-
// Conflict — abort and leave resolution to stateSync (which will push a branch and log a PR URL)
266-
yield* _(gitExitCode(root, ["merge", "--abort"], gitBaseEnv))
267-
yield* _(Effect.logWarning(`Merge conflict with ${remoteRef}; sync will open a PR for manual resolution`))
268-
})
269-
270215
export const stateInit = (
271216
input: StateInitInput
272217
): Effect.Effect<void, CommandFailedError | PlatformError, StateRepoEnv> =>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
2+
import type { PlatformError } from "@effect/platform/Error"
3+
import { Effect } from "effect"
4+
import type { CommandFailedError } from "../../shell/errors.js"
5+
import { git, gitBaseEnv, gitExitCode, successExitCode } from "./git-commands.js"
6+
7+
// CHANGE: align local history with remote when histories have no common ancestor
8+
// WHY: prevents creation of new branches when local repo was git-init'd without cloning (divergent root commits)
9+
// QUOTE(ТЗ): "у нас должна быть единая система облака в виде .docker-git. Новая ветка открывается только тогда когда не возможно исправить конфликт и сделать push в main"
10+
// REF: issue-141
11+
// PURITY: SHELL
12+
// EFFECT: Effect<void, CommandFailedError | PlatformError, CommandExecutor>
13+
// INVARIANT: soft-resets only when merge-base finds no common ancestor; idempotent when histories are already related
14+
// COMPLEXITY: O(1) git operations
15+
export const adoptRemoteHistoryIfOrphan = (
16+
root: string,
17+
repoRef: string
18+
): Effect.Effect<void, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> =>
19+
Effect.gen(function*(_) {
20+
// Fetch remote history first — required for merge-base and reset
21+
const fetchExit = yield* _(gitExitCode(root, ["fetch", "origin", repoRef], gitBaseEnv))
22+
if (fetchExit !== successExitCode) {
23+
yield* _(Effect.logWarning(`git fetch origin ${repoRef} failed (exit ${fetchExit}); starting fresh history`))
24+
return
25+
}
26+
const remoteRef = `origin/${repoRef}`
27+
const hasRemoteExit = yield* _(
28+
gitExitCode(root, ["show-ref", "--verify", "--quiet", `refs/remotes/${remoteRef}`], gitBaseEnv)
29+
)
30+
if (hasRemoteExit !== successExitCode) {
31+
return // Remote branch does not exist yet (brand-new repo)
32+
}
33+
34+
// Case 1: orphan branch (no local commits at all)
35+
const revParseExit = yield* _(gitExitCode(root, ["rev-parse", "HEAD"], gitBaseEnv))
36+
if (revParseExit !== successExitCode) {
37+
yield* _(git(root, ["reset", "--soft", remoteRef], gitBaseEnv))
38+
yield* _(Effect.log(`Adopted remote history from ${remoteRef}`))
39+
return
40+
}
41+
42+
// Case 2: local commits exist but histories share no common ancestor
43+
// (e.g. git-init without cloning produced a divergent root commit)
44+
const mergeBaseExit = yield* _(gitExitCode(root, ["merge-base", "HEAD", remoteRef], gitBaseEnv))
45+
if (mergeBaseExit === successExitCode) {
46+
return // Histories are related — normal rebase in stateSync will handle it
47+
}
48+
49+
// Merge unrelated histories so both are preserved; abort on conflict — stateSync will open a PR
50+
yield* _(Effect.logWarning(`Local history has no common ancestor with ${remoteRef}; merging unrelated histories`))
51+
const mergeExit = yield* _(
52+
gitExitCode(root, ["merge", "--allow-unrelated-histories", "--no-edit", remoteRef], gitBaseEnv)
53+
)
54+
if (mergeExit === successExitCode) {
55+
yield* _(Effect.log(`Merged unrelated histories from ${remoteRef}`))
56+
return
57+
}
58+
// Conflict — abort and leave resolution to stateSync (which will push a branch and log a PR URL)
59+
yield* _(gitExitCode(root, ["merge", "--abort"], gitBaseEnv))
60+
yield* _(Effect.logWarning(`Merge conflict with ${remoteRef}; sync will open a PR for manual resolution`))
61+
})

packages/lib/src/usecases/state-repo/git-commands.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ export const successExitCode = Number(ExitCode(0))
99

1010
export const gitBaseEnv: Readonly<Record<string, string>> = {
1111
// Avoid blocking on interactive credential prompts in CI / TUI contexts.
12-
GIT_TERMINAL_PROMPT: "0"
12+
GIT_TERMINAL_PROMPT: "0",
13+
// Ensure git commits never fail due to missing identity.
14+
GIT_AUTHOR_NAME: "docker-git",
15+
GIT_AUTHOR_EMAIL: "docker-git@users.noreply.github.com",
16+
GIT_COMMITTER_NAME: "docker-git",
17+
GIT_COMMITTER_EMAIL: "docker-git@users.noreply.github.com"
1318
}
1419

1520
export const git = (

0 commit comments

Comments
 (0)