Monorepo for Tangled tangled.org
5

Configure Feed

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

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