1// Copyright 2015 The Gogs Authors. 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 6package git 7 8import ( 9 "bytes" 10 "context" 11 "fmt" 12 "io" 13 "net/url" 14 "os" 15 "path" 16 "path/filepath" 17 "strconv" 18 "strings" 19 "time" 20 21 "code.gitea.io/gitea/modules/proxy" 22) 23 24// GPGSettings represents the default GPG settings for this repository 25type GPGSettings struct { 26 Sign bool 27 KeyID string 28 Email string 29 Name string 30 PublicKeyContent string 31} 32 33const prettyLogFormat = `--pretty=format:%H` 34 35// GetAllCommitsCount returns count of all commits in repository 36func (repo *Repository) GetAllCommitsCount() (int64, error) { 37 return AllCommitsCount(repo.Path, false) 38} 39 40func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, error) { 41 var commits []*Commit 42 if len(logs) == 0 { 43 return commits, nil 44 } 45 46 parts := bytes.Split(logs, []byte{'\n'}) 47 48 for _, commitID := range parts { 49 commit, err := repo.GetCommit(string(commitID)) 50 if err != nil { 51 return nil, err 52 } 53 commits = append(commits, commit) 54 } 55 56 return commits, nil 57} 58 59// IsRepoURLAccessible checks if given repository URL is accessible. 60func IsRepoURLAccessible(url string) bool { 61 _, err := NewCommand("ls-remote", "-q", "-h", url, "HEAD").Run() 62 return err == nil 63} 64 65// InitRepository initializes a new Git repository. 66func InitRepository(repoPath string, bare bool) error { 67 err := os.MkdirAll(repoPath, os.ModePerm) 68 if err != nil { 69 return err 70 } 71 72 cmd := NewCommand("init") 73 if bare { 74 cmd.AddArguments("--bare") 75 } 76 _, err = cmd.RunInDir(repoPath) 77 return err 78} 79 80// IsEmpty Check if repository is empty. 81func (repo *Repository) IsEmpty() (bool, error) { 82 var errbuf, output strings.Builder 83 if err := NewCommandContext(repo.Ctx, "show-ref", "--head", "^HEAD$").RunWithContext(&RunContext{ 84 Timeout: -1, 85 Dir: repo.Path, 86 Stdout: &output, 87 Stderr: &errbuf, 88 }); err != nil { 89 if err.Error() == "exit status 1" && errbuf.String() == "" { 90 return true, nil 91 } 92 return true, fmt.Errorf("check empty: %v - %s", err, errbuf.String()) 93 } 94 95 return strings.TrimSpace(output.String()) == "", nil 96} 97 98// CloneRepoOptions options when clone a repository 99type CloneRepoOptions struct { 100 Timeout time.Duration 101 Mirror bool 102 Bare bool 103 Quiet bool 104 Branch string 105 Shared bool 106 NoCheckout bool 107 Depth int 108 Filter string 109 SkipTLSVerify bool 110} 111 112// Clone clones original repository to target path. 113func Clone(from, to string, opts CloneRepoOptions) error { 114 return CloneWithContext(DefaultContext, from, to, opts) 115} 116 117// CloneWithContext clones original repository to target path. 118func CloneWithContext(ctx context.Context, from, to string, opts CloneRepoOptions) error { 119 cargs := make([]string, len(GlobalCommandArgs)) 120 copy(cargs, GlobalCommandArgs) 121 return CloneWithArgs(ctx, from, to, cargs, opts) 122} 123 124// CloneWithArgs original repository to target path. 125func CloneWithArgs(ctx context.Context, from, to string, args []string, opts CloneRepoOptions) (err error) { 126 toDir := path.Dir(to) 127 if err = os.MkdirAll(toDir, os.ModePerm); err != nil { 128 return err 129 } 130 131 cmd := NewCommandContextNoGlobals(ctx, args...).AddArguments("clone") 132 if opts.SkipTLSVerify { 133 cmd.AddArguments("-c", "http.sslVerify=false") 134 } 135 if opts.Mirror { 136 cmd.AddArguments("--mirror") 137 } 138 if opts.Bare { 139 cmd.AddArguments("--bare") 140 } 141 if opts.Quiet { 142 cmd.AddArguments("--quiet") 143 } 144 if opts.Shared { 145 cmd.AddArguments("-s") 146 } 147 if opts.NoCheckout { 148 cmd.AddArguments("--no-checkout") 149 } 150 if opts.Depth > 0 { 151 cmd.AddArguments("--depth", strconv.Itoa(opts.Depth)) 152 } 153 if opts.Filter != "" { 154 cmd.AddArguments("--filter", opts.Filter) 155 } 156 if len(opts.Branch) > 0 { 157 cmd.AddArguments("-b", opts.Branch) 158 } 159 cmd.AddArguments("--", from, to) 160 161 if opts.Timeout <= 0 { 162 opts.Timeout = -1 163 } 164 165 var envs = os.Environ() 166 u, err := url.Parse(from) 167 if err == nil && (strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https")) { 168 if proxy.Match(u.Host) { 169 envs = append(envs, fmt.Sprintf("https_proxy=%s", proxy.GetProxyURL())) 170 } 171 } 172 173 var stderr = new(bytes.Buffer) 174 if err = cmd.RunWithContext(&RunContext{ 175 Timeout: opts.Timeout, 176 Env: envs, 177 Stdout: io.Discard, 178 Stderr: stderr, 179 }); err != nil { 180 return ConcatenateError(err, stderr.String()) 181 } 182 return nil 183} 184 185// PullRemoteOptions options when pull from remote 186type PullRemoteOptions struct { 187 Timeout time.Duration 188 All bool 189 Rebase bool 190 Remote string 191 Branch string 192} 193 194// Pull pulls changes from remotes. 195func Pull(repoPath string, opts PullRemoteOptions) error { 196 cmd := NewCommand("pull") 197 if opts.Rebase { 198 cmd.AddArguments("--rebase") 199 } 200 if opts.All { 201 cmd.AddArguments("--all") 202 } else { 203 cmd.AddArguments("--", opts.Remote, opts.Branch) 204 } 205 206 if opts.Timeout <= 0 { 207 opts.Timeout = -1 208 } 209 210 _, err := cmd.RunInDirTimeout(opts.Timeout, repoPath) 211 return err 212} 213 214// PushOptions options when push to remote 215type PushOptions struct { 216 Remote string 217 Branch string 218 Force bool 219 Mirror bool 220 Env []string 221 Timeout time.Duration 222} 223 224// Push pushs local commits to given remote branch. 225func Push(ctx context.Context, repoPath string, opts PushOptions) error { 226 cmd := NewCommandContext(ctx, "push") 227 if opts.Force { 228 cmd.AddArguments("-f") 229 } 230 if opts.Mirror { 231 cmd.AddArguments("--mirror") 232 } 233 cmd.AddArguments("--", opts.Remote) 234 if len(opts.Branch) > 0 { 235 cmd.AddArguments(opts.Branch) 236 } 237 var outbuf, errbuf strings.Builder 238 239 if opts.Timeout == 0 { 240 opts.Timeout = -1 241 } 242 243 err := cmd.RunInDirTimeoutEnvPipeline(opts.Env, opts.Timeout, repoPath, &outbuf, &errbuf) 244 if err != nil { 245 if strings.Contains(errbuf.String(), "non-fast-forward") { 246 return &ErrPushOutOfDate{ 247 StdOut: outbuf.String(), 248 StdErr: errbuf.String(), 249 Err: err, 250 } 251 } else if strings.Contains(errbuf.String(), "! [remote rejected]") { 252 err := &ErrPushRejected{ 253 StdOut: outbuf.String(), 254 StdErr: errbuf.String(), 255 Err: err, 256 } 257 err.GenerateMessage() 258 return err 259 } else if strings.Contains(errbuf.String(), "matches more than one") { 260 err := &ErrMoreThanOne{ 261 StdOut: outbuf.String(), 262 StdErr: errbuf.String(), 263 Err: err, 264 } 265 return err 266 } 267 } 268 269 if errbuf.Len() > 0 && err != nil { 270 return fmt.Errorf("%v - %s", err, errbuf.String()) 271 } 272 273 return err 274} 275 276// CheckoutOptions options when heck out some branch 277type CheckoutOptions struct { 278 Timeout time.Duration 279 Branch string 280 OldBranch string 281} 282 283// Checkout checkouts a branch 284func Checkout(repoPath string, opts CheckoutOptions) error { 285 cmd := NewCommand("checkout") 286 if len(opts.OldBranch) > 0 { 287 cmd.AddArguments("-b") 288 } 289 290 if opts.Timeout <= 0 { 291 opts.Timeout = -1 292 } 293 294 cmd.AddArguments(opts.Branch) 295 296 if len(opts.OldBranch) > 0 { 297 cmd.AddArguments(opts.OldBranch) 298 } 299 300 _, err := cmd.RunInDirTimeout(opts.Timeout, repoPath) 301 return err 302} 303 304// ResetHEAD resets HEAD to given revision or head of branch. 305func ResetHEAD(repoPath string, hard bool, revision string) error { 306 cmd := NewCommand("reset") 307 if hard { 308 cmd.AddArguments("--hard") 309 } 310 _, err := cmd.AddArguments(revision).RunInDir(repoPath) 311 return err 312} 313 314// MoveFile moves a file to another file or directory. 315func MoveFile(repoPath, oldTreeName, newTreeName string) error { 316 _, err := NewCommand("mv").AddArguments(oldTreeName, newTreeName).RunInDir(repoPath) 317 return err 318} 319 320// CountObject represents repository count objects report 321type CountObject struct { 322 Count int64 323 Size int64 324 InPack int64 325 Packs int64 326 SizePack int64 327 PrunePack int64 328 Garbage int64 329 SizeGarbage int64 330} 331 332const ( 333 statCount = "count: " 334 statSize = "size: " 335 statInpack = "in-pack: " 336 statPacks = "packs: " 337 statSizePack = "size-pack: " 338 statPrunePackage = "prune-package: " 339 statGarbage = "garbage: " 340 statSizeGarbage = "size-garbage: " 341) 342 343// CountObjects returns the results of git count-objects on the repoPath 344func CountObjects(repoPath string) (*CountObject, error) { 345 cmd := NewCommand("count-objects", "-v") 346 stdout, err := cmd.RunInDir(repoPath) 347 if err != nil { 348 return nil, err 349 } 350 351 return parseSize(stdout), nil 352} 353 354// parseSize parses the output from count-objects and return a CountObject 355func parseSize(objects string) *CountObject { 356 repoSize := new(CountObject) 357 for _, line := range strings.Split(objects, "\n") { 358 switch { 359 case strings.HasPrefix(line, statCount): 360 repoSize.Count, _ = strconv.ParseInt(line[7:], 10, 64) 361 case strings.HasPrefix(line, statSize): 362 repoSize.Size, _ = strconv.ParseInt(line[6:], 10, 64) 363 repoSize.Size *= 1024 364 case strings.HasPrefix(line, statInpack): 365 repoSize.InPack, _ = strconv.ParseInt(line[9:], 10, 64) 366 case strings.HasPrefix(line, statPacks): 367 repoSize.Packs, _ = strconv.ParseInt(line[7:], 10, 64) 368 case strings.HasPrefix(line, statSizePack): 369 repoSize.Count, _ = strconv.ParseInt(line[11:], 10, 64) 370 repoSize.Count *= 1024 371 case strings.HasPrefix(line, statPrunePackage): 372 repoSize.PrunePack, _ = strconv.ParseInt(line[16:], 10, 64) 373 case strings.HasPrefix(line, statGarbage): 374 repoSize.Garbage, _ = strconv.ParseInt(line[9:], 10, 64) 375 case strings.HasPrefix(line, statSizeGarbage): 376 repoSize.SizeGarbage, _ = strconv.ParseInt(line[14:], 10, 64) 377 repoSize.SizeGarbage *= 1024 378 } 379 } 380 return repoSize 381} 382 383// GetLatestCommitTime returns time for latest commit in repository (across all branches) 384func GetLatestCommitTime(repoPath string) (time.Time, error) { 385 cmd := NewCommand("for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)") 386 stdout, err := cmd.RunInDir(repoPath) 387 if err != nil { 388 return time.Time{}, err 389 } 390 commitTime := strings.TrimSpace(stdout) 391 return time.Parse(GitTimeLayout, commitTime) 392} 393 394// DivergeObject represents commit count diverging commits 395type DivergeObject struct { 396 Ahead int 397 Behind int 398} 399 400func checkDivergence(repoPath, baseBranch, targetBranch string) (int, error) { 401 branches := fmt.Sprintf("%s..%s", baseBranch, targetBranch) 402 cmd := NewCommand("rev-list", "--count", branches) 403 stdout, err := cmd.RunInDir(repoPath) 404 if err != nil { 405 return -1, err 406 } 407 outInteger, errInteger := strconv.Atoi(strings.Trim(stdout, "\n")) 408 if errInteger != nil { 409 return -1, errInteger 410 } 411 return outInteger, nil 412} 413 414// GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch 415func GetDivergingCommits(repoPath, baseBranch, targetBranch string) (DivergeObject, error) { 416 // $(git rev-list --count master..feature) commits ahead of master 417 ahead, errorAhead := checkDivergence(repoPath, baseBranch, targetBranch) 418 if errorAhead != nil { 419 return DivergeObject{}, errorAhead 420 } 421 422 // $(git rev-list --count feature..master) commits behind master 423 behind, errorBehind := checkDivergence(repoPath, targetBranch, baseBranch) 424 if errorBehind != nil { 425 return DivergeObject{}, errorBehind 426 } 427 428 return DivergeObject{ahead, behind}, nil 429} 430 431// CreateBundle create bundle content to the target path 432func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error { 433 tmp, err := os.MkdirTemp(os.TempDir(), "gitea-bundle") 434 if err != nil { 435 return err 436 } 437 defer os.RemoveAll(tmp) 438 439 env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects")) 440 _, err = NewCommandContext(ctx, "init", "--bare").RunInDirWithEnv(tmp, env) 441 if err != nil { 442 return err 443 } 444 445 _, err = NewCommandContext(ctx, "reset", "--soft", commit).RunInDirWithEnv(tmp, env) 446 if err != nil { 447 return err 448 } 449 450 _, err = NewCommandContext(ctx, "branch", "-m", "bundle").RunInDirWithEnv(tmp, env) 451 if err != nil { 452 return err 453 } 454 455 tmpFile := filepath.Join(tmp, "bundle") 456 _, err = NewCommandContext(ctx, "bundle", "create", tmpFile, "bundle", "HEAD").RunInDirWithEnv(tmp, env) 457 if err != nil { 458 return err 459 } 460 461 fi, err := os.Open(tmpFile) 462 if err != nil { 463 return err 464 } 465 defer fi.Close() 466 467 _, err = io.Copy(out, fi) 468 return err 469} 470