1package git23import (4 "bufio"5 "bytes"6 "context"7 "fmt"8 "io"9 "os"10 "path"11 "path/filepath"12 "strconv"13 "strings"14)1516// ObjectFormat represent the hash algorithm of a Git repository17type ObjectFormat string1819const (20 ObjectFormatSHA1 ObjectFormat = "sha1"21 ObjectFormatSHA256 ObjectFormat = "sha256"22)2324func (obf *ObjectFormat) String() string {25 return string(*obf)26}2728// Repository contains information of a Git repository.29type Repository struct {30 path string3132 cachedCommits *objectCache33 cachedTags *objectCache34 cachedTrees *objectCache35 cachedObjectFomart ObjectFormat36}3738// Path returns the path of the repository.39func (r *Repository) Path() string {40 return r.path41}4243const LogFormatHashOnly = `format:%H`4445// parsePrettyFormatLogToList returns a list of commits parsed from given logs46// that are formatted in LogFormatHashOnly.47func (r *Repository) parsePrettyFormatLogToList(ctx context.Context, logs []byte) ([]*Commit, error) {48 if len(logs) == 0 {49 return []*Commit{}, nil50 }5152 var err error53 ids := bytes.Split(logs, []byte{'\n'})54 commits := make([]*Commit, len(ids))55 for i, id := range ids {56 commits[i], err = r.CatFileCommit(ctx, string(id))57 if err != nil {58 return nil, err59 }60 }61 return commits, nil62}6364// InitOptions contains optional arguments for initializing a repository.65//66// Docs: https://git-scm.com/docs/git-init67type InitOptions struct {68 // Indicates whether the repository should be initialized in bare format.69 Bare bool70 ObjectFormat ObjectFormat7172 CommandOptions73}7475// Init initializes a new Git repository.76func Init(ctx context.Context, path string, opts ...InitOptions) error {77 var opt InitOptions78 if len(opts) > 0 {79 opt = opts[0]80 }8182 err := os.MkdirAll(path, os.ModePerm)83 if err != nil {84 return err85 }8687 args := []string{"init"}88 if opt.Bare {89 args = append(args, "--bare")90 }91 if opt.ObjectFormat != "" {92 args = append(args, "--object-format", opt.ObjectFormat.String())93 }94 args = append(args, "--end-of-options")95 _, err = exec(ctx, path, args, opt.Envs)96 return err97}9899// Open opens the repository at the given path. It returns an os.ErrNotExist if100// the path does not exist.101func Open(repoPath string) (*Repository, error) {102 repoPath, err := filepath.Abs(repoPath)103 if err != nil {104 return nil, err105 } else if !isDir(repoPath) {106 return nil, os.ErrNotExist107 }108109 return &Repository{110 path: repoPath,111 cachedCommits: newObjectCache(),112 cachedTags: newObjectCache(),113 cachedTrees: newObjectCache(),114 }, nil115}116117// CloneOptions contains optional arguments for cloning a repository.118//119// Docs: https://git-scm.com/docs/git-clone120type CloneOptions struct {121 // Indicates whether the repository should be cloned as a mirror.122 Mirror bool123 // Indicates whether the repository should be cloned in bare format.124 Bare bool125 // Indicates whether to suppress the log output.126 Quiet bool127 // The branch to checkout for the working tree when Bare=false.128 Branch string129 // The number of revisions to clone.130 Depth uint64131 CommandOptions132}133134// Clone clones the repository from remote URL to the destination.135func Clone(ctx context.Context, url, dst string, opts ...CloneOptions) error {136 var opt CloneOptions137 if len(opts) > 0 {138 opt = opts[0]139 }140141 err := os.MkdirAll(path.Dir(dst), os.ModePerm)142 if err != nil {143 return err144 }145146 args := []string{"clone"}147 if opt.Mirror {148 args = append(args, "--mirror")149 }150 if opt.Bare {151 args = append(args, "--bare")152 }153 if opt.Quiet {154 args = append(args, "--quiet")155 }156 if !opt.Bare && opt.Branch != "" {157 args = append(args, "-b", opt.Branch)158 }159 if opt.Depth > 0 {160 args = append(args, "--depth", strconv.FormatUint(opt.Depth, 10))161 }162163 args = append(args, "--end-of-options", url, dst)164 _, err = exec(ctx, "", args, opt.Envs)165 return err166}167168// FetchOptions contains optional arguments for fetching repository updates.169//170// Docs: https://git-scm.com/docs/git-fetch171type FetchOptions struct {172 // Indicates whether to prune during fetching.173 Prune bool174 CommandOptions175}176177// Fetch fetches updates for the repository.178func (r *Repository) Fetch(ctx context.Context, opts ...FetchOptions) error {179 var opt FetchOptions180 if len(opts) > 0 {181 opt = opts[0]182 }183184 args := []string{"fetch"}185 if opt.Prune {186 args = append(args, "--prune")187 }188 args = append(args, "--end-of-options")189190 _, err := exec(ctx, r.path, args, opt.Envs)191 return err192}193194// PullOptions contains optional arguments for pulling repository updates.195//196// Docs: https://git-scm.com/docs/git-pull197type PullOptions struct {198 // Indicates whether to rebased during pulling.199 Rebase bool200 // Indicates whether to pull from all remotes.201 All bool202 // The remote to pull updates from when All=false.203 Remote string204 // The branch to pull updates from when All=false and Remote is supplied.205 Branch string206 CommandOptions207}208209// Pull pulls updates for the repository.210func (r *Repository) Pull(ctx context.Context, opts ...PullOptions) error {211 var opt PullOptions212 if len(opts) > 0 {213 opt = opts[0]214 }215216 args := []string{"pull"}217 if opt.Rebase {218 args = append(args, "--rebase")219 }220 if opt.All {221 args = append(args, "--all")222 }223 args = append(args, "--end-of-options")224 if !opt.All && opt.Remote != "" {225 args = append(args, opt.Remote)226 if opt.Branch != "" {227 args = append(args, opt.Branch)228 }229 }230231 _, err := exec(ctx, r.path, args, opt.Envs)232 return err233}234235// PushOptions contains optional arguments for pushing repository changes.236//237// Docs: https://git-scm.com/docs/git-push238type PushOptions struct {239 // Indicates whether to set upstream tracking for the branch.240 SetUpstream bool241 CommandOptions242}243244// Push pushes local changes to given remote and branch for the repository.245func (r *Repository) Push(ctx context.Context, remote, branch string, opts ...PushOptions) error {246 var opt PushOptions247 if len(opts) > 0 {248 opt = opts[0]249 }250251 args := []string{"push"}252 if opt.SetUpstream {253 args = append(args, "-u")254 }255 args = append(args, "--end-of-options", remote, branch)256 _, err := exec(ctx, r.path, args, opt.Envs)257 return err258}259260// CheckoutOptions contains optional arguments for checking out to a branch.261//262// Docs: https://git-scm.com/docs/git-checkout263type CheckoutOptions struct {264 // The base branch if checks out to a new branch.265 BaseBranch string266 CommandOptions267}268269// Checkout checks out to given branch for the repository.270func (r *Repository) Checkout(ctx context.Context, branch string, opts ...CheckoutOptions) error {271 var opt CheckoutOptions272 if len(opts) > 0 {273 opt = opts[0]274 }275276 args := []string{"checkout"}277 if opt.BaseBranch != "" {278 args = append(args, "-b", branch, "--end-of-options", opt.BaseBranch)279 } else {280 args = append(args, "--end-of-options", branch)281 }282283 _, err := exec(ctx, r.path, args, opt.Envs)284 return err285}286287// ResetOptions contains optional arguments for resetting a branch.288//289// Docs: https://git-scm.com/docs/git-reset290type ResetOptions struct {291 // Indicates whether to perform a hard reset.292 Hard bool293 CommandOptions294}295296// Reset resets working tree to given revision for the repository.297func (r *Repository) Reset(ctx context.Context, rev string, opts ...ResetOptions) error {298 var opt ResetOptions299 if len(opts) > 0 {300 opt = opts[0]301 }302303 args := []string{"reset"}304 if opt.Hard {305 args = append(args, "--hard")306 }307 args = append(args, "--end-of-options", rev)308309 _, err := exec(ctx, r.path, args, opt.Envs)310 return err311}312313// MoveOptions contains optional arguments for moving a file, a directory, or a314// symlink.315//316// Docs: https://git-scm.com/docs/git-mv317type MoveOptions struct {318 CommandOptions319}320321// Move moves a file, a directory, or a symlink file or directory from source to322// destination for the repository.323func (r *Repository) Move(ctx context.Context, src, dst string, opts ...MoveOptions) error {324 var opt MoveOptions325 if len(opts) > 0 {326 opt = opts[0]327 }328329 args := []string{"mv", "--end-of-options", src, dst}330 _, err := exec(ctx, r.path, args, opt.Envs)331 return err332}333334// AddOptions contains optional arguments for adding local changes.335//336// Docs: https://git-scm.com/docs/git-add337type AddOptions struct {338 // Indicates whether to add all changes to index.339 All bool340 // The specific pathspecs to be added to index.341 Pathspecs []string342 CommandOptions343}344345// Add adds local changes to index for the repository.346func (r *Repository) Add(ctx context.Context, opts ...AddOptions) error {347 var opt AddOptions348 if len(opts) > 0 {349 opt = opts[0]350 }351352 args := []string{"add"}353 if opt.All {354 args = append(args, "--all")355 }356 if len(opt.Pathspecs) > 0 {357 args = append(args, "--")358 args = append(args, opt.Pathspecs...)359 }360 _, err := exec(ctx, r.path, args, opt.Envs)361 return err362}363364// CommitOptions contains optional arguments to commit changes.365//366// Docs: https://git-scm.com/docs/git-commit367type CommitOptions struct {368 // Author is the author of the changes if that's not the same as committer.369 Author *Signature370 CommandOptions371}372373// Commit commits local changes with given author, committer and message for the374// repository.375func (r *Repository) Commit(ctx context.Context, committer *Signature, message string, opts ...CommitOptions) error {376 var opt CommitOptions377 if len(opts) > 0 {378 opt = opts[0]379 }380381 envs := committerEnvs(committer)382 envs = append(envs, opt.Envs...)383384 if opt.Author == nil {385 opt.Author = committer386 }387388 args := []string{"commit"}389 args = append(args, fmt.Sprintf("--author=%s <%s>", opt.Author.Name, opt.Author.Email))390 args = append(args, "-m", message)391 args = append(args, "--end-of-options")392393 _, err := exec(ctx, r.path, args, envs)394 // No stderr but exit status 1 means nothing to commit.395 if isExitStatus(err, 1) {396 return nil397 }398 return err399}400401// NameStatus contains name status of a commit.402type NameStatus struct {403 Added []string404 Removed []string405 Modified []string406}407408// ShowNameStatusOptions contains optional arguments for showing name status.409//410// Docs: https://git-scm.com/docs/git-show#Documentation/git-show.txt---name-status411type ShowNameStatusOptions struct {412 CommandOptions413}414415// ShowNameStatus returns name status of given revision of the repository.416func (r *Repository) ShowNameStatus(ctx context.Context, rev string, opts ...ShowNameStatusOptions) (*NameStatus, error) {417 var opt ShowNameStatusOptions418 if len(opts) > 0 {419 opt = opts[0]420 }421422 fileStatus := &NameStatus{}423 stdout, w := io.Pipe()424 done := make(chan struct{})425 go func() {426 scanner := bufio.NewScanner(stdout)427 for scanner.Scan() {428 fields := strings.Fields(scanner.Text())429 if len(fields) < 2 {430 continue431 }432433 switch fields[0][0] {434 case 'A':435 fileStatus.Added = append(fileStatus.Added, fields[1])436 case 'D':437 fileStatus.Removed = append(fileStatus.Removed, fields[1])438 case 'M':439 fileStatus.Modified = append(fileStatus.Modified, fields[1])440 }441 }442 done <- struct{}{}443 }()444445 args := []string{"show", "--name-status", "--pretty=format:''", "--end-of-options", rev}446447 err := pipe(ctx, r.path, args, opt.Envs, w)448 _ = w.Close() // Close writer to exit parsing goroutine449 if err != nil {450 return nil, err451 }452453 <-done454 return fileStatus, nil455}456457// RevParseOptions contains optional arguments for parsing revision.458//459// Docs: https://git-scm.com/docs/git-rev-parse460type RevParseOptions struct {461 CommandOptions462}463464// RevParse returns full length (40 or 64) commit ID by given revision in the465// repository.466func (r *Repository) RevParse(ctx context.Context, rev string, opts ...RevParseOptions) (string, error) {467 var opt RevParseOptions468 if len(opts) > 0 {469 opt = opts[0]470 }471472 args := []string{"rev-parse", rev}473474 commitID, err := exec(ctx, r.path, args, opt.Envs)475 if err != nil {476 if isExitStatus(err, 128) {477 return "", ErrRevisionNotExist478 }479 return "", err480 }481 return strings.TrimSpace(string(commitID)), nil482}483484// CountObject contains disk usage report of a repository.485type CountObject struct {486 Count int64487 Size int64488 InPack int64489 Packs int64490 SizePack int64491 PrunePackable int64492 Garbage int64493 SizeGarbage int64494}495496// CountObjectsOptions contains optional arguments for counting objects.497//498// Docs: https://git-scm.com/docs/git-count-objects499type CountObjectsOptions struct {500 CommandOptions501}502503// CountObjects returns disk usage report of the repository.504func (r *Repository) CountObjects(ctx context.Context, opts ...CountObjectsOptions) (*CountObject, error) {505 var opt CountObjectsOptions506 if len(opts) > 0 {507 opt = opts[0]508 }509510 args := []string{"count-objects", "-v", "--end-of-options"}511512 stdout, err := exec(ctx, r.path, args, opt.Envs)513 if err != nil {514 return nil, err515 }516517 toInt64 := func(b []byte) int64 {518 i, _ := strconv.ParseInt(string(b), 10, 64)519 return i520 }521522 countObject := new(CountObject)523 for _, line := range bytes.Split(stdout, []byte("\n")) {524 switch {525 case bytes.HasPrefix(line, []byte("count: ")):526 countObject.Count = toInt64(line[7:])527 case bytes.HasPrefix(line, []byte("size: ")):528 countObject.Size = toInt64(line[6:]) * 1024529 case bytes.HasPrefix(line, []byte("in-pack: ")):530 countObject.InPack = toInt64(line[9:])531 case bytes.HasPrefix(line, []byte("packs: ")):532 countObject.Packs = toInt64(line[7:])533 case bytes.HasPrefix(line, []byte("size-pack: ")):534 countObject.SizePack = toInt64(line[11:]) * 1024535 case bytes.HasPrefix(line, []byte("prune-packable: ")):536 countObject.PrunePackable = toInt64(line[16:])537 case bytes.HasPrefix(line, []byte("garbage: ")):538 countObject.Garbage = toInt64(line[9:])539 case bytes.HasPrefix(line, []byte("size-garbage: ")):540 countObject.SizeGarbage = toInt64(line[14:]) * 1024541 }542 }543544 return countObject, nil545}546547// FsckOptions contains optional arguments for verifying the objects.548//549// Docs: https://git-scm.com/docs/git-fsck550type FsckOptions struct {551 CommandOptions552}553554// Fsck verifies the connectivity and validity of the objects in the database555// for the repository.556func (r *Repository) Fsck(ctx context.Context, opts ...FsckOptions) error {557 var opt FsckOptions558 if len(opts) > 0 {559 opt = opts[0]560 }561562 args := []string{"fsck", "--end-of-options"}563 _, err := exec(ctx, r.path, args, opt.Envs)564 return err565566}567568// ObjectFomat returns hash algorithm (sha1 or sha256) of the repository569func (r *Repository) ObjectFormat(ctx context.Context) (ObjectFormat, error) {570 if r.cachedObjectFomart != "" {571 return r.cachedObjectFomart, nil572 }573574 id, err := exec(ctx, r.path, []string{"hash-object", "--stdin"}, nil)575 if err != nil {576 return "", fmt.Errorf("hash object: %v", err)577 }578 oid, err := NewIDFromString(strings.TrimSpace(string(id)))579 if err != nil {580 return "", fmt.Errorf("parse oid: %v", err)581 }582 switch oid.(type) {583 case *SHA1:584 r.cachedObjectFomart = ObjectFormatSHA1585 return ObjectFormatSHA1, nil586 case *SHA256:587 r.cachedObjectFomart = ObjectFormatSHA256588 return ObjectFormatSHA256, nil589 default:590 return "", fmt.Errorf("unknown object format")591 }592}