Archived
Add read-only git provider
Implement submodule.Provider with the git CLI so submarine reads real repositories: `submarine [dir]`, with `--mock` for the built-in data. - Tree reads .gitmodules recursively; Status derives presence, checkout, worktree, staged pin, pushed state and pin -> ref resolution from local git commands only (no network). - Concurrent Status calls are bounded by runtime.NumCPU(). - scripts/make-fixture.sh builds a superproject covering every state; the integration tests run against it. - theme.Ago shows "never" for a zero time. - Docs updated; decisions D14-D16 added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c84f34ee2b
commit
64fe34b5b0
@@ -0,0 +1,125 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"submarine/internal/submodule"
|
||||
)
|
||||
|
||||
// gitmodule is one entry of a .gitmodules file.
|
||||
type gitmodule struct {
|
||||
name, path, url, branch string
|
||||
}
|
||||
|
||||
// parseGitmodules parses the output of
|
||||
// `git config -f .gitmodules --get-regexp '^submodule\..*\.(path|url|branch)$'`.
|
||||
// Entries keep the order of first appearance; entries without a path are
|
||||
// dropped.
|
||||
func parseGitmodules(out string) []gitmodule {
|
||||
var order []string
|
||||
byName := map[string]*gitmodule{}
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
key, value, _ := strings.Cut(line, " ")
|
||||
key, ok := strings.CutPrefix(key, "submodule.")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
dot := strings.LastIndexByte(key, '.')
|
||||
if dot < 0 {
|
||||
continue
|
||||
}
|
||||
name, attr := key[:dot], key[dot+1:]
|
||||
g := byName[name]
|
||||
if g == nil {
|
||||
g = &gitmodule{name: name}
|
||||
byName[name] = g
|
||||
order = append(order, name)
|
||||
}
|
||||
switch attr {
|
||||
case "path":
|
||||
g.path = value
|
||||
case "url":
|
||||
g.url = value
|
||||
case "branch":
|
||||
g.branch = value
|
||||
}
|
||||
}
|
||||
var mods []gitmodule
|
||||
for _, name := range order {
|
||||
if g := byName[name]; g.path != "" {
|
||||
mods = append(mods, *g)
|
||||
}
|
||||
}
|
||||
return mods
|
||||
}
|
||||
|
||||
// parseWorktree summarizes `git status --porcelain=v2` output. A conflict
|
||||
// outranks modifications, which outrank untracked files.
|
||||
func parseWorktree(out string) submodule.Worktree {
|
||||
w := submodule.WorktreeClean
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "u "):
|
||||
return submodule.WorktreeConflict
|
||||
case strings.HasPrefix(line, "1 "), strings.HasPrefix(line, "2 "):
|
||||
w = submodule.WorktreeModified
|
||||
case strings.HasPrefix(line, "? ") && w == submodule.WorktreeClean:
|
||||
w = submodule.WorktreeUntracked
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// parseLeftRight parses `git rev-list --left-right --count a...b`.
|
||||
func parseLeftRight(out string) (left, right int, err error) {
|
||||
f := strings.Fields(out)
|
||||
if len(f) != 2 {
|
||||
return 0, 0, fmt.Errorf("unexpected rev-list output %q", out)
|
||||
}
|
||||
if left, err = strconv.Atoi(f[0]); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if right, err = strconv.Atoi(f[1]); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return left, right, nil
|
||||
}
|
||||
|
||||
// commitFormat is the `git log --format` matching parseCommits: fields
|
||||
// separated by US, records terminated by RS.
|
||||
const commitFormat = "--format=%h%x1f%s%x1f%an%x1f%ct%x1e"
|
||||
|
||||
// parseCommits parses `git log` output produced with commitFormat.
|
||||
func parseCommits(out string) []submodule.Commit {
|
||||
var cs []submodule.Commit
|
||||
for _, rec := range strings.Split(out, "\x1e") {
|
||||
f := strings.Split(strings.TrimLeft(rec, "\n"), "\x1f")
|
||||
if len(f) != 4 {
|
||||
continue
|
||||
}
|
||||
c := submodule.Commit{SHA: f[0], Subject: f[1], Author: f[2]}
|
||||
if sec, err := strconv.ParseInt(f[3], 10, 64); err == nil {
|
||||
c.When = time.Unix(sec, 0)
|
||||
}
|
||||
cs = append(cs, c)
|
||||
}
|
||||
return cs
|
||||
}
|
||||
|
||||
// parseGitlink returns the SHA of a gitlink (mode 160000) entry in
|
||||
// `git ls-files -s` or `git ls-tree` output, or "" if there is none.
|
||||
func parseGitlink(out string) string {
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
f := strings.Fields(line)
|
||||
switch {
|
||||
case len(f) >= 3 && f[0] == "160000" && f[1] == "commit": // ls-tree
|
||||
return f[2]
|
||||
case len(f) >= 2 && f[0] == "160000": // ls-files -s
|
||||
return f[1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user