forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 8.0 kB view raw
1package git 2 3import ( 4 "archive/tar" 5 "bytes" 6 "errors" 7 "fmt" 8 "io" 9 "io/fs" 10 "path" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/go-git/go-git/v5" 16 "github.com/go-git/go-git/v5/config" 17 "github.com/go-git/go-git/v5/plumbing" 18 "github.com/go-git/go-git/v5/plumbing/object" 19) 20 21var ( 22 ErrBinaryFile = errors.New("binary file") 23 ErrNotBinaryFile = errors.New("not binary file") 24 ErrMissingGitModules = errors.New("no .gitmodules file found") 25 ErrInvalidGitModules = errors.New("invalid .gitmodules file") 26 ErrNotSubmodule = errors.New("path is not a submodule") 27) 28 29type GitRepo struct { 30 path string 31 r *git.Repository 32 h plumbing.Hash 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 Open(path string, ref string) (*GitRepo, error) { 46 var err error 47 g := GitRepo{path: path} 48 g.r, err = git.PlainOpen(path) 49 if err != nil { 50 return nil, fmt.Errorf("opening %s: %w", path, err) 51 } 52 53 if ref == "" { 54 head, err := g.r.Head() 55 if err != nil { 56 return nil, fmt.Errorf("getting head of %s: %w", path, err) 57 } 58 g.h = head.Hash() 59 } else { 60 hash, err := g.r.ResolveRevision(plumbing.Revision(ref)) 61 if err != nil { 62 return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err) 63 } 64 g.h = *hash 65 } 66 return &g, nil 67} 68 69func PlainOpen(path string) (*GitRepo, error) { 70 var err error 71 g := GitRepo{path: path} 72 g.r, err = git.PlainOpen(path) 73 if err != nil { 74 return nil, fmt.Errorf("opening %s: %w", path, err) 75 } 76 return &g, nil 77} 78 79// re-open a repository and update references 80func (g *GitRepo) Refresh() error { 81 refreshed, err := PlainOpen(g.path) 82 if err != nil { 83 return err 84 } 85 86 *g = *refreshed 87 return nil 88} 89 90func (g *GitRepo) Commits(offset, limit int) ([]*object.Commit, error) { 91 commits := []*object.Commit{} 92 93 output, err := g.revList( 94 g.h.String(), 95 fmt.Sprintf("--skip=%d", offset), 96 fmt.Sprintf("--max-count=%d", limit), 97 ) 98 if err != nil { 99 return nil, fmt.Errorf("commits from ref: %w", err) 100 } 101 102 lines := strings.Split(strings.TrimSpace(string(output)), "\n") 103 if len(lines) == 1 && lines[0] == "" { 104 return commits, nil 105 } 106 107 for _, item := range lines { 108 obj, err := g.r.CommitObject(plumbing.NewHash(item)) 109 if err != nil { 110 continue 111 } 112 commits = append(commits, obj) 113 } 114 115 return commits, nil 116} 117 118func (g *GitRepo) TotalCommits() (int, error) { 119 output, err := g.revList( 120 g.h.String(), 121 fmt.Sprintf("--count"), 122 ) 123 if err != nil { 124 return 0, fmt.Errorf("failed to run rev-list: %w", err) 125 } 126 127 count, err := strconv.Atoi(strings.TrimSpace(string(output))) 128 if err != nil { 129 return 0, err 130 } 131 132 return count, nil 133} 134 135func (g *GitRepo) Commit(h plumbing.Hash) (*object.Commit, error) { 136 return g.r.CommitObject(h) 137} 138 139func (g *GitRepo) FileContentN(path string, cap int64) ([]byte, error) { 140 c, err := g.r.CommitObject(g.h) 141 if err != nil { 142 return nil, fmt.Errorf("commit object: %w", err) 143 } 144 145 tree, err := c.Tree() 146 if err != nil { 147 return nil, fmt.Errorf("file tree: %w", err) 148 } 149 150 file, err := tree.File(path) 151 if err != nil { 152 return nil, err 153 } 154 155 isbin, _ := file.IsBinary() 156 if isbin { 157 return nil, ErrBinaryFile 158 } 159 160 reader, err := file.Reader() 161 if err != nil { 162 return nil, err 163 } 164 165 buf := new(bytes.Buffer) 166 if _, err = buf.ReadFrom(io.LimitReader(reader, cap)); err != nil { 167 return nil, err 168 } 169 170 return buf.Bytes(), nil 171} 172 173func (g *GitRepo) RawContent(path string) ([]byte, error) { 174 c, err := g.r.CommitObject(g.h) 175 if err != nil { 176 return nil, fmt.Errorf("commit object: %w", err) 177 } 178 179 tree, err := c.Tree() 180 if err != nil { 181 return nil, fmt.Errorf("file tree: %w", err) 182 } 183 184 file, err := tree.File(path) 185 if err != nil { 186 return nil, err 187 } 188 189 reader, err := file.Reader() 190 if err != nil { 191 return nil, fmt.Errorf("opening file reader: %w", err) 192 } 193 defer reader.Close() 194 195 return io.ReadAll(reader) 196} 197 198// read and parse .gitmodules 199func (g *GitRepo) Submodules() (*config.Modules, error) { 200 c, err := g.r.CommitObject(g.h) 201 if err != nil { 202 return nil, fmt.Errorf("commit object: %w", err) 203 } 204 205 tree, err := c.Tree() 206 if err != nil { 207 return nil, fmt.Errorf("tree: %w", err) 208 } 209 210 // read .gitmodules file 211 modulesEntry, err := tree.FindEntry(".gitmodules") 212 if err != nil { 213 return nil, fmt.Errorf("%w: %w", ErrMissingGitModules, err) 214 } 215 216 modulesFile, err := tree.TreeEntryFile(modulesEntry) 217 if err != nil { 218 return nil, fmt.Errorf("%w: failed to read file: %w", ErrInvalidGitModules, err) 219 } 220 221 content, err := modulesFile.Contents() 222 if err != nil { 223 return nil, fmt.Errorf("%w: failed to read contents: %w", ErrInvalidGitModules, err) 224 } 225 226 // parse .gitmodules 227 modules := config.NewModules() 228 if err = modules.Unmarshal([]byte(content)); err != nil { 229 return nil, fmt.Errorf("%w: failed to parse: %w", ErrInvalidGitModules, err) 230 } 231 232 return modules, nil 233} 234 235func (g *GitRepo) Submodule(path string) (*config.Submodule, error) { 236 modules, err := g.Submodules() 237 if err != nil { 238 return nil, err 239 } 240 241 for _, submodule := range modules.Submodules { 242 if submodule.Path == path { 243 return submodule, nil 244 } 245 } 246 247 // path is not a submodule 248 return nil, ErrNotSubmodule 249} 250 251func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) { 252 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false) 253 if err != nil { 254 return nil, fmt.Errorf("branch: %w", err) 255 } 256 257 if !ref.Name().IsBranch() { 258 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name()) 259 } 260 261 return ref, nil 262} 263 264func (g *GitRepo) SetDefaultBranch(branch string) error { 265 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch)) 266 return g.r.Storer.SetReference(ref) 267} 268 269func (g *GitRepo) FindMainBranch() (string, error) { 270 output, err := g.revParse("--abbrev-ref", "HEAD") 271 if err != nil { 272 return "", fmt.Errorf("failed to find main branch: %w", err) 273 } 274 275 return strings.TrimSpace(string(output)), nil 276} 277 278// WriteTar writes itself from a tree into a binary tar file format. 279// prefix is root folder to be appended. 280func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { 281 tw := tar.NewWriter(w) 282 defer tw.Close() 283 284 c, err := g.r.CommitObject(g.h) 285 if err != nil { 286 return fmt.Errorf("commit object: %w", err) 287 } 288 289 tree, err := c.Tree() 290 if err != nil { 291 return err 292 } 293 294 walker := object.NewTreeWalker(tree, true, nil) 295 defer walker.Close() 296 297 name, entry, err := walker.Next() 298 for ; err == nil; name, entry, err = walker.Next() { 299 info, err := newInfoWrapper(name, prefix, &entry, tree) 300 if err != nil { 301 return err 302 } 303 304 header, err := tar.FileInfoHeader(info, "") 305 if err != nil { 306 return err 307 } 308 309 err = tw.WriteHeader(header) 310 if err != nil { 311 return err 312 } 313 314 if !info.IsDir() { 315 file, err := tree.File(name) 316 if err != nil { 317 return err 318 } 319 320 reader, err := file.Blob.Reader() 321 if err != nil { 322 return err 323 } 324 325 _, err = io.Copy(tw, reader) 326 if err != nil { 327 reader.Close() 328 return err 329 } 330 reader.Close() 331 } 332 } 333 334 return nil 335} 336 337func newInfoWrapper( 338 name string, 339 prefix string, 340 entry *object.TreeEntry, 341 tree *object.Tree, 342) (*infoWrapper, error) { 343 var ( 344 size int64 345 mode fs.FileMode 346 isDir bool 347 ) 348 349 if entry.Mode.IsFile() { 350 file, err := tree.TreeEntryFile(entry) 351 if err != nil { 352 return nil, err 353 } 354 mode = fs.FileMode(file.Mode) 355 356 size, err = tree.Size(name) 357 if err != nil { 358 return nil, err 359 } 360 } else { 361 isDir = true 362 mode = fs.ModeDir | fs.ModePerm 363 } 364 365 fullname := path.Join(prefix, name) 366 return &infoWrapper{ 367 name: fullname, 368 size: size, 369 mode: mode, 370 modTime: time.Unix(0, 0), 371 isDir: isDir, 372 }, nil 373} 374 375func (i *infoWrapper) Name() string { 376 return i.name 377} 378 379func (i *infoWrapper) Size() int64 { 380 return i.size 381} 382 383func (i *infoWrapper) Mode() fs.FileMode { 384 return i.mode 385} 386 387func (i *infoWrapper) ModTime() time.Time { 388 return i.modTime 389} 390 391func (i *infoWrapper) IsDir() bool { 392 return i.isDir 393} 394 395func (i *infoWrapper) Sys() any { 396 return nil 397}