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