forked from tangled.org/core
this repo has no description tangled.org
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 fmt.Sprintf("--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) Branch(name string) (*plumbing.Reference, error) { 256 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false) 257 if err != nil { 258 return nil, fmt.Errorf("branch: %w", err) 259 } 260 261 if !ref.Name().IsBranch() { 262 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name()) 263 } 264 265 return ref, nil 266} 267 268func (g *GitRepo) SetDefaultBranch(branch string) error { 269 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch)) 270 return g.r.Storer.SetReference(ref) 271} 272 273func (g *GitRepo) FindMainBranch() (string, error) { 274 output, err := g.revParse("--abbrev-ref", "HEAD") 275 if err != nil { 276 return "", fmt.Errorf("failed to find main branch: %w", err) 277 } 278 279 return strings.TrimSpace(string(output)), nil 280} 281 282// WriteTar writes itself from a tree into a binary tar file format. 283// prefix is root folder to be appended. 284func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { 285 tw := tar.NewWriter(w) 286 defer tw.Close() 287 288 c, err := g.r.CommitObject(g.h) 289 if err != nil { 290 return fmt.Errorf("commit object: %w", err) 291 } 292 293 tree, err := c.Tree() 294 if err != nil { 295 return err 296 } 297 298 walker := object.NewTreeWalker(tree, true, nil) 299 defer walker.Close() 300 301 name, entry, err := walker.Next() 302 for ; err == nil; name, entry, err = walker.Next() { 303 info, err := newInfoWrapper(name, prefix, &entry, tree) 304 if err != nil { 305 return err 306 } 307 308 header, err := tar.FileInfoHeader(info, "") 309 if err != nil { 310 return err 311 } 312 313 err = tw.WriteHeader(header) 314 if err != nil { 315 return err 316 } 317 318 if !info.IsDir() { 319 file, err := tree.File(name) 320 if err != nil { 321 return err 322 } 323 324 reader, err := file.Blob.Reader() 325 if err != nil { 326 return err 327 } 328 329 _, err = io.Copy(tw, reader) 330 if err != nil { 331 reader.Close() 332 return err 333 } 334 reader.Close() 335 } 336 } 337 338 return nil 339} 340 341func newInfoWrapper( 342 name string, 343 prefix string, 344 entry *object.TreeEntry, 345 tree *object.Tree, 346) (*infoWrapper, error) { 347 var ( 348 size int64 349 mode fs.FileMode 350 isDir bool 351 ) 352 353 if entry.Mode.IsFile() { 354 file, err := tree.TreeEntryFile(entry) 355 if err != nil { 356 return nil, err 357 } 358 mode = fs.FileMode(file.Mode) 359 360 size, err = tree.Size(name) 361 if err != nil { 362 return nil, err 363 } 364 } else { 365 isDir = true 366 mode = fs.ModeDir | fs.ModePerm 367 } 368 369 fullname := path.Join(prefix, name) 370 return &infoWrapper{ 371 name: fullname, 372 size: size, 373 mode: mode, 374 modTime: time.Unix(0, 0), 375 isDir: isDir, 376 }, nil 377} 378 379func (i *infoWrapper) Name() string { 380 return i.name 381} 382 383func (i *infoWrapper) Size() int64 { 384 return i.size 385} 386 387func (i *infoWrapper) Mode() fs.FileMode { 388 return i.mode 389} 390 391func (i *infoWrapper) ModTime() time.Time { 392 return i.modTime 393} 394 395func (i *infoWrapper) IsDir() bool { 396 return i.isDir 397} 398 399func (i *infoWrapper) Sys() any { 400 return nil 401}