forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 4.1 kB view raw
1package git 2 3import ( 4 "bufio" 5 "context" 6 "crypto/sha256" 7 "fmt" 8 "io" 9 "os/exec" 10 "path" 11 "strings" 12 "time" 13 14 "github.com/dgraph-io/ristretto" 15 "github.com/go-git/go-git/v5/plumbing" 16 "github.com/go-git/go-git/v5/plumbing/object" 17) 18 19var ( 20 commitCache *ristretto.Cache 21) 22 23func init() { 24 cache, _ := ristretto.NewCache(&ristretto.Config{ 25 NumCounters: 1e7, 26 MaxCost: 1 << 30, 27 BufferItems: 64, 28 TtlTickerDurationInSec: 120, 29 }) 30 commitCache = cache 31} 32 33// processReader wraps a reader and ensures the associated process is cleaned up 34type processReader struct { 35 io.Reader 36 cmd *exec.Cmd 37 stdout io.ReadCloser 38} 39 40func (pr *processReader) Close() error { 41 if err := pr.stdout.Close(); err != nil { 42 return err 43 } 44 return pr.cmd.Wait() 45} 46 47func (g *GitRepo) streamingGitLog(ctx context.Context, extraArgs ...string) (io.ReadCloser, error) { 48 args := []string{} 49 args = append(args, "log") 50 args = append(args, g.h.String()) 51 args = append(args, extraArgs...) 52 53 cmd := exec.CommandContext(ctx, "git", args...) 54 cmd.Dir = g.path 55 56 stdout, err := cmd.StdoutPipe() 57 if err != nil { 58 return nil, err 59 } 60 61 if err := cmd.Start(); err != nil { 62 return nil, err 63 } 64 65 return &processReader{ 66 Reader: stdout, 67 cmd: cmd, 68 stdout: stdout, 69 }, nil 70} 71 72type commit struct { 73 hash plumbing.Hash 74 when time.Time 75 files []string 76 message string 77} 78 79func cacheKey(g *GitRepo, path string) string { 80 sep := byte(':') 81 hash := sha256.Sum256(fmt.Append([]byte{}, g.path, sep, g.h.String(), sep, path)) 82 return fmt.Sprintf("%x", hash) 83} 84 85func (g *GitRepo) calculateCommitTimeIn(ctx context.Context, subtree *object.Tree, parent string, timeout time.Duration) (map[string]commit, error) { 86 ctx, cancel := context.WithTimeout(ctx, timeout) 87 defer cancel() 88 return g.calculateCommitTime(ctx, subtree, parent) 89} 90 91func (g *GitRepo) calculateCommitTime(ctx context.Context, subtree *object.Tree, parent string) (map[string]commit, error) { 92 filesToDo := make(map[string]struct{}) 93 filesDone := make(map[string]commit) 94 for _, e := range subtree.Entries { 95 fpath := path.Clean(path.Join(parent, e.Name)) 96 filesToDo[fpath] = struct{}{} 97 } 98 99 for _, e := range subtree.Entries { 100 f := path.Clean(path.Join(parent, e.Name)) 101 cacheKey := cacheKey(g, f) 102 if cached, ok := commitCache.Get(cacheKey); ok { 103 filesDone[f] = cached.(commit) 104 delete(filesToDo, f) 105 } else { 106 filesToDo[f] = struct{}{} 107 } 108 } 109 110 if len(filesToDo) == 0 { 111 return filesDone, nil 112 } 113 114 ctx, cancel := context.WithCancel(ctx) 115 defer cancel() 116 117 pathSpec := "." 118 if parent != "" { 119 pathSpec = parent 120 } 121 output, err := g.streamingGitLog(ctx, "--pretty=format:%H,%ad,%s", "--date=iso", "--name-only", "--", pathSpec) 122 if err != nil { 123 return nil, err 124 } 125 defer output.Close() // Ensure the git process is properly cleaned up 126 127 reader := bufio.NewReader(output) 128 var current commit 129 for { 130 line, err := reader.ReadString('\n') 131 if err != nil && err != io.EOF { 132 return nil, err 133 } 134 line = strings.TrimSpace(line) 135 136 if line == "" { 137 if !current.hash.IsZero() { 138 // we have a fully parsed commit 139 for _, f := range current.files { 140 if _, ok := filesToDo[f]; ok { 141 filesDone[f] = current 142 delete(filesToDo, f) 143 commitCache.Set(cacheKey(g, f), current, 0) 144 } 145 } 146 147 if len(filesToDo) == 0 { 148 cancel() 149 break 150 } 151 current = commit{} 152 } 153 } else if current.hash.IsZero() { 154 parts := strings.SplitN(line, ",", 3) 155 if len(parts) == 3 { 156 current.hash = plumbing.NewHash(parts[0]) 157 current.when, _ = time.Parse("2006-01-02 15:04:05 -0700", parts[1]) 158 current.message = parts[2] 159 } 160 } else { 161 // all ancestors along this path should also be included 162 file := path.Clean(line) 163 ancestors := ancestors(file) 164 current.files = append(current.files, file) 165 current.files = append(current.files, ancestors...) 166 } 167 168 if err == io.EOF { 169 break 170 } 171 } 172 173 return filesDone, nil 174} 175 176func ancestors(p string) []string { 177 var ancestors []string 178 179 for { 180 p = path.Dir(p) 181 if p == "." || p == "/" { 182 break 183 } 184 ancestors = append(ancestors, p) 185 } 186 return ancestors 187}