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