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