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 }