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