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