Commit 5f3b0a7e1a

5f3b0a7e1a6bd7aa2f3ef9cedf4db065f6a8b6c6

parent: 936ce1198d

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-20 00:08 UTC

gitutil: commit-file writes the first commit of an empty repository

A branch that will not resolve is a root commit only when the repository
has no refs at all; elsewhere it stays the error it was.

Ref #236
internal/gitutil/commitfile_test.go added +38
@@ -0,0 +1,38 @@
1package gitutil
2
3import "testing"
4
5func TestCommitFileChangeEmptyRepo(t *testing.T) {
6 dir := t.TempDir()
7 if err := InitBare(dir, "main", ""); err != nil {
8 t.Fatal(err)
9 }
10 sha, err := CommitFileChange(dir, "main", "profile/README.md",
11 []byte("# hello\n"), "alice", "alice@example.org", "add about")
12 if err != nil {
13 t.Fatalf("first commit into an empty repository: %v", err)
14 }
15 if sha == "" {
16 t.Fatal("no sha returned")
17 }
18 raw, err := ReadBlob(dir, "main", "profile/README.md", 1<<20)
19 if err != nil {
20 t.Fatalf("reading it back: %v", err)
21 }
22 if string(raw) != "# hello\n" {
23 t.Errorf("read back %q", raw)
24 }
25
26 // The second commit still takes the parented path.
27 if _, err := CommitFileChange(dir, "main", "profile/README.md",
28 []byte("# hello again\n"), "alice", "alice@example.org", "edit"); err != nil {
29 t.Fatalf("second commit: %v", err)
30 }
31
32 // An unknown branch in a repository that has history is a typo, not a
33 // new orphan branch.
34 if _, err := CommitFileChange(dir, "nope", "x.md",
35 []byte("x"), "alice", "alice@example.org", "x"); err == nil {
36 t.Error("committing to an unknown branch of a non-empty repository succeeded")
37 }
38}
internal/gitutil/merge.go +24 −3
@@ -189,7 +189,13 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes
189189 branchRef := "refs/heads/" + branch
190190 parent, err := ResolveRef(dir, branchRef)
191191 if err != nil {
192 return "", fmt.Errorf("branch %s: %w", branch, err)
192 // An unborn branch is a root commit only in a repository with no
193 // refs at all. Anywhere else an unresolvable branch is a typo, and
194 // starting an orphan branch for it would be worse than refusing.
195 if !isEmptyRepo(dir) {
196 return "", fmt.Errorf("branch %s: %w", branch, err)
197 }
198 parent = ""
193199 }
194200
195201 // Hash the new blob.
@@ -211,7 +217,11 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes
211217 defer os.Remove(idx.Name())
212218 env := append(os.Environ(), "GIT_INDEX_FILE="+idx.Name())
213219
214 rt := exec.Command(toolpath.Look("git"), "-C", dir, "read-tree", parent+"^{tree}")
220 tree0 := parent + "^{tree}"
221 if parent == "" {
222 tree0 = "--empty"
223 }
224 rt := exec.Command(toolpath.Look("git"), "-C", dir, "read-tree", tree0)
215225 rt.Env = env
216226 if out, err := rt.CombinedOutput(); err != nil {
217227 return "", fmt.Errorf("read-tree: %v\n%s", err, out)
@@ -229,7 +239,11 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes
229239 }
230240 tree := strings.TrimSpace(string(out))
231241
232 sha, err := CommitTree(dir, tree, []string{parent}, name, email, message)
242 var parents []string
243 if parent != "" {
244 parents = []string{parent}
245 }
246 sha, err := CommitTree(dir, tree, parents, name, email, message)
233247 if err != nil {
234248 return "", err
235249 }
@@ -239,6 +253,13 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes
239253 return sha, nil
240254}
241255
256// isEmptyRepo reports whether dir has no refs at all — a repository
257// created but never pushed to.
258func isEmptyRepo(dir string) bool {
259 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "rev-list", "-n1", "--all").Output()
260 return err == nil && strings.TrimSpace(string(out)) == ""
261}
262
242263// CommitParents returns the parent SHAs of a commit.
243264func CommitParents(dir, sha string) ([]string, error) {
244265 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "rev-list", "--parents", "-n1", "--end-of-options", sha).Output()