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