code.gitea.io/gitea@v1.22.3/modules/git/repo_commit.go (about) 1 // Copyright 2015 The Gogs Authors. All rights reserved. 2 // Copyright 2019 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package git 6 7 import ( 8 "bytes" 9 "io" 10 "os" 11 "strconv" 12 "strings" 13 14 "code.gitea.io/gitea/modules/cache" 15 "code.gitea.io/gitea/modules/setting" 16 ) 17 18 // GetBranchCommitID returns last commit ID string of given branch. 19 func (repo *Repository) GetBranchCommitID(name string) (string, error) { 20 return repo.GetRefCommitID(BranchPrefix + name) 21 } 22 23 // GetTagCommitID returns last commit ID string of given tag. 24 func (repo *Repository) GetTagCommitID(name string) (string, error) { 25 return repo.GetRefCommitID(TagPrefix + name) 26 } 27 28 // GetCommit returns commit object of by ID string. 29 func (repo *Repository) GetCommit(commitID string) (*Commit, error) { 30 id, err := repo.ConvertToGitID(commitID) 31 if err != nil { 32 return nil, err 33 } 34 35 return repo.getCommit(id) 36 } 37 38 // GetBranchCommit returns the last commit of given branch. 39 func (repo *Repository) GetBranchCommit(name string) (*Commit, error) { 40 commitID, err := repo.GetBranchCommitID(name) 41 if err != nil { 42 return nil, err 43 } 44 return repo.GetCommit(commitID) 45 } 46 47 // GetTagCommit get the commit of the specific tag via name 48 func (repo *Repository) GetTagCommit(name string) (*Commit, error) { 49 commitID, err := repo.GetTagCommitID(name) 50 if err != nil { 51 return nil, err 52 } 53 return repo.GetCommit(commitID) 54 } 55 56 func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Commit, error) { 57 // File name starts with ':' must be escaped. 58 if relpath[0] == ':' { 59 relpath = `\` + relpath 60 } 61 62 stdout, _, runErr := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat).AddDynamicArguments(id.String()).AddDashesAndList(relpath).RunStdString(&RunOpts{Dir: repo.Path}) 63 if runErr != nil { 64 return nil, runErr 65 } 66 67 id, err := NewIDFromString(stdout) 68 if err != nil { 69 return nil, err 70 } 71 72 return repo.getCommit(id) 73 } 74 75 // GetCommitByPath returns the last commit of relative path. 76 func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { 77 stdout, _, runErr := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat).AddDashesAndList(relpath).RunStdBytes(&RunOpts{Dir: repo.Path}) 78 if runErr != nil { 79 return nil, runErr 80 } 81 82 commits, err := repo.parsePrettyFormatLogToList(stdout) 83 if err != nil { 84 return nil, err 85 } 86 if len(commits) == 0 { 87 return nil, ErrNotExist{ID: relpath} 88 } 89 return commits[0], nil 90 } 91 92 func (repo *Repository) commitsByRange(id ObjectID, page, pageSize int, not string) ([]*Commit, error) { 93 cmd := NewCommand(repo.Ctx, "log"). 94 AddOptionFormat("--skip=%d", (page-1)*pageSize). 95 AddOptionFormat("--max-count=%d", pageSize). 96 AddArguments(prettyLogFormat). 97 AddDynamicArguments(id.String()) 98 99 if not != "" { 100 cmd.AddOptionValues("--not", not) 101 } 102 103 stdout, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) 104 if err != nil { 105 return nil, err 106 } 107 108 return repo.parsePrettyFormatLogToList(stdout) 109 } 110 111 func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([]*Commit, error) { 112 // add common arguments to git command 113 addCommonSearchArgs := func(c *Command) { 114 // ignore case 115 c.AddArguments("-i") 116 117 // add authors if present in search query 118 for _, v := range opts.Authors { 119 c.AddOptionFormat("--author=%s", v) 120 } 121 122 // add committers if present in search query 123 for _, v := range opts.Committers { 124 c.AddOptionFormat("--committer=%s", v) 125 } 126 127 // add time constraints if present in search query 128 if len(opts.After) > 0 { 129 c.AddOptionFormat("--after=%s", opts.After) 130 } 131 if len(opts.Before) > 0 { 132 c.AddOptionFormat("--before=%s", opts.Before) 133 } 134 } 135 136 // create new git log command with limit of 100 commits 137 cmd := NewCommand(repo.Ctx, "log", "-100", prettyLogFormat).AddDynamicArguments(id.String()) 138 139 // pretend that all refs along with HEAD were listed on command line as <commis> 140 // https://git-scm.com/docs/git-log#Documentation/git-log.txt---all 141 // note this is done only for command created above 142 if opts.All { 143 cmd.AddArguments("--all") 144 } 145 146 // interpret search string keywords as string instead of regex 147 cmd.AddArguments("--fixed-strings") 148 149 // add remaining keywords from search string 150 // note this is done only for command created above 151 for _, v := range opts.Keywords { 152 cmd.AddOptionFormat("--grep=%s", v) 153 } 154 155 // search for commits matching given constraints and keywords in commit msg 156 addCommonSearchArgs(cmd) 157 stdout, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) 158 if err != nil { 159 return nil, err 160 } 161 if len(stdout) != 0 { 162 stdout = append(stdout, '\n') 163 } 164 165 // if there are any keywords (ie not committer:, author:, time:) 166 // then let's iterate over them 167 for _, v := range opts.Keywords { 168 // ignore anything not matching a valid sha pattern 169 if id.Type().IsValid(v) { 170 // create new git log command with 1 commit limit 171 hashCmd := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat) 172 // add previous arguments except for --grep and --all 173 addCommonSearchArgs(hashCmd) 174 // add keyword as <commit> 175 hashCmd.AddDynamicArguments(v) 176 177 // search with given constraints for commit matching sha hash of v 178 hashMatching, _, err := hashCmd.RunStdBytes(&RunOpts{Dir: repo.Path}) 179 if err != nil || bytes.Contains(stdout, hashMatching) { 180 continue 181 } 182 stdout = append(stdout, hashMatching...) 183 stdout = append(stdout, '\n') 184 } 185 } 186 187 return repo.parsePrettyFormatLogToList(bytes.TrimSuffix(stdout, []byte{'\n'})) 188 } 189 190 // FileChangedBetweenCommits Returns true if the file changed between commit IDs id1 and id2 191 // You must ensure that id1 and id2 are valid commit ids. 192 func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bool, error) { 193 stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only", "-z").AddDynamicArguments(id1, id2).AddDashesAndList(filename).RunStdBytes(&RunOpts{Dir: repo.Path}) 194 if err != nil { 195 return false, err 196 } 197 return len(strings.TrimSpace(string(stdout))) > 0, nil 198 } 199 200 // FileCommitsCount return the number of files at a revision 201 func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) { 202 return CommitsCount(repo.Ctx, 203 CommitsCountOptions{ 204 RepoPath: repo.Path, 205 Revision: []string{revision}, 206 RelPath: []string{file}, 207 }) 208 } 209 210 type CommitsByFileAndRangeOptions struct { 211 Revision string 212 File string 213 Not string 214 Page int 215 } 216 217 // CommitsByFileAndRange return the commits according revision file and the page 218 func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) { 219 skip := (opts.Page - 1) * setting.Git.CommitsRangeSize 220 221 stdoutReader, stdoutWriter := io.Pipe() 222 defer func() { 223 _ = stdoutReader.Close() 224 _ = stdoutWriter.Close() 225 }() 226 go func() { 227 stderr := strings.Builder{} 228 gitCmd := NewCommand(repo.Ctx, "rev-list"). 229 AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize*opts.Page). 230 AddOptionFormat("--skip=%d", skip) 231 gitCmd.AddDynamicArguments(opts.Revision) 232 233 if opts.Not != "" { 234 gitCmd.AddOptionValues("--not", opts.Not) 235 } 236 237 gitCmd.AddDashesAndList(opts.File) 238 err := gitCmd.Run(&RunOpts{ 239 Dir: repo.Path, 240 Stdout: stdoutWriter, 241 Stderr: &stderr, 242 }) 243 if err != nil { 244 _ = stdoutWriter.CloseWithError(ConcatenateError(err, (&stderr).String())) 245 } else { 246 _ = stdoutWriter.Close() 247 } 248 }() 249 250 objectFormat, err := repo.GetObjectFormat() 251 if err != nil { 252 return nil, err 253 } 254 255 length := objectFormat.FullLength() 256 commits := []*Commit{} 257 shaline := make([]byte, length+1) 258 for { 259 n, err := io.ReadFull(stdoutReader, shaline) 260 if err != nil || n < length { 261 if err == io.EOF { 262 err = nil 263 } 264 return commits, err 265 } 266 objectID, err := NewIDFromString(string(shaline[0:length])) 267 if err != nil { 268 return nil, err 269 } 270 commit, err := repo.getCommit(objectID) 271 if err != nil { 272 return nil, err 273 } 274 commits = append(commits, commit) 275 } 276 } 277 278 // FilesCountBetween return the number of files changed between two commits 279 func (repo *Repository) FilesCountBetween(startCommitID, endCommitID string) (int, error) { 280 stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(startCommitID + "..." + endCommitID).RunStdString(&RunOpts{Dir: repo.Path}) 281 if err != nil && strings.Contains(err.Error(), "no merge base") { 282 // git >= 2.28 now returns an error if startCommitID and endCommitID have become unrelated. 283 // previously it would return the results of git diff --name-only startCommitID endCommitID so let's try that... 284 stdout, _, err = NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(startCommitID, endCommitID).RunStdString(&RunOpts{Dir: repo.Path}) 285 } 286 if err != nil { 287 return 0, err 288 } 289 return len(strings.Split(stdout, "\n")) - 1, nil 290 } 291 292 // CommitsBetween returns a list that contains commits between [before, last). 293 // If before is detached (removed by reset + push) it is not included. 294 func (repo *Repository) CommitsBetween(last, before *Commit) ([]*Commit, error) { 295 var stdout []byte 296 var err error 297 if before == nil { 298 stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) 299 } else { 300 stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String() + ".." + last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) 301 if err != nil && strings.Contains(err.Error(), "no merge base") { 302 // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. 303 // previously it would return the results of git rev-list before last so let's try that... 304 stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) 305 } 306 } 307 if err != nil { 308 return nil, err 309 } 310 return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) 311 } 312 313 // CommitsBetweenLimit returns a list that contains at most limit commits skipping the first skip commits between [before, last) 314 func (repo *Repository) CommitsBetweenLimit(last, before *Commit, limit, skip int) ([]*Commit, error) { 315 var stdout []byte 316 var err error 317 if before == nil { 318 stdout, _, err = NewCommand(repo.Ctx, "rev-list"). 319 AddOptionValues("--max-count", strconv.Itoa(limit)). 320 AddOptionValues("--skip", strconv.Itoa(skip)). 321 AddDynamicArguments(last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) 322 } else { 323 stdout, _, err = NewCommand(repo.Ctx, "rev-list"). 324 AddOptionValues("--max-count", strconv.Itoa(limit)). 325 AddOptionValues("--skip", strconv.Itoa(skip)). 326 AddDynamicArguments(before.ID.String() + ".." + last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) 327 if err != nil && strings.Contains(err.Error(), "no merge base") { 328 // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. 329 // previously it would return the results of git rev-list --max-count n before last so let's try that... 330 stdout, _, err = NewCommand(repo.Ctx, "rev-list"). 331 AddOptionValues("--max-count", strconv.Itoa(limit)). 332 AddOptionValues("--skip", strconv.Itoa(skip)). 333 AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) 334 } 335 } 336 if err != nil { 337 return nil, err 338 } 339 return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) 340 } 341 342 // CommitsBetweenNotBase returns a list that contains commits between [before, last), excluding commits in baseBranch. 343 // If before is detached (removed by reset + push) it is not included. 344 func (repo *Repository) CommitsBetweenNotBase(last, before *Commit, baseBranch string) ([]*Commit, error) { 345 var stdout []byte 346 var err error 347 if before == nil { 348 stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path}) 349 } else { 350 stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String()+".."+last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path}) 351 if err != nil && strings.Contains(err.Error(), "no merge base") { 352 // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. 353 // previously it would return the results of git rev-list before last so let's try that... 354 stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path}) 355 } 356 } 357 if err != nil { 358 return nil, err 359 } 360 return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) 361 } 362 363 // CommitsBetweenIDs return commits between twoe commits 364 func (repo *Repository) CommitsBetweenIDs(last, before string) ([]*Commit, error) { 365 lastCommit, err := repo.GetCommit(last) 366 if err != nil { 367 return nil, err 368 } 369 if before == "" { 370 return repo.CommitsBetween(lastCommit, nil) 371 } 372 beforeCommit, err := repo.GetCommit(before) 373 if err != nil { 374 return nil, err 375 } 376 return repo.CommitsBetween(lastCommit, beforeCommit) 377 } 378 379 // CommitsCountBetween return numbers of commits between two commits 380 func (repo *Repository) CommitsCountBetween(start, end string) (int64, error) { 381 count, err := CommitsCount(repo.Ctx, CommitsCountOptions{ 382 RepoPath: repo.Path, 383 Revision: []string{start + ".." + end}, 384 }) 385 386 if err != nil && strings.Contains(err.Error(), "no merge base") { 387 // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. 388 // previously it would return the results of git rev-list before last so let's try that... 389 return CommitsCount(repo.Ctx, CommitsCountOptions{ 390 RepoPath: repo.Path, 391 Revision: []string{start, end}, 392 }) 393 } 394 395 return count, err 396 } 397 398 // commitsBefore the limit is depth, not total number of returned commits. 399 func (repo *Repository) commitsBefore(id ObjectID, limit int) ([]*Commit, error) { 400 cmd := NewCommand(repo.Ctx, "log", prettyLogFormat) 401 if limit > 0 { 402 cmd.AddOptionFormat("-%d", limit) 403 } 404 cmd.AddDynamicArguments(id.String()) 405 406 stdout, _, runErr := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) 407 if runErr != nil { 408 return nil, runErr 409 } 410 411 formattedLog, err := repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) 412 if err != nil { 413 return nil, err 414 } 415 416 commits := make([]*Commit, 0, len(formattedLog)) 417 for _, commit := range formattedLog { 418 branches, err := repo.getBranches(os.Environ(), commit.ID.String(), 2) 419 if err != nil { 420 return nil, err 421 } 422 423 if len(branches) > 1 { 424 break 425 } 426 427 commits = append(commits, commit) 428 } 429 430 return commits, nil 431 } 432 433 func (repo *Repository) getCommitsBefore(id ObjectID) ([]*Commit, error) { 434 return repo.commitsBefore(id, 0) 435 } 436 437 func (repo *Repository) getCommitsBeforeLimit(id ObjectID, num int) ([]*Commit, error) { 438 return repo.commitsBefore(id, num) 439 } 440 441 func (repo *Repository) getBranches(env []string, commitID string, limit int) ([]string, error) { 442 if DefaultFeatures().CheckVersionAtLeast("2.7.0") { 443 stdout, _, err := NewCommand(repo.Ctx, "for-each-ref", "--format=%(refname:strip=2)"). 444 AddOptionFormat("--count=%d", limit). 445 AddOptionValues("--contains", commitID, BranchPrefix). 446 RunStdString(&RunOpts{ 447 Dir: repo.Path, 448 Env: env, 449 }) 450 if err != nil { 451 return nil, err 452 } 453 454 branches := strings.Fields(stdout) 455 return branches, nil 456 } 457 458 stdout, _, err := NewCommand(repo.Ctx, "branch").AddOptionValues("--contains", commitID).RunStdString(&RunOpts{ 459 Dir: repo.Path, 460 Env: env, 461 }) 462 if err != nil { 463 return nil, err 464 } 465 466 refs := strings.Split(stdout, "\n") 467 468 var max int 469 if len(refs) > limit { 470 max = limit 471 } else { 472 max = len(refs) - 1 473 } 474 475 branches := make([]string, max) 476 for i, ref := range refs[:max] { 477 parts := strings.Fields(ref) 478 479 branches[i] = parts[len(parts)-1] 480 } 481 return branches, nil 482 } 483 484 // GetCommitsFromIDs get commits from commit IDs 485 func (repo *Repository) GetCommitsFromIDs(commitIDs []string) []*Commit { 486 commits := make([]*Commit, 0, len(commitIDs)) 487 488 for _, commitID := range commitIDs { 489 commit, err := repo.GetCommit(commitID) 490 if err == nil && commit != nil { 491 commits = append(commits, commit) 492 } 493 } 494 495 return commits 496 } 497 498 // IsCommitInBranch check if the commit is on the branch 499 func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err error) { 500 stdout, _, err := NewCommand(repo.Ctx, "branch", "--contains").AddDynamicArguments(commitID, branch).RunStdString(&RunOpts{Dir: repo.Path}) 501 if err != nil { 502 return false, err 503 } 504 return len(stdout) > 0, err 505 } 506 507 func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error { 508 if repo.LastCommitCache == nil { 509 commitsCount, err := cache.GetInt64(cacheKey, func() (int64, error) { 510 commit, err := repo.GetCommit(sha) 511 if err != nil { 512 return 0, err 513 } 514 return commit.CommitsCount() 515 }) 516 if err != nil { 517 return err 518 } 519 repo.LastCommitCache = NewLastCommitCache(commitsCount, fullName, repo, cache.GetCache()) 520 } 521 return nil 522 } 523 524 func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) { 525 cmd := NewCommand(repo.Ctx, "log", prettyLogFormat) 526 cmd.AddDynamicArguments(endCommitID) 527 528 stdout, _, runErr := cmd.RunStdBytes(&RunOpts{ 529 Dir: repo.Path, 530 Env: env, 531 }) 532 if runErr != nil { 533 return "", runErr 534 } 535 536 parts := bytes.Split(bytes.TrimSpace(stdout), []byte{'\n'}) 537 538 var startCommitID string 539 for _, commitID := range parts { 540 branches, err := repo.getBranches(env, string(commitID), 2) 541 if err != nil { 542 return "", err 543 } 544 for _, b := range branches { 545 if b != branch { 546 return startCommitID, nil 547 } 548 } 549 550 startCommitID = string(commitID) 551 } 552 553 return "", nil 554 }