Monorepo for Tangled
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at ca1162d4e168afc2b4b3f0fbb2eb91c152d9146f 403 lines 8.1 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 := Open(g.path, g.Hash().String()) 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 defer reader.Close() 169 170 buf := new(bytes.Buffer) 171 if _, err = buf.ReadFrom(io.LimitReader(reader, cap)); err != nil { 172 return nil, err 173 } 174 175 return buf.Bytes(), nil 176} 177 178func (g *GitRepo) RawContent(path string) ([]byte, error) { 179 c, err := g.r.CommitObject(g.h) 180 if err != nil { 181 return nil, fmt.Errorf("commit object: %w", err) 182 } 183 184 tree, err := c.Tree() 185 if err != nil { 186 return nil, fmt.Errorf("file tree: %w", err) 187 } 188 189 file, err := tree.File(path) 190 if err != nil { 191 return nil, err 192 } 193 194 reader, err := file.Reader() 195 if err != nil { 196 return nil, fmt.Errorf("opening file reader: %w", err) 197 } 198 defer reader.Close() 199 200 return io.ReadAll(reader) 201} 202 203func (g *GitRepo) File(path string) (*object.File, 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("file tree: %w", err) 212 } 213 214 return tree.File(path) 215} 216 217// read and parse .gitmodules 218func (g *GitRepo) Submodules() (*config.Modules, error) { 219 c, err := g.r.CommitObject(g.h) 220 if err != nil { 221 return nil, fmt.Errorf("commit object: %w", err) 222 } 223 224 tree, err := c.Tree() 225 if err != nil { 226 return nil, fmt.Errorf("tree: %w", err) 227 } 228 229 // read .gitmodules file 230 modulesEntry, err := tree.FindEntry(".gitmodules") 231 if err != nil { 232 return nil, fmt.Errorf("%w: %w", ErrMissingGitModules, err) 233 } 234 235 modulesFile, err := tree.TreeEntryFile(modulesEntry) 236 if err != nil { 237 return nil, fmt.Errorf("%w: failed to read file: %w", ErrInvalidGitModules, err) 238 } 239 240 content, err := modulesFile.Contents() 241 if err != nil { 242 return nil, fmt.Errorf("%w: failed to read contents: %w", ErrInvalidGitModules, err) 243 } 244 245 // parse .gitmodules 246 modules := config.NewModules() 247 if err = modules.Unmarshal([]byte(content)); err != nil { 248 return nil, fmt.Errorf("%w: failed to parse: %w", ErrInvalidGitModules, err) 249 } 250 251 return modules, nil 252} 253 254func (g *GitRepo) Submodule(path string) (*config.Submodule, error) { 255 modules, err := g.Submodules() 256 if err != nil { 257 return nil, err 258 } 259 260 for _, submodule := range modules.Submodules { 261 if submodule.Path == path { 262 return submodule, nil 263 } 264 } 265 266 // path is not a submodule 267 return nil, ErrNotSubmodule 268} 269 270func (g *GitRepo) SetDefaultBranch(branch string) error { 271 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch)) 272 return g.r.Storer.SetReference(ref) 273} 274 275func (g *GitRepo) FindMainBranch() (string, error) { 276 output, err := g.revParse("--abbrev-ref", "HEAD") 277 if err != nil { 278 return "", fmt.Errorf("failed to find main branch: %w", err) 279 } 280 281 return strings.TrimSpace(string(output)), nil 282} 283 284// WriteTar writes itself from a tree into a binary tar file format. 285// prefix is root folder to be appended. 286func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { 287 tw := tar.NewWriter(w) 288 defer tw.Close() 289 290 c, err := g.r.CommitObject(g.h) 291 if err != nil { 292 return fmt.Errorf("commit object: %w", err) 293 } 294 295 tree, err := c.Tree() 296 if err != nil { 297 return err 298 } 299 300 walker := object.NewTreeWalker(tree, true, nil) 301 defer walker.Close() 302 303 name, entry, err := walker.Next() 304 for ; err == nil; name, entry, err = walker.Next() { 305 info, err := newInfoWrapper(name, prefix, &entry, tree) 306 if err != nil { 307 return err 308 } 309 310 header, err := tar.FileInfoHeader(info, "") 311 if err != nil { 312 return err 313 } 314 315 err = tw.WriteHeader(header) 316 if err != nil { 317 return err 318 } 319 320 if !info.IsDir() { 321 file, err := tree.File(name) 322 if err != nil { 323 return err 324 } 325 326 reader, err := file.Blob.Reader() 327 if err != nil { 328 return err 329 } 330 331 _, err = io.Copy(tw, reader) 332 if err != nil { 333 reader.Close() 334 return err 335 } 336 reader.Close() 337 } 338 } 339 340 return nil 341} 342 343func newInfoWrapper( 344 name string, 345 prefix string, 346 entry *object.TreeEntry, 347 tree *object.Tree, 348) (*infoWrapper, error) { 349 var ( 350 size int64 351 mode fs.FileMode 352 isDir bool 353 ) 354 355 if entry.Mode.IsFile() { 356 file, err := tree.TreeEntryFile(entry) 357 if err != nil { 358 return nil, err 359 } 360 mode = fs.FileMode(file.Mode) 361 362 size, err = tree.Size(name) 363 if err != nil { 364 return nil, err 365 } 366 } else { 367 isDir = true 368 mode = fs.ModeDir | fs.ModePerm 369 } 370 371 fullname := path.Join(prefix, name) 372 return &infoWrapper{ 373 name: fullname, 374 size: size, 375 mode: mode, 376 modTime: time.Unix(0, 0), 377 isDir: isDir, 378 }, nil 379} 380 381func (i *infoWrapper) Name() string { 382 return i.name 383} 384 385func (i *infoWrapper) Size() int64 { 386 return i.size 387} 388 389func (i *infoWrapper) Mode() fs.FileMode { 390 return i.mode 391} 392 393func (i *infoWrapper) ModTime() time.Time { 394 return i.modTime 395} 396 397func (i *infoWrapper) IsDir() bool { 398 return i.isDir 399} 400 401func (i *infoWrapper) Sys() any { 402 return nil 403}