Gitfed
bastien-mrq/gitfed/ Commits/ fcad9b0

Four small UI improvements from the roadmap's "petits gains"

- Login link in the nav is now an accent CTA button, not plain text. - "Commits" button shows the total count (new CountCommits on admin.Ops). - Commit list rows are more compact. - File tree icons are type-specific (code/doc/image/config) instead of one generic glyph for everything.

bastien-mrq 2026-07-29 19:07 commit fcad9b0b6409598ae62b570edcbe62eae3cc6411 parent 38dbfcced3c5d263124753f5e48c3f581a8ff062
11 files changed +181 −20
M CHANGELOG.md +4 −0
M ROADMAP.md +0 −13
M cmd/gitfed-web/handlers_repo.go +9 −3
M cmd/gitfed-web/render.go +50 −4
A cmd/gitfed-web/render_test.go +30 −0
M internal/admin/admin.go +9 −0
M internal/adminrpc/client.go +6 −0
M internal/adminrpc/protocol.go +6 −0
M internal/adminrpc/server.go +8 −0
M internal/gitexec/gitexec.go +20 −0
M internal/gitexec/gitexec_test.go +39 −0
CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md index d54eb55..326689f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ A bullet starting with `**BREAKING:**` flags a change gitfed-ctl's update wizard makes you acknowledge individually before it will let you upgrade past that version. +## 1.2.11 + +- Four small UI improvements from ROADMAP.md's "Petits gains": the login link in the nav is now a proper accent CTA button instead of plain text; the "Commits" button on the repo page shows the total count (`git rev-list --count`, new `CountCommits` on `admin.Ops`); the commit list is more compact (tighter row padding); and files in the repo tree now get a type-specific icon (code/doc/image/config) instead of the same generic file glyph for everything. + ## 1.2.10 - Fixed a real bug found live: `CloneMirror` (the "import from URL" one-shot import) copied the source repo's `HEAD` verbatim, which can point at a branch that doesn't actually exist among what got mirrored (e.g. the source's default branch was renamed upstream — master to main — and its `HEAD` metadata never caught up). git refuses to advertise a `symref` for a dangling `HEAD`, breaking anonymous `git clone` for that repo entirely, with `--depth 1` (what `gitfed-install` uses) failing hardest — an empty checkout instead of a clear error. `CloneMirror` now repoints `HEAD` at the repo's one real branch after mirroring, same fix `InitBareRepo` already had for freshly created repos. This only fixes future imports — a repo already imported with a dangling `HEAD` needs a one-time manual fix: `kubectl -n gitfed exec deployment/gitfed -c server -- git --git-dir=/data/repos/<owner>/<repo>.git symbolic-ref HEAD refs/heads/<real-branch>`.
ROADMAP.md
diff --git a/ROADMAP.md b/ROADMAP.md index 24d6ac8..0ec277f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -68,19 +68,6 @@ qui ne sont pas des manques identifiés aujourd'hui. Ratio effort/valeur indicatif entre parenthèses, pour se souvenir pourquoi l'ordre est celui-là la prochaine fois qu'on rouvre ce document. -### Petits gains (effort minime, presque pas de décision de design) - -- **Bouton de connexion en style CTA** (accent, plus visible que les - autres liens de la barre de nav). *(effort très faible)*. -- **Affichage plus compact de la liste des commits**. *(effort très - faible — ajustements CSS)*. -- **Nombre total de commits affiché sur le bouton "Commits"** de la page - repo (`git rev-list --count`). *(effort très faible)*. -- **Icônes par type de fichier** dans l'arborescence de la page repo, à - la place de l'icône générique actuelle — le sprite SVG existe déjà, il - s'agit d'ajouter des symboles et un mapping extension → icône. - *(effort faible)*. - ### Interface du dépôt - **Dropdown de branches** sur la page repo pour parcourir l'arborescence
cmd/gitfed-web/handlers_repo.go
diff --git a/cmd/gitfed-web/handlers_repo.go b/cmd/gitfed-web/handlers_repo.go index 0bc6e0b..87e6a8c 100644 --- a/cmd/gitfed-web/handlers_repo.go +++ b/cmd/gitfed-web/handlers_repo.go @@ -108,7 +108,7 @@ var repoTpl = newTpl("repo", ` <div class="gf-actions-row"> {{if .Branch}}<span class="gf-btn">{{icon "branch"}} {{.Branch}}</span>{{end}} - {{if not .Empty}}<a href="/repo-commits/{{.Repo.Name}}" class="gf-btn">{{icon "history"}} {{t .Lang "repo.commits_title"}}</a>{{end}} + {{if not .Empty}}<a href="/repo-commits/{{.Repo.Name}}" class="gf-btn">{{icon "history"}} {{t .Lang "repo.commits_title"}}{{if .CommitCount}} ({{.CommitCount}}){{end}}</a>{{end}} {{if not .Empty}}<a href="/repo-mrs/{{.Repo.Name}}" class="gf-btn">{{icon "branch"}} {{t .Lang "mr.list_title"}}</a>{{end}} {{if .CanAdminister}}<a href="/repo-settings/{{.Repo.Name}}" class="gf-btn">{{t .Lang "nav.settings"}}</a>{{end}} </div> @@ -158,7 +158,7 @@ var repoTpl = newTpl("repo", ` {{if eq .Type "tree"}} <tr><td class="icon">{{icon "folder"}}</td><td class="name"><a href="/r/{{$.Repo.Name}}?path={{.FullPath}}">{{.Name}}</a></td><td class="meta">{{t $.Lang "repo.directory"}}</td></tr> {{else}} - <tr><td class="icon">{{icon "file"}}</td><td class="name"><a href="/repo-blob/{{$.Repo.Name}}?path={{.FullPath}}">{{.Name}}</a></td><td class="meta">{{t $.Lang "repo.file"}}</td></tr> + <tr><td class="icon">{{icon (fileIcon .Name)}}</td><td class="name"><a href="/repo-blob/{{$.Repo.Name}}?path={{.FullPath}}">{{.Name}}</a></td><td class="meta">{{t $.Lang "repo.file"}}</td></tr> {{end}} {{else}} {{if not .ShowUp}}<tr><td colspan="3" class="muted" style="padding:0.9rem;">{{t .Lang "repo.nothing_here"}}</td></tr>{{end}} @@ -226,6 +226,11 @@ func (s *server) handleRepoView(w http.ResponseWriter, r *http.Request) { return } + // Decorative (shown as a "(N)" suffix on the Commits button) — a + // failure here shouldn't take down the whole repo page, so it's + // deliberately not treated the same as the errors above. + commitCount, _, _ := s.ops.CountCommits(name) + var readmeHTML template.HTML var licenseFile string var tags []string @@ -266,6 +271,7 @@ func (s *server) handleRepoView(w http.ResponseWriter, r *http.Request) { CloneURL string HTTPSCloneURL string Branch string + CommitCount int Lang string Crumbs []crumb Entries []treeEntryView @@ -278,7 +284,7 @@ func (s *server) handleRepoView(w http.ResponseWriter, r *http.Request) { CanAdminister bool Flash template.HTML }{ - repo, s.domain, "ssh://git@" + s.domain + ":2222/" + name + ".git", "https://" + s.domain + "/" + name + ".git", branch, string(lang), + repo, s.domain, "ssh://git@" + s.domain + ":2222/" + name + ".git", "https://" + s.domain + "/" + name + ".git", branch, commitCount, string(lang), breadcrumbs(path), views, path != "", parentPath(path), path == "" && !found, tags, readmeHTML, licenseFile, canAdminister, flash(r), })
cmd/gitfed-web/render.go
diff --git a/cmd/gitfed-web/render.go b/cmd/gitfed-web/render.go index ccd522c..91e92dc 100644 --- a/cmd/gitfed-web/render.go +++ b/cmd/gitfed-web/render.go @@ -25,6 +25,7 @@ import ( var commonFuncs = template.FuncMap{ "t": func(lang, key string, args ...any) string { return i18n.T(i18n.Lang(lang), key, args...) }, "icon": icon, + "fileIcon": fileIcon, "roleLabel": func(lang, role string) string { return roleLabel(i18n.Lang(lang), role) }, "localUser": localUser, } @@ -139,6 +140,10 @@ const iconSprite = `<svg width="0" height="0" style="position:absolute" aria-hid <defs> <symbol id="ic-folder" viewBox="0 0 24 24"><path d="M3 6.2c0-.66.54-1.2 1.2-1.2h4l1.6 1.8h7c.66 0 1.2.54 1.2 1.2v8.6c0 .66-.54 1.2-1.2 1.2H4.2c-.66 0-1.2-.54-1.2-1.2V6.2Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></symbol> <symbol id="ic-file" viewBox="0 0 24 24"><path d="M6 3.5h7l4 4v12c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-15c0-.55.45-1 1-1Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M13 3.5v4h4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></symbol> +<symbol id="ic-file-code" viewBox="0 0 24 24"><path d="M6 3.5h7l4 4v12c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-15c0-.55.45-1 1-1Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M13 3.5v4h4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M9.5 12.3l-2.2 2.2 2.2 2.2M14.5 12.3l2.2 2.2-2.2 2.2" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></symbol> +<symbol id="ic-file-doc" viewBox="0 0 24 24"><path d="M6 3.5h7l4 4v12c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-15c0-.55.45-1 1-1Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M13 3.5v4h4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M8 12.3h8M8 15h8M8 17.7h5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></symbol> +<symbol id="ic-file-image" viewBox="0 0 24 24"><path d="M6 3.5h7l4 4v12c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-15c0-.55.45-1 1-1Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M13 3.5v4h4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><rect x="7.5" y="11.6" width="9" height="6.4" rx="1" fill="none" stroke="currentColor" stroke-width="1.2"/><circle cx="9.8" cy="13.9" r="0.9" fill="currentColor"/><path d="M8 17.2l2.6-2.6 2 2 2.9-2.9 1 1" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></symbol> +<symbol id="ic-file-config" viewBox="0 0 24 24"><path d="M6 3.5h7l4 4v12c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-15c0-.55.45-1 1-1Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M13 3.5v4h4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><circle cx="12" cy="15" r="2.1" fill="none" stroke="currentColor" stroke-width="1.2"/><path d="M12 11.8v1M12 17.2v1M9.2 15h1M14.8 15h1M10 13l.7.7M13.3 16.3l.7.7M14 13l-.7.7M10.7 16.3l-.7.7" stroke="currentColor" stroke-width="1" stroke-linecap="round"/></symbol> <symbol id="ic-branch" viewBox="0 0 24 24"><rect x="10.5" y="12" width="3" height="8.5" fill="currentColor"/><rect x="3.3" y="3" width="3" height="8" fill="currentColor" transform="rotate(35 4.8 7)"/><rect x="16.7" y="3" width="3" height="8" fill="currentColor" transform="rotate(-35 18.2 7)"/></symbol> <symbol id="ic-copy" viewBox="0 0 24 24"><rect x="8.5" y="8.5" width="10.5" height="12.5" rx="1.6" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M5.5 15V4.6c0-.6.48-1.1 1.1-1.1H16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol> <symbol id="ic-check" viewBox="0 0 24 24"><path d="M4.5 12.5l4.6 4.6L19.5 6.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></symbol> @@ -165,6 +170,45 @@ func icon(name string) template.HTML { return template.HTML(`<svg class="icon" aria-hidden="true"><use href="#ic-` + name + `"/></svg>`) } +var ( + codeExtensions = map[string]bool{ + ".go": true, ".js": true, ".mjs": true, ".ts": true, ".jsx": true, ".tsx": true, + ".py": true, ".rb": true, ".java": true, ".c": true, ".h": true, ".cpp": true, ".cc": true, ".hpp": true, + ".rs": true, ".php": true, ".sh": true, ".bash": true, ".css": true, ".scss": true, + ".html": true, ".htm": true, ".sql": true, ".lua": true, ".swift": true, ".kt": true, ".pl": true, + } + docExtensions = map[string]bool{".md": true, ".markdown": true, ".txt": true, ".rst": true, ".adoc": true} + imageExtensions = map[string]bool{".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".svg": true, ".webp": true, ".bmp": true, ".ico": true} + configExtensions = map[string]bool{".json": true, ".yaml": true, ".yml": true, ".toml": true, ".xml": true, ".ini": true, ".conf": true, ".env": true, ".lock": true} + + // docFilenames catches conventional extensionless files that are + // clearly documentation, not code, even though they have no matching + // extension above. + docFilenames = map[string]bool{"README": true, "LICENSE": true, "CHANGELOG": true, "AUTHORS": true, "NOTICE": true} +) + +// fileIcon maps a file's name to one of iconSprite's "file-*" symbols +// (falling back to the plain "file" glyph for anything unrecognized) — +// extension-based, same lightweight heuristic spirit as the license +// detection in ROADMAP.md §3, not a real MIME/language sniffer. +func fileIcon(name string) string { + if docFilenames[strings.ToUpper(name)] { + return "file-doc" + } + switch ext := strings.ToLower(path.Ext(name)); { + case codeExtensions[ext]: + return "file-code" + case docExtensions[ext]: + return "file-doc" + case imageExtensions[ext]: + return "file-image" + case configExtensions[ext]: + return "file-config" + default: + return "file" + } +} + // roleLabel translates an ACL role (or the synthetic "owner") for display. func roleLabel(lang i18n.Lang, role string) string { switch role { @@ -219,6 +263,8 @@ const shellHeadSrc = `<!doctype html> .gf-nav-right { display: flex; align-items: center; gap: 0.6rem; flex-shrink: 0; } .gf-nav-right a { color: var(--text-dim); text-decoration: none; font-size: 0.86rem; } .gf-nav-right a:hover { color: var(--text); } + .gf-nav-right .gf-btn { margin-top: 0; } + .gf-nav-right .gf-btn.primary:hover { color: var(--accent-ink); } .linklike { background: none; border: none; color: var(--text-dim); font-size: 0.86rem; cursor: pointer; padding: 0; font-family: inherit; } .linklike:hover { color: var(--text); } .icon { width: 15px; height: 15px; flex-shrink: 0; vertical-align: -0.15em; } @@ -396,12 +442,12 @@ const shellHeadSrc = `<!doctype html> .gf-repo-main .topics a:hover { color: var(--text-dim); } .gf-commit-list { display: flex; flex-direction: column; } - .gf-commit-row { display: flex; align-items: center; gap: 0.9rem; padding: 0.8rem 1.1rem; border-bottom: 1px solid var(--border); text-decoration: none; color: inherit; } + .gf-commit-row { display: flex; align-items: center; gap: 0.7rem; padding: 0.45rem 1.1rem; border-bottom: 1px solid var(--border); text-decoration: none; color: inherit; } .gf-commit-row:last-child { border-bottom: none; } .gf-commit-row:hover { background: var(--surface-2); } .gf-commit-main { flex: 1; min-width: 0; } - .gf-commit-main .subject { font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .gf-commit-main .meta { font-size: 0.78rem; color: var(--text-faint); margin-top: 0.2rem; } + .gf-commit-main .subject { font-size: 0.88rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .gf-commit-main .meta { font-size: 0.75rem; color: var(--text-faint); margin-top: 0.1rem; } .gf-commit-hash { flex-shrink: 0; font-size: 0.78rem; color: var(--text-dim); background: var(--surface-2); padding: 0.15rem 0.5rem; border-radius: 5px; } .gf-commit-detail-head { padding: 1.1rem 1.2rem; border-bottom: 1px solid var(--border); } @@ -691,7 +737,7 @@ const shellHeadSrc = `<!doctype html> </div> </div> {{else}} - <a href="/login">{{t .Lang "nav.login"}}</a> + <a href="/login" class="gf-btn primary">{{t .Lang "nav.login"}}</a> {{end}} </div> </div>
cmd/gitfed-web/render_test.go
diff --git a/cmd/gitfed-web/render_test.go b/cmd/gitfed-web/render_test.go new file mode 100644 index 0000000..446e7c6 --- /dev/null +++ b/cmd/gitfed-web/render_test.go @@ -0,0 +1,30 @@ +package main + +import "testing" + +func TestFileIcon(t *testing.T) { + cases := []struct { + name string + want string + }{ + {"main.go", "file-code"}, + {"index.tsx", "file-code"}, + {"script.sh", "file-code"}, + {"README.md", "file-doc"}, + {"README", "file-doc"}, + {"LICENSE", "file-doc"}, + {"notes.txt", "file-doc"}, + {"logo.png", "file-image"}, + {"photo.JPEG", "file-image"}, + {"config.yaml", "file-config"}, + {"package.json", "file-config"}, + {".gitignore", "file"}, + {"Makefile", "file"}, + {"data.bin", "file"}, + } + for _, c := range cases { + if got := fileIcon(c.name); got != c.want { + t.Errorf("fileIcon(%q) = %q, want %q", c.name, got, c.want) + } + } +}
internal/admin/admin.go
diff --git a/internal/admin/admin.go b/internal/admin/admin.go index 34aea70..9e2a93a 100644 --- a/internal/admin/admin.go +++ b/internal/admin/admin.go @@ -59,6 +59,7 @@ type Ops interface { GetRepoFile(name, path string) (content string, found bool, err error) GetRepoBranch(name string) (branch string, found bool, err error) ListCommits(name string, limit int) (commits []gitexec.Commit, found bool, err error) + CountCommits(name string) (count int, found bool, err error) ShowCommit(name, hash string) (detail gitexec.CommitDetail, found bool, err error) CommitDiff(name, hash string) (diff string, truncated bool, err error) ListBranches(name string) (branches []string, err error) @@ -441,6 +442,14 @@ func (a *Admin) ListCommits(name string, limit int) ([]gitexec.Commit, bool, err return gitexec.ListCommits(repo.Path, limit) } +func (a *Admin) CountCommits(name string) (int, bool, error) { + repo, err := a.Store.GetRepo(name) + if err != nil { + return 0, false, err + } + return gitexec.CountCommits(repo.Path) +} + func (a *Admin) ShowCommit(name, hash string) (gitexec.CommitDetail, bool, error) { repo, err := a.Store.GetRepo(name) if err != nil {
internal/adminrpc/client.go
diff --git a/internal/adminrpc/client.go b/internal/adminrpc/client.go index b254f93..2d80a51 100644 --- a/internal/adminrpc/client.go +++ b/internal/adminrpc/client.go @@ -214,6 +214,12 @@ func (c *Client) GetRepoBranch(name string) (string, bool, error) { return out.Branch, out.Found, err } +func (c *Client) CountCommits(name string) (int, bool, error) { + var out countFoundResult + err := c.call(methodCountCommits, nameArgs{Name: name}, &out) + return out.Count, out.Found, err +} + func (c *Client) GetUser(username string) (store.User, error) { var out userResult err := c.call(methodGetUser, nameArgs{Name: username}, &out)
internal/adminrpc/protocol.go
diff --git a/internal/adminrpc/protocol.go b/internal/adminrpc/protocol.go index 9b40b7b..e9e6bce 100644 --- a/internal/adminrpc/protocol.go +++ b/internal/adminrpc/protocol.go @@ -49,6 +49,7 @@ const ( methodDeleteSession = "DeleteSession" methodCheckAccess = "CheckAccess" methodListCommits = "ListCommits" + methodCountCommits = "CountCommits" methodShowCommit = "ShowCommit" methodCommitDiff = "CommitDiff" methodListBranches = "ListBranches" @@ -180,6 +181,11 @@ type branchResult struct { Found bool `json:"found"` } +type countFoundResult struct { + Count int `json:"count"` + Found bool `json:"found"` +} + type userResult struct { User store.User `json:"user"` }
internal/adminrpc/server.go
diff --git a/internal/adminrpc/server.go b/internal/adminrpc/server.go index e8c280a..b1ed21a 100644 --- a/internal/adminrpc/server.go +++ b/internal/adminrpc/server.go @@ -236,6 +236,14 @@ func (s *Server) dispatch(req wireRequest) (any, error) { branch, found, err := s.ops.GetRepoBranch(a.Name) return branchResult{Branch: branch, Found: found}, err + case methodCountCommits: + var a nameArgs + if err := json.Unmarshal(req.Args, &a); err != nil { + return nil, err + } + count, found, err := s.ops.CountCommits(a.Name) + return countFoundResult{Count: count, Found: found}, err + case methodGetUser: var a nameArgs if err := json.Unmarshal(req.Args, &a); err != nil {
internal/gitexec/gitexec.go
diff --git a/internal/gitexec/gitexec.go b/internal/gitexec/gitexec.go index 6f7f183..89a3814 100644 --- a/internal/gitexec/gitexec.go +++ b/internal/gitexec/gitexec.go @@ -348,6 +348,26 @@ type Commit struct { // subject line itself contains (unlike a printable character such as "|"). const commitFieldSep, commitRecordSep = "\x1f", "\x1e" +// CountCommits returns how many commits are reachable from the repo's +// default branch (see resolveDefaultRef). found is false (nil error) for a +// repo with no commits yet, matching ListCommits's contract. +func CountCommits(repoPath string) (count int, found bool, err error) { + ref, ok := resolveDefaultRef(repoPath) + if !ok { + return 0, false, nil + } + + out, err := exec.Command("git", "--git-dir="+repoPath, "rev-list", "--count", ref).Output() + if err != nil { + return 0, false, fmt.Errorf("gitexec: rev-list --count %s: %w", ref, err) + } + n, err := strconv.Atoi(strings.TrimSpace(string(out))) + if err != nil { + return 0, false, fmt.Errorf("gitexec: parse rev-list --count output %q: %w", out, err) + } + return n, true, nil +} + // ListCommits returns up to limit commits reachable from the repo's default // branch (see resolveDefaultRef), most recent first. found is false (nil // error) for a repo with no commits yet.
internal/gitexec/gitexec_test.go
diff --git a/internal/gitexec/gitexec_test.go b/internal/gitexec/gitexec_test.go index c672680..1756367 100644 --- a/internal/gitexec/gitexec_test.go +++ b/internal/gitexec/gitexec_test.go @@ -240,6 +240,45 @@ func TestCloneMirrorFixesDanglingHead(t *testing.T) { } } +func TestCountCommits(t *testing.T) { + tmp := t.TempDir() + barePath := filepath.Join(tmp, "repo.git") + if err := InitBareRepo(barePath); err != nil { + t.Fatalf("init bare repo: %v", err) + } + + count, found, err := CountCommits(barePath) + if err != nil { + t.Fatalf("CountCommits on empty repo: %v", err) + } + if found { + t.Fatalf("empty repo should report found=false, got count=%d", count) + } + + work := filepath.Join(tmp, "work") + if err := os.Mkdir(work, 0755); err != nil { + t.Fatal(err) + } + run(t, work, "git", "init", "-q", "-b", "main") + for i := 0; i < 3; i++ { + if err := os.WriteFile(filepath.Join(work, "f"), []byte{byte('a' + i)}, 0644); err != nil { + t.Fatal(err) + } + run(t, work, "git", "add", "f") + run(t, work, "git", "commit", "-q", "-m", "commit") + } + run(t, work, "git", "remote", "add", "origin", barePath) + run(t, work, "git", "push", "-q", "origin", "main") + + count, found, err = CountCommits(barePath) + if err != nil { + t.Fatalf("CountCommits: %v", err) + } + if !found || count != 3 { + t.Fatalf("CountCommits: found=%v count=%d, want found=true count=3", found, count) + } +} + func TestDefaultBranchNameViaHEAD(t *testing.T) { tmp := t.TempDir() barePath := filepath.Join(tmp, "repo.git")