forked from tangled.org/core
this repo has no description
at stars 8.0 kB view raw
1package git 2 3import ( 4 "archive/tar" 5 "bytes" 6 "fmt" 7 "io" 8 "io/fs" 9 "os/exec" 10 "path" 11 "sort" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/dgraph-io/ristretto" 17 "github.com/go-git/go-git/v5" 18 "github.com/go-git/go-git/v5/plumbing" 19 "github.com/go-git/go-git/v5/plumbing/object" 20) 21 22var ( 23 commitCache *ristretto.Cache 24 cacheMu sync.RWMutex 25) 26 27func init() { 28 cache, _ := ristretto.NewCache(&ristretto.Config{ 29 NumCounters: 1e7, 30 MaxCost: 1 << 30, 31 BufferItems: 64, 32 TtlTickerDurationInSec: 120, 33 }) 34 commitCache = cache 35} 36 37var ( 38 ErrBinaryFile = fmt.Errorf("binary file") 39) 40 41type GitRepo struct { 42 path string 43 r *git.Repository 44 h plumbing.Hash 45} 46 47type TagList struct { 48 refs []*TagReference 49 r *git.Repository 50} 51 52// TagReference is used to list both tag and non-annotated tags. 53// Non-annotated tags should only contains a reference. 54// Annotated tags should contain its reference and its tag information. 55type TagReference struct { 56 ref *plumbing.Reference 57 tag *object.Tag 58} 59 60// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo 61// to tar WriteHeader 62type infoWrapper struct { 63 name string 64 size int64 65 mode fs.FileMode 66 modTime time.Time 67 isDir bool 68} 69 70func (self *TagList) Len() int { 71 return len(self.refs) 72} 73 74func (self *TagList) Swap(i, j int) { 75 self.refs[i], self.refs[j] = self.refs[j], self.refs[i] 76} 77 78// sorting tags in reverse chronological order 79func (self *TagList) Less(i, j int) bool { 80 var dateI time.Time 81 var dateJ time.Time 82 83 if self.refs[i].tag != nil { 84 dateI = self.refs[i].tag.Tagger.When 85 } else { 86 c, err := self.r.CommitObject(self.refs[i].ref.Hash()) 87 if err != nil { 88 dateI = time.Now() 89 } else { 90 dateI = c.Committer.When 91 } 92 } 93 94 if self.refs[j].tag != nil { 95 dateJ = self.refs[j].tag.Tagger.When 96 } else { 97 c, err := self.r.CommitObject(self.refs[j].ref.Hash()) 98 if err != nil { 99 dateJ = time.Now() 100 } else { 101 dateJ = c.Committer.When 102 } 103 } 104 105 return dateI.After(dateJ) 106} 107 108func Open(path string, ref string) (*GitRepo, error) { 109 var err error 110 g := GitRepo{path: path} 111 g.r, err = git.PlainOpen(path) 112 if err != nil { 113 return nil, fmt.Errorf("opening %s: %w", path, err) 114 } 115 116 if ref == "" { 117 head, err := g.r.Head() 118 if err != nil { 119 return nil, fmt.Errorf("getting head of %s: %w", path, err) 120 } 121 g.h = head.Hash() 122 } else { 123 hash, err := g.r.ResolveRevision(plumbing.Revision(ref)) 124 if err != nil { 125 return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err) 126 } 127 g.h = *hash 128 } 129 return &g, nil 130} 131 132func (g *GitRepo) Commits() ([]*object.Commit, error) { 133 ci, err := g.r.Log(&git.LogOptions{From: g.h}) 134 if err != nil { 135 return nil, fmt.Errorf("commits from ref: %w", err) 136 } 137 138 commits := []*object.Commit{} 139 ci.ForEach(func(c *object.Commit) error { 140 commits = append(commits, c) 141 return nil 142 }) 143 144 return commits, nil 145} 146 147func (g *GitRepo) LastCommit() (*object.Commit, error) { 148 c, err := g.r.CommitObject(g.h) 149 if err != nil { 150 return nil, fmt.Errorf("last commit: %w", err) 151 } 152 return c, nil 153} 154 155func (g *GitRepo) FileContent(path string) (string, error) { 156 c, err := g.r.CommitObject(g.h) 157 if err != nil { 158 return "", fmt.Errorf("commit object: %w", err) 159 } 160 161 tree, err := c.Tree() 162 if err != nil { 163 return "", fmt.Errorf("file tree: %w", err) 164 } 165 166 file, err := tree.File(path) 167 if err != nil { 168 return "", err 169 } 170 171 isbin, _ := file.IsBinary() 172 173 if !isbin { 174 return file.Contents() 175 } else { 176 return "", ErrBinaryFile 177 } 178} 179 180func (g *GitRepo) Tags() ([]*TagReference, error) { 181 iter, err := g.r.Tags() 182 if err != nil { 183 return nil, fmt.Errorf("tag objects: %w", err) 184 } 185 186 tags := make([]*TagReference, 0) 187 188 if err := iter.ForEach(func(ref *plumbing.Reference) error { 189 obj, err := g.r.TagObject(ref.Hash()) 190 switch err { 191 case nil: 192 tags = append(tags, &TagReference{ 193 ref: ref, 194 tag: obj, 195 }) 196 case plumbing.ErrObjectNotFound: 197 tags = append(tags, &TagReference{ 198 ref: ref, 199 }) 200 default: 201 return err 202 } 203 return nil 204 }); err != nil { 205 return nil, err 206 } 207 208 tagList := &TagList{r: g.r, refs: tags} 209 sort.Sort(tagList) 210 return tags, nil 211} 212 213func (g *GitRepo) Branches() ([]*plumbing.Reference, error) { 214 bi, err := g.r.Branches() 215 if err != nil { 216 return nil, fmt.Errorf("branchs: %w", err) 217 } 218 219 branches := []*plumbing.Reference{} 220 221 _ = bi.ForEach(func(ref *plumbing.Reference) error { 222 branches = append(branches, ref) 223 return nil 224 }) 225 226 return branches, nil 227} 228 229func (g *GitRepo) FindMainBranch() (string, error) { 230 ref, err := g.r.Head() 231 if err != nil { 232 return "", fmt.Errorf("unable to find main branch: %w", err) 233 } 234 if ref.Name().IsBranch() { 235 return strings.TrimPrefix(string(ref.Name()), "refs/heads/"), nil 236 } 237 238 return "", fmt.Errorf("unable to find main branch: %w", err) 239} 240 241// WriteTar writes itself from a tree into a binary tar file format. 242// prefix is root folder to be appended. 243func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { 244 tw := tar.NewWriter(w) 245 defer tw.Close() 246 247 c, err := g.r.CommitObject(g.h) 248 if err != nil { 249 return fmt.Errorf("commit object: %w", err) 250 } 251 252 tree, err := c.Tree() 253 if err != nil { 254 return err 255 } 256 257 walker := object.NewTreeWalker(tree, true, nil) 258 defer walker.Close() 259 260 name, entry, err := walker.Next() 261 for ; err == nil; name, entry, err = walker.Next() { 262 info, err := newInfoWrapper(name, prefix, &entry, tree) 263 if err != nil { 264 return err 265 } 266 267 header, err := tar.FileInfoHeader(info, "") 268 if err != nil { 269 return err 270 } 271 272 err = tw.WriteHeader(header) 273 if err != nil { 274 return err 275 } 276 277 if !info.IsDir() { 278 file, err := tree.File(name) 279 if err != nil { 280 return err 281 } 282 283 reader, err := file.Blob.Reader() 284 if err != nil { 285 return err 286 } 287 288 _, err = io.Copy(tw, reader) 289 if err != nil { 290 reader.Close() 291 return err 292 } 293 reader.Close() 294 } 295 } 296 297 return nil 298} 299 300func (g *GitRepo) LastCommitForPath(path string) (*object.Commit, error) { 301 cacheKey := fmt.Sprintf("%s:%s", g.h.String(), path) 302 cacheMu.RLock() 303 if commit, found := commitCache.Get(cacheKey); found { 304 cacheMu.RUnlock() 305 return commit.(*object.Commit), nil 306 } 307 cacheMu.RUnlock() 308 309 cmd := exec.Command("git", "-C", g.path, "log", "-1", "--format=%H", "--", path) 310 311 var out bytes.Buffer 312 cmd.Stdout = &out 313 cmd.Stderr = &out 314 315 if err := cmd.Run(); err != nil { 316 return nil, fmt.Errorf("failed to get commit hash: %w", err) 317 } 318 319 commitHash := strings.TrimSpace(out.String()) 320 if commitHash == "" { 321 return nil, fmt.Errorf("no commits found for path: %s", path) 322 } 323 324 hash := plumbing.NewHash(commitHash) 325 326 commit, err := g.r.CommitObject(hash) 327 if err != nil { 328 return nil, err 329 } 330 331 cacheMu.Lock() 332 commitCache.Set(cacheKey, commit, 1) 333 cacheMu.Unlock() 334 335 return commit, nil 336} 337 338func newInfoWrapper( 339 name string, 340 prefix string, 341 entry *object.TreeEntry, 342 tree *object.Tree, 343) (*infoWrapper, error) { 344 var ( 345 size int64 346 mode fs.FileMode 347 isDir bool 348 ) 349 350 if entry.Mode.IsFile() { 351 file, err := tree.TreeEntryFile(entry) 352 if err != nil { 353 return nil, err 354 } 355 mode = fs.FileMode(file.Mode) 356 357 size, err = tree.Size(name) 358 if err != nil { 359 return nil, err 360 } 361 } else { 362 isDir = true 363 mode = fs.ModeDir | fs.ModePerm 364 } 365 366 fullname := path.Join(prefix, name) 367 return &infoWrapper{ 368 name: fullname, 369 size: size, 370 mode: mode, 371 modTime: time.Unix(0, 0), 372 isDir: isDir, 373 }, nil 374} 375 376func (i *infoWrapper) Name() string { 377 return i.name 378} 379 380func (i *infoWrapper) Size() int64 { 381 return i.size 382} 383 384func (i *infoWrapper) Mode() fs.FileMode { 385 return i.mode 386} 387 388func (i *infoWrapper) ModTime() time.Time { 389 return i.modTime 390} 391 392func (i *infoWrapper) IsDir() bool { 393 return i.isDir 394} 395 396func (i *infoWrapper) Sys() any { 397 return nil 398} 399 400func (t *TagReference) Name() string { 401 return t.ref.Name().Short() 402} 403 404func (t *TagReference) Message() string { 405 if t.tag != nil { 406 return t.tag.Message 407 } 408 return "" 409} 410 411func (t *TagReference) TagObject() *object.Tag { 412 return t.tag 413} 414 415func (t *TagReference) Hash() plumbing.Hash { 416 return t.ref.Hash() 417}