github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/git/repo.go (about)

     1  // Copyright 2023 The GitBundle Inc. All rights reserved.
     2  // Copyright 2017 The Gitea Authors. All rights reserved.
     3  // Use of this source code is governed by a MIT-style
     4  // license that can be found in the LICENSE file.
     5  
     6  // Copyright 2015 The Gogs Authors. All rights reserved.
     7  
     8  package git
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"fmt"
    14  	"io"
    15  	"net/url"
    16  	"os"
    17  	"path"
    18  	"path/filepath"
    19  	"strconv"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/gitbundle/modules/proxy"
    24  	"github.com/gitbundle/modules/util"
    25  )
    26  
    27  // GPGSettings represents the default GPG settings for this repository
    28  type GPGSettings struct {
    29  	Sign             bool
    30  	KeyID            string
    31  	Email            string
    32  	Name             string
    33  	PublicKeyContent string
    34  }
    35  
    36  const prettyLogFormat = `--pretty=format:%H`
    37  
    38  // GetAllCommitsCount returns count of all commits in repository
    39  func (repo *Repository) GetAllCommitsCount() (int64, error) {
    40  	return AllCommitsCount(repo.Ctx, repo.Path, false)
    41  }
    42  
    43  func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, error) {
    44  	var commits []*Commit
    45  	if len(logs) == 0 {
    46  		return commits, nil
    47  	}
    48  
    49  	parts := bytes.Split(logs, []byte{'\n'})
    50  
    51  	for _, commitID := range parts {
    52  		commit, err := repo.GetCommit(string(commitID))
    53  		if err != nil {
    54  			return nil, err
    55  		}
    56  		commits = append(commits, commit)
    57  	}
    58  
    59  	return commits, nil
    60  }
    61  
    62  // IsRepoURLAccessible checks if given repository URL is accessible.
    63  func IsRepoURLAccessible(ctx context.Context, url string) bool {
    64  	_, _, err := NewCommand(ctx, "ls-remote", "-q", "-h", url, "HEAD").RunStdString(nil)
    65  	return err == nil
    66  }
    67  
    68  // InitRepository initializes a new Git repository.
    69  func InitRepository(ctx context.Context, repoPath string, bare bool) error {
    70  	err := os.MkdirAll(repoPath, os.ModePerm)
    71  	if err != nil {
    72  		return err
    73  	}
    74  
    75  	cmd := NewCommand(ctx, "init")
    76  	if bare {
    77  		cmd.AddArguments("--bare")
    78  	}
    79  	_, _, err = cmd.RunStdString(&RunOpts{Dir: repoPath})
    80  	return err
    81  }
    82  
    83  // IsEmpty Check if repository is empty.
    84  func (repo *Repository) IsEmpty() (bool, error) {
    85  	var errbuf, output strings.Builder
    86  	if err := NewCommand(repo.Ctx, "show-ref", "--head", "^HEAD$").
    87  		Run(&RunOpts{
    88  			Dir:    repo.Path,
    89  			Stdout: &output,
    90  			Stderr: &errbuf,
    91  		}); err != nil {
    92  		if err.Error() == "exit status 1" && errbuf.String() == "" {
    93  			return true, nil
    94  		}
    95  		return true, fmt.Errorf("check empty: %v - %s", err, errbuf.String())
    96  	}
    97  
    98  	return strings.TrimSpace(output.String()) == "", nil
    99  }
   100  
   101  // CloneRepoOptions options when clone a repository
   102  type CloneRepoOptions struct {
   103  	Timeout       time.Duration
   104  	Mirror        bool
   105  	Bare          bool
   106  	Quiet         bool
   107  	Branch        string
   108  	Shared        bool
   109  	NoCheckout    bool
   110  	Depth         int
   111  	Filter        string
   112  	SkipTLSVerify bool
   113  }
   114  
   115  // Clone clones original repository to target path.
   116  func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
   117  	cargs := make([]string, len(globalCommandArgs))
   118  	copy(cargs, globalCommandArgs)
   119  	return CloneWithArgs(ctx, from, to, cargs, opts)
   120  }
   121  
   122  // CloneWithArgs original repository to target path.
   123  func CloneWithArgs(ctx context.Context, from, to string, args []string, opts CloneRepoOptions) (err error) {
   124  	toDir := path.Dir(to)
   125  	if err = os.MkdirAll(toDir, os.ModePerm); err != nil {
   126  		return err
   127  	}
   128  
   129  	cmd := NewCommandContextNoGlobals(ctx, args...).AddArguments("clone")
   130  	if opts.SkipTLSVerify {
   131  		cmd.AddArguments("-c", "http.sslVerify=false")
   132  	}
   133  	if opts.Mirror {
   134  		cmd.AddArguments("--mirror")
   135  	}
   136  	if opts.Bare {
   137  		cmd.AddArguments("--bare")
   138  	}
   139  	if opts.Quiet {
   140  		cmd.AddArguments("--quiet")
   141  	}
   142  	if opts.Shared {
   143  		cmd.AddArguments("-s")
   144  	}
   145  	if opts.NoCheckout {
   146  		cmd.AddArguments("--no-checkout")
   147  	}
   148  	if opts.Depth > 0 {
   149  		cmd.AddArguments("--depth", strconv.Itoa(opts.Depth))
   150  	}
   151  	if opts.Filter != "" {
   152  		cmd.AddArguments("--filter", opts.Filter)
   153  	}
   154  	if len(opts.Branch) > 0 {
   155  		cmd.AddArguments("-b", opts.Branch)
   156  	}
   157  	cmd.AddArguments("--", from, to)
   158  
   159  	if strings.Contains(from, "://") && strings.Contains(from, "@") {
   160  		cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, util.SanitizeCredentialURLs(from), to, opts.Shared, opts.Mirror, opts.Depth))
   161  	} else {
   162  		cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, from, to, opts.Shared, opts.Mirror, opts.Depth))
   163  	}
   164  
   165  	if opts.Timeout <= 0 {
   166  		opts.Timeout = -1
   167  	}
   168  
   169  	envs := os.Environ()
   170  	u, err := url.Parse(from)
   171  	if err == nil && (strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https")) {
   172  		if proxy.Match(u.Host) {
   173  			envs = append(envs, fmt.Sprintf("https_proxy=%s", proxy.GetProxyURL()))
   174  		}
   175  	}
   176  
   177  	stderr := new(bytes.Buffer)
   178  	if err = cmd.Run(&RunOpts{
   179  		Timeout: opts.Timeout,
   180  		Env:     envs,
   181  		Stdout:  io.Discard,
   182  		Stderr:  stderr,
   183  	}); err != nil {
   184  		return ConcatenateError(err, stderr.String())
   185  	}
   186  	return nil
   187  }
   188  
   189  // PushOptions options when push to remote
   190  type PushOptions struct {
   191  	Remote  string
   192  	Branch  string
   193  	Force   bool
   194  	Mirror  bool
   195  	Env     []string
   196  	Timeout time.Duration
   197  }
   198  
   199  // Push pushs local commits to given remote branch.
   200  func Push(ctx context.Context, repoPath string, opts PushOptions) error {
   201  	cmd := NewCommand(ctx, "push")
   202  	if opts.Force {
   203  		cmd.AddArguments("-f")
   204  	}
   205  	if opts.Mirror {
   206  		cmd.AddArguments("--mirror")
   207  	}
   208  	cmd.AddArguments("--", opts.Remote)
   209  	if len(opts.Branch) > 0 {
   210  		cmd.AddArguments(opts.Branch)
   211  	}
   212  	if strings.Contains(opts.Remote, "://") && strings.Contains(opts.Remote, "@") {
   213  		cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, util.SanitizeCredentialURLs(opts.Remote), opts.Force, opts.Mirror))
   214  	} else {
   215  		cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, opts.Remote, opts.Force, opts.Mirror))
   216  	}
   217  	var outbuf, errbuf strings.Builder
   218  
   219  	if opts.Timeout == 0 {
   220  		opts.Timeout = -1
   221  	}
   222  
   223  	err := cmd.Run(&RunOpts{
   224  		Env:     opts.Env,
   225  		Timeout: opts.Timeout,
   226  		Dir:     repoPath,
   227  		Stdout:  &outbuf,
   228  		Stderr:  &errbuf,
   229  	})
   230  	if err != nil {
   231  		if strings.Contains(errbuf.String(), "non-fast-forward") {
   232  			return &ErrPushOutOfDate{
   233  				StdOut: outbuf.String(),
   234  				StdErr: errbuf.String(),
   235  				Err:    err,
   236  			}
   237  		} else if strings.Contains(errbuf.String(), "! [remote rejected]") {
   238  			err := &ErrPushRejected{
   239  				StdOut: outbuf.String(),
   240  				StdErr: errbuf.String(),
   241  				Err:    err,
   242  			}
   243  			err.GenerateMessage()
   244  			return err
   245  		} else if strings.Contains(errbuf.String(), "matches more than one") {
   246  			err := &ErrMoreThanOne{
   247  				StdOut: outbuf.String(),
   248  				StdErr: errbuf.String(),
   249  				Err:    err,
   250  			}
   251  			return err
   252  		}
   253  	}
   254  
   255  	if errbuf.Len() > 0 && err != nil {
   256  		return fmt.Errorf("%v - %s", err, errbuf.String())
   257  	}
   258  
   259  	return err
   260  }
   261  
   262  // GetLatestCommitTime returns time for latest commit in repository (across all branches)
   263  func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error) {
   264  	cmd := NewCommand(ctx, "for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)")
   265  	stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
   266  	if err != nil {
   267  		return time.Time{}, err
   268  	}
   269  	commitTime := strings.TrimSpace(stdout)
   270  	return time.Parse(GitTimeLayout, commitTime)
   271  }
   272  
   273  // DivergeObject represents commit count diverging commits
   274  type DivergeObject struct {
   275  	Ahead  int
   276  	Behind int
   277  }
   278  
   279  func checkDivergence(ctx context.Context, repoPath, baseBranch, targetBranch string) (int, error) {
   280  	branches := fmt.Sprintf("%s..%s", baseBranch, targetBranch)
   281  	cmd := NewCommand(ctx, "rev-list", "--count", branches)
   282  	stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
   283  	if err != nil {
   284  		return -1, err
   285  	}
   286  	outInteger, errInteger := strconv.Atoi(strings.Trim(stdout, "\n"))
   287  	if errInteger != nil {
   288  		return -1, errInteger
   289  	}
   290  	return outInteger, nil
   291  }
   292  
   293  // GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch
   294  func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (DivergeObject, error) {
   295  	// $(git rev-list --count master..feature) commits ahead of master
   296  	ahead, errorAhead := checkDivergence(ctx, repoPath, baseBranch, targetBranch)
   297  	if errorAhead != nil {
   298  		return DivergeObject{}, errorAhead
   299  	}
   300  
   301  	// $(git rev-list --count feature..master) commits behind master
   302  	behind, errorBehind := checkDivergence(ctx, repoPath, targetBranch, baseBranch)
   303  	if errorBehind != nil {
   304  		return DivergeObject{}, errorBehind
   305  	}
   306  
   307  	return DivergeObject{ahead, behind}, nil
   308  }
   309  
   310  // CreateBundle create bundle content to the target path
   311  func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error {
   312  	tmp, err := os.MkdirTemp(os.TempDir(), "gitbundle-bundle")
   313  	if err != nil {
   314  		return err
   315  	}
   316  	defer os.RemoveAll(tmp)
   317  
   318  	env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects"))
   319  	_, _, err = NewCommand(ctx, "init", "--bare").RunStdString(&RunOpts{Dir: tmp, Env: env})
   320  	if err != nil {
   321  		return err
   322  	}
   323  
   324  	_, _, err = NewCommand(ctx, "reset", "--soft", commit).RunStdString(&RunOpts{Dir: tmp, Env: env})
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	_, _, err = NewCommand(ctx, "branch", "-m", "bundle").RunStdString(&RunOpts{Dir: tmp, Env: env})
   330  	if err != nil {
   331  		return err
   332  	}
   333  
   334  	tmpFile := filepath.Join(tmp, "bundle")
   335  	_, _, err = NewCommand(ctx, "bundle", "create", tmpFile, "bundle", "HEAD").RunStdString(&RunOpts{Dir: tmp, Env: env})
   336  	if err != nil {
   337  		return err
   338  	}
   339  
   340  	fi, err := os.Open(tmpFile)
   341  	if err != nil {
   342  		return err
   343  	}
   344  	defer fi.Close()
   345  
   346  	_, err = io.Copy(out, fi)
   347  	return err
   348  }