Monorepo for Tangled tangled.org
2

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