3 files changed,
152 insertions(+),
64 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-03-09 16:24:32 +0200
Authored at:
2026-03-08 13:08:14 +0200
Change ID:
pllsurrkqkpuqttuzrumoxkmkwywwlww
Parent:
4ca522b
jump to
| M | internal/git/external.go |
| M | internal/git/repo.go |
| M | internal/git/tree.go |
M
internal/git/external.go
路路路 33 33 cmd.Stderr = opts.Stderr 34 34 return cmd.Run() 35 35 } 36 + 37 +func (g *Repo) streamingGitLog(ctx context.Context, extraArgs ...string) (io.ReadCloser, error) { 38 + args := []string{"log", g.h.String()} 39 + args = append(args, extraArgs...) 40 + 41 + cmd := exec.CommandContext(ctx, "git", args...) 42 + cmd.Dir = g.path 43 + 44 + stdout, err := cmd.StdoutPipe() 45 + if err != nil { 46 + return nil, err 47 + } 48 + 49 + if err := cmd.Start(); err != nil { 50 + return nil, err 51 + } 52 + 53 + return &processReader{ 54 + Reader: stdout, 55 + cmd: cmd, 56 + stdout: stdout, 57 + }, nil 58 +} 59 + 60 +// processReader wraps a reader and ensures the associated process is cleaned up 61 +type processReader struct { 62 + io.Reader 63 + cmd *exec.Cmd 64 + stdout io.ReadCloser 65 +} 66 + 67 +func (pr *processReader) Close() error { 68 + if err := pr.stdout.Close(); err != nil { 69 + return err 70 + } 71 + return pr.cmd.Wait() 72 +}
M
internal/git/repo.go
路路路 192 192 return newCommit(c), nil 193 193 } 194 194 195 -// lastCommitForFilesInTree ... 196 -// TODO: at the moment it doesn't work well with merges, ideally i would "shell" out it, 197 -// `git log --pretty:format:%H,%ad,%s --date=iso --name-only -- g.path` 198 -func (g *Repo) lastCommitForFilesInTree(tree *object.Tree, dirPath string) (map[string]*Commit, error) { 199 - log, err := g.r.Log(&git.LogOptions{From: g.h}) 200 - if err != nil { 201 - return nil, err 202 - } 203 - 204 - result := make(map[string]*Commit) 205 - err = log.ForEach(func(c *object.Commit) error { 206 - if c.NumParents() == 0 { 207 - for _, entry := range tree.Entries { 208 - if _, seen := result[entry.Name]; !seen { 209 - result[entry.Name] = newCommit(c) 210 - } 211 - } 212 - return storer.ErrStop 213 - } 214 - 215 - // skip merge commits 216 - if c.NumParents() > 1 { 217 - return nil 218 - } 219 - 220 - parent, perr := c.Parent(0) 221 - if perr != nil { 222 - return perr 223 - } 224 - 225 - patch, perr := parent.Patch(c) 226 - if perr != nil { 227 - return perr 228 - } 229 - 230 - for _, fp := range patch.FilePatches() { 231 - from, to := fp.Files() 232 - 233 - var affectedPath string 234 - if to != nil { 235 - affectedPath = to.Path() 236 - } else if from != nil { 237 - affectedPath = from.Path() 238 - } 239 - 240 - name := topLevelEntry(affectedPath, dirPath) 241 - if name == "" { 242 - continue 243 - } 244 - 245 - if _, seen := result[name]; !seen { 246 - result[name] = newCommit(c) 247 - } 248 - } 249 - return nil 250 - }) 251 - if err != nil && !errors.Is(err, storer.ErrStop) { 252 - return nil, err 253 - } 254 - return result, nil 255 -} 256 - 257 195 type Branch struct { 258 196 Name string 259 197 LastUpdate time.Time
M
internal/git/tree.go
路路路 1 1 package git 2 2 3 3 import ( 4 + "bufio" 5 + "context" 4 6 "errors" 5 7 "fmt" 6 8 "io" 7 9 "mime" 10 + "path" 8 11 "path/filepath" 9 12 "strings" 13 + "time" 10 14 15 + "github.com/go-git/go-git/v5/plumbing" 11 16 "github.com/go-git/go-git/v5/plumbing/object" 12 17 ) 13 18 路路路 22 27 func (g *Repo) makeNiceTree(t *object.Tree, parent string) []NiceTree { 23 28 var nts []NiceTree 24 29 25 - cms, err := g.lastCommitForFilesInTree(t, parent) 30 + ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second) 31 + defer cancel() 32 + 33 + cms, err := g.lastCommitForFilesInTree(ctx, t, parent) 26 34 if err != nil { 27 35 return nts 28 36 } 29 37 30 38 for _, e := range t.Entries { 39 + fpath := path.Join(parent, e.Name) 31 40 mode, _ := e.Mode.ToOSFileMode() 32 41 sz, _ := t.Size(e.Name) 33 42 nts = append(nts, NiceTree{ 43 + Commit: cms[fpath], 34 44 Name: e.Name, 35 45 Mode: mode.String(), 36 46 IsFile: e.Mode.IsFile(), 37 - Commit: cms[e.Name], 38 47 Size: sz, 39 48 }) 40 49 } 路路路 135 144 Size: file.Size, 136 145 }, nil 137 146 } 147 + 148 +type logCommit struct { 149 + Commit 150 + hash plumbing.Hash 151 + files []string 152 +} 153 + 154 +func (g *Repo) lastCommitForFilesInTree(ctx context.Context, subtree *object.Tree, parent string) (map[string]*Commit, error) { 155 + filesToDo := make(map[string]struct{}) 156 + filesDone := make(map[string]*Commit) 157 + for _, e := range subtree.Entries { 158 + fpath := path.Clean(path.Join(parent, e.Name)) 159 + filesToDo[fpath] = struct{}{} 160 + } 161 + 162 + if len(filesToDo) == 0 { 163 + return filesDone, nil 164 + } 165 + 166 + ctx, cancel := context.WithCancel(ctx) 167 + defer cancel() 168 + 169 + pathSpec := "." 170 + if parent != "" { 171 + pathSpec = parent 172 + } 173 + 174 + output, err := g.streamingGitLog(ctx, "--pretty=format:%H,%ad,%ae,%an,%ce,%cn,%s", "--date=iso", "--name-only", "--", pathSpec) 175 + if err != nil { 176 + return nil, err 177 + } 178 + defer output.Close() // Ensure the git process is properly cleaned up 179 + 180 + var current logCommit 181 + reader := bufio.NewReader(output) 182 + for { 183 + line, err := reader.ReadString('\n') 184 + if err != nil && err != io.EOF { 185 + return nil, err 186 + } 187 + 188 + line = strings.TrimSpace(line) 189 + fmt.Println("LINE", line) 190 + 191 + if line == "" { 192 + if !current.hash.IsZero() { 193 + c := current.Commit 194 + // we have a fully parsed commit 195 + for _, f := range current.files { 196 + if _, ok := filesToDo[f]; ok { 197 + filesDone[f] = &c 198 + delete(filesToDo, f) 199 + } 200 + } 201 + 202 + if len(filesToDo) == 0 { 203 + cancel() 204 + break 205 + } 206 + 207 + current = logCommit{} 208 + } 209 + } else if current.hash.IsZero() { 210 + parts := strings.SplitN(line, ",", 7) 211 + if len(parts) == 7 { 212 + current.hash = plumbing.NewHash(parts[0]) 213 + 214 + // NOTE: this is copy-paste of [newCommit] 215 + current.Hash = parts[0] 216 + current.HashShort = parts[0][:7] 217 + current.Committed, _ = time.Parse("2006-01-02 15:04:05 -0700", parts[1]) 218 + current.AuthorEmail = parts[2] 219 + current.AuthorName = parts[3] 220 + current.CommitterEmail = parts[4] 221 + current.CommitterName = parts[5] 222 + current.Message = parts[6] 223 + } 224 + } else { 225 + // all ancestors along this path should also be included 226 + file := path.Clean(line) 227 + ancestors := ancestors(file) 228 + current.files = append(current.files, file) 229 + current.files = append(current.files, ancestors...) 230 + } 231 + 232 + if err == io.EOF { 233 + break 234 + } 235 + } 236 + 237 + return filesDone, nil 238 +} 239 + 240 +func ancestors(p string) []string { 241 + var ancestors []string 242 + for { 243 + p = path.Dir(p) 244 + if p == "." || p == "/" { 245 + break 246 + } 247 + ancestors = append(ancestors, p) 248 + } 249 + return ancestors 250 +}