code.gitea.io/gitea@v1.22.3/modules/git/commit.go (about) 1 // Copyright 2015 The Gogs Authors. All rights reserved. 2 // Copyright 2018 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package git 6 7 import ( 8 "bufio" 9 "bytes" 10 "context" 11 "errors" 12 "fmt" 13 "io" 14 "os/exec" 15 "strconv" 16 "strings" 17 18 "code.gitea.io/gitea/modules/log" 19 "code.gitea.io/gitea/modules/util" 20 ) 21 22 // Commit represents a git commit. 23 type Commit struct { 24 Tree 25 ID ObjectID // The ID of this commit object 26 Author *Signature 27 Committer *Signature 28 CommitMessage string 29 Signature *CommitSignature 30 31 Parents []ObjectID // ID strings 32 submoduleCache *ObjectCache 33 } 34 35 // CommitSignature represents a git commit signature part. 36 type CommitSignature struct { 37 Signature string 38 Payload string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data 39 } 40 41 // Message returns the commit message. Same as retrieving CommitMessage directly. 42 func (c *Commit) Message() string { 43 return c.CommitMessage 44 } 45 46 // Summary returns first line of commit message. 47 // The string is forced to be valid UTF8 48 func (c *Commit) Summary() string { 49 return strings.ToValidUTF8(strings.Split(strings.TrimSpace(c.CommitMessage), "\n")[0], "?") 50 } 51 52 // ParentID returns oid of n-th parent (0-based index). 53 // It returns nil if no such parent exists. 54 func (c *Commit) ParentID(n int) (ObjectID, error) { 55 if n >= len(c.Parents) { 56 return nil, ErrNotExist{"", ""} 57 } 58 return c.Parents[n], nil 59 } 60 61 // Parent returns n-th parent (0-based index) of the commit. 62 func (c *Commit) Parent(n int) (*Commit, error) { 63 id, err := c.ParentID(n) 64 if err != nil { 65 return nil, err 66 } 67 parent, err := c.repo.getCommit(id) 68 if err != nil { 69 return nil, err 70 } 71 return parent, nil 72 } 73 74 // ParentCount returns number of parents of the commit. 75 // 0 if this is the root commit, otherwise 1,2, etc. 76 func (c *Commit) ParentCount() int { 77 return len(c.Parents) 78 } 79 80 // GetCommitByPath return the commit of relative path object. 81 func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) { 82 if c.repo.LastCommitCache != nil { 83 return c.repo.LastCommitCache.GetCommitByPath(c.ID.String(), relpath) 84 } 85 return c.repo.getCommitByPathWithID(c.ID, relpath) 86 } 87 88 // AddChanges marks local changes to be ready for commit. 89 func AddChanges(repoPath string, all bool, files ...string) error { 90 return AddChangesWithArgs(repoPath, globalCommandArgs, all, files...) 91 } 92 93 // AddChangesWithArgs marks local changes to be ready for commit. 94 func AddChangesWithArgs(repoPath string, globalArgs TrustedCmdArgs, all bool, files ...string) error { 95 cmd := NewCommandContextNoGlobals(DefaultContext, globalArgs...).AddArguments("add") 96 if all { 97 cmd.AddArguments("--all") 98 } 99 cmd.AddDashesAndList(files...) 100 _, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) 101 return err 102 } 103 104 // CommitChangesOptions the options when a commit created 105 type CommitChangesOptions struct { 106 Committer *Signature 107 Author *Signature 108 Message string 109 } 110 111 // CommitChanges commits local changes with given committer, author and message. 112 // If author is nil, it will be the same as committer. 113 func CommitChanges(repoPath string, opts CommitChangesOptions) error { 114 cargs := make(TrustedCmdArgs, len(globalCommandArgs)) 115 copy(cargs, globalCommandArgs) 116 return CommitChangesWithArgs(repoPath, cargs, opts) 117 } 118 119 // CommitChangesWithArgs commits local changes with given committer, author and message. 120 // If author is nil, it will be the same as committer. 121 func CommitChangesWithArgs(repoPath string, args TrustedCmdArgs, opts CommitChangesOptions) error { 122 cmd := NewCommandContextNoGlobals(DefaultContext, args...) 123 if opts.Committer != nil { 124 cmd.AddOptionValues("-c", "user.name="+opts.Committer.Name) 125 cmd.AddOptionValues("-c", "user.email="+opts.Committer.Email) 126 } 127 cmd.AddArguments("commit") 128 129 if opts.Author == nil { 130 opts.Author = opts.Committer 131 } 132 if opts.Author != nil { 133 cmd.AddOptionFormat("--author='%s <%s>'", opts.Author.Name, opts.Author.Email) 134 } 135 cmd.AddOptionFormat("--message=%s", opts.Message) 136 137 _, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) 138 // No stderr but exit status 1 means nothing to commit. 139 if err != nil && err.Error() == "exit status 1" { 140 return nil 141 } 142 return err 143 } 144 145 // AllCommitsCount returns count of all commits in repository 146 func AllCommitsCount(ctx context.Context, repoPath string, hidePRRefs bool, files ...string) (int64, error) { 147 cmd := NewCommand(ctx, "rev-list") 148 if hidePRRefs { 149 cmd.AddArguments("--exclude=" + PullPrefix + "*") 150 } 151 cmd.AddArguments("--all", "--count") 152 if len(files) > 0 { 153 cmd.AddDashesAndList(files...) 154 } 155 156 stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) 157 if err != nil { 158 return 0, err 159 } 160 161 return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) 162 } 163 164 // CommitsCountOptions the options when counting commits 165 type CommitsCountOptions struct { 166 RepoPath string 167 Not string 168 Revision []string 169 RelPath []string 170 } 171 172 // CommitsCount returns number of total commits of until given revision. 173 func CommitsCount(ctx context.Context, opts CommitsCountOptions) (int64, error) { 174 cmd := NewCommand(ctx, "rev-list", "--count") 175 176 cmd.AddDynamicArguments(opts.Revision...) 177 178 if opts.Not != "" { 179 cmd.AddOptionValues("--not", opts.Not) 180 } 181 182 if len(opts.RelPath) > 0 { 183 cmd.AddDashesAndList(opts.RelPath...) 184 } 185 186 stdout, _, err := cmd.RunStdString(&RunOpts{Dir: opts.RepoPath}) 187 if err != nil { 188 return 0, err 189 } 190 191 return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) 192 } 193 194 // CommitsCount returns number of total commits of until current revision. 195 func (c *Commit) CommitsCount() (int64, error) { 196 return CommitsCount(c.repo.Ctx, CommitsCountOptions{ 197 RepoPath: c.repo.Path, 198 Revision: []string{c.ID.String()}, 199 }) 200 } 201 202 // CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize 203 func (c *Commit) CommitsByRange(page, pageSize int, not string) ([]*Commit, error) { 204 return c.repo.commitsByRange(c.ID, page, pageSize, not) 205 } 206 207 // CommitsBefore returns all the commits before current revision 208 func (c *Commit) CommitsBefore() ([]*Commit, error) { 209 return c.repo.getCommitsBefore(c.ID) 210 } 211 212 // HasPreviousCommit returns true if a given commitHash is contained in commit's parents 213 func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) { 214 this := c.ID.String() 215 that := objectID.String() 216 217 if this == that { 218 return false, nil 219 } 220 221 _, _, err := NewCommand(c.repo.Ctx, "merge-base", "--is-ancestor").AddDynamicArguments(that, this).RunStdString(&RunOpts{Dir: c.repo.Path}) 222 if err == nil { 223 return true, nil 224 } 225 var exitError *exec.ExitError 226 if errors.As(err, &exitError) { 227 if exitError.ProcessState.ExitCode() == 1 && len(exitError.Stderr) == 0 { 228 return false, nil 229 } 230 } 231 return false, err 232 } 233 234 // IsForcePush returns true if a push from oldCommitHash to this is a force push 235 func (c *Commit) IsForcePush(oldCommitID string) (bool, error) { 236 objectFormat, err := c.repo.GetObjectFormat() 237 if err != nil { 238 return false, err 239 } 240 if oldCommitID == objectFormat.EmptyObjectID().String() { 241 return false, nil 242 } 243 244 oldCommit, err := c.repo.GetCommit(oldCommitID) 245 if err != nil { 246 return false, err 247 } 248 hasPreviousCommit, err := c.HasPreviousCommit(oldCommit.ID) 249 return !hasPreviousCommit, err 250 } 251 252 // CommitsBeforeLimit returns num commits before current revision 253 func (c *Commit) CommitsBeforeLimit(num int) ([]*Commit, error) { 254 return c.repo.getCommitsBeforeLimit(c.ID, num) 255 } 256 257 // CommitsBeforeUntil returns the commits between commitID to current revision 258 func (c *Commit) CommitsBeforeUntil(commitID string) ([]*Commit, error) { 259 endCommit, err := c.repo.GetCommit(commitID) 260 if err != nil { 261 return nil, err 262 } 263 return c.repo.CommitsBetween(c, endCommit) 264 } 265 266 // SearchCommitsOptions specify the parameters for SearchCommits 267 type SearchCommitsOptions struct { 268 Keywords []string 269 Authors, Committers []string 270 After, Before string 271 All bool 272 } 273 274 // NewSearchCommitsOptions construct a SearchCommitsOption from a space-delimited search string 275 func NewSearchCommitsOptions(searchString string, forAllRefs bool) SearchCommitsOptions { 276 var keywords, authors, committers []string 277 var after, before string 278 279 fields := strings.Fields(searchString) 280 for _, k := range fields { 281 switch { 282 case strings.HasPrefix(k, "author:"): 283 authors = append(authors, strings.TrimPrefix(k, "author:")) 284 case strings.HasPrefix(k, "committer:"): 285 committers = append(committers, strings.TrimPrefix(k, "committer:")) 286 case strings.HasPrefix(k, "after:"): 287 after = strings.TrimPrefix(k, "after:") 288 case strings.HasPrefix(k, "before:"): 289 before = strings.TrimPrefix(k, "before:") 290 default: 291 keywords = append(keywords, k) 292 } 293 } 294 295 return SearchCommitsOptions{ 296 Keywords: keywords, 297 Authors: authors, 298 Committers: committers, 299 After: after, 300 Before: before, 301 All: forAllRefs, 302 } 303 } 304 305 // SearchCommits returns the commits match the keyword before current revision 306 func (c *Commit) SearchCommits(opts SearchCommitsOptions) ([]*Commit, error) { 307 return c.repo.searchCommits(c.ID, opts) 308 } 309 310 // GetFilesChangedSinceCommit get all changed file names between pastCommit to current revision 311 func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error) { 312 return c.repo.GetFilesChangedBetween(pastCommit, c.ID.String()) 313 } 314 315 // FileChangedSinceCommit Returns true if the file given has changed since the past commit 316 // YOU MUST ENSURE THAT pastCommit is a valid commit ID. 317 func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) { 318 return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String()) 319 } 320 321 // HasFile returns true if the file given exists on this commit 322 // This does only mean it's there - it does not mean the file was changed during the commit. 323 func (c *Commit) HasFile(filename string) (bool, error) { 324 _, err := c.GetBlobByPath(filename) 325 if err != nil { 326 return false, err 327 } 328 return true, nil 329 } 330 331 // GetFileContent reads a file content as a string or returns false if this was not possible 332 func (c *Commit) GetFileContent(filename string, limit int) (string, error) { 333 entry, err := c.GetTreeEntryByPath(filename) 334 if err != nil { 335 return "", err 336 } 337 338 r, err := entry.Blob().DataAsync() 339 if err != nil { 340 return "", err 341 } 342 defer r.Close() 343 344 if limit > 0 { 345 bs := make([]byte, limit) 346 n, err := util.ReadAtMost(r, bs) 347 if err != nil { 348 return "", err 349 } 350 return string(bs[:n]), nil 351 } 352 353 bytes, err := io.ReadAll(r) 354 if err != nil { 355 return "", err 356 } 357 return string(bytes), nil 358 } 359 360 // GetSubModules get all the sub modules of current revision git tree 361 func (c *Commit) GetSubModules() (*ObjectCache, error) { 362 if c.submoduleCache != nil { 363 return c.submoduleCache, nil 364 } 365 366 entry, err := c.GetTreeEntryByPath(".gitmodules") 367 if err != nil { 368 if _, ok := err.(ErrNotExist); ok { 369 return nil, nil 370 } 371 return nil, err 372 } 373 374 rd, err := entry.Blob().DataAsync() 375 if err != nil { 376 return nil, err 377 } 378 379 defer rd.Close() 380 scanner := bufio.NewScanner(rd) 381 c.submoduleCache = newObjectCache() 382 var ismodule bool 383 var path string 384 for scanner.Scan() { 385 if strings.HasPrefix(scanner.Text(), "[submodule") { 386 ismodule = true 387 continue 388 } 389 if ismodule { 390 fields := strings.Split(scanner.Text(), "=") 391 k := strings.TrimSpace(fields[0]) 392 if k == "path" { 393 path = strings.TrimSpace(fields[1]) 394 } else if k == "url" { 395 c.submoduleCache.Set(path, &SubModule{path, strings.TrimSpace(fields[1])}) 396 ismodule = false 397 } 398 } 399 } 400 if err = scanner.Err(); err != nil { 401 return nil, fmt.Errorf("GetSubModules scan: %w", err) 402 } 403 404 return c.submoduleCache, nil 405 } 406 407 // GetSubModule get the sub module according entryname 408 func (c *Commit) GetSubModule(entryname string) (*SubModule, error) { 409 modules, err := c.GetSubModules() 410 if err != nil { 411 return nil, err 412 } 413 414 if modules != nil { 415 module, has := modules.Get(entryname) 416 if has { 417 return module.(*SubModule), nil 418 } 419 } 420 return nil, nil 421 } 422 423 // GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') 424 func (c *Commit) GetBranchName() (string, error) { 425 cmd := NewCommand(c.repo.Ctx, "name-rev") 426 if DefaultFeatures().CheckVersionAtLeast("2.13.0") { 427 cmd.AddArguments("--exclude", "refs/tags/*") 428 } 429 cmd.AddArguments("--name-only", "--no-undefined").AddDynamicArguments(c.ID.String()) 430 data, _, err := cmd.RunStdString(&RunOpts{Dir: c.repo.Path}) 431 if err != nil { 432 // handle special case where git can not describe commit 433 if strings.Contains(err.Error(), "cannot describe") { 434 return "", nil 435 } 436 437 return "", err 438 } 439 440 // name-rev commitID output will be "master" or "master~12" 441 return strings.SplitN(strings.TrimSpace(data), "~", 2)[0], nil 442 } 443 444 // CommitFileStatus represents status of files in a commit. 445 type CommitFileStatus struct { 446 Added []string 447 Removed []string 448 Modified []string 449 } 450 451 // NewCommitFileStatus creates a CommitFileStatus 452 func NewCommitFileStatus() *CommitFileStatus { 453 return &CommitFileStatus{ 454 []string{}, []string{}, []string{}, 455 } 456 } 457 458 func parseCommitFileStatus(fileStatus *CommitFileStatus, stdout io.Reader) { 459 rd := bufio.NewReader(stdout) 460 peek, err := rd.Peek(1) 461 if err != nil { 462 if err != io.EOF { 463 log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) 464 } 465 return 466 } 467 if peek[0] == '\n' || peek[0] == '\x00' { 468 _, _ = rd.Discard(1) 469 } 470 for { 471 modifier, err := rd.ReadString('\x00') 472 if err != nil { 473 if err != io.EOF { 474 log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) 475 } 476 return 477 } 478 file, err := rd.ReadString('\x00') 479 if err != nil { 480 if err != io.EOF { 481 log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) 482 } 483 return 484 } 485 file = file[:len(file)-1] 486 switch modifier[0] { 487 case 'A': 488 fileStatus.Added = append(fileStatus.Added, file) 489 case 'D': 490 fileStatus.Removed = append(fileStatus.Removed, file) 491 case 'M': 492 fileStatus.Modified = append(fileStatus.Modified, file) 493 } 494 } 495 } 496 497 // GetCommitFileStatus returns file status of commit in given repository. 498 func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*CommitFileStatus, error) { 499 stdout, w := io.Pipe() 500 done := make(chan struct{}) 501 fileStatus := NewCommitFileStatus() 502 go func() { 503 parseCommitFileStatus(fileStatus, stdout) 504 close(done) 505 }() 506 507 stderr := new(bytes.Buffer) 508 err := NewCommand(ctx, "log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1").AddDynamicArguments(commitID).Run(&RunOpts{ 509 Dir: repoPath, 510 Stdout: w, 511 Stderr: stderr, 512 }) 513 w.Close() // Close writer to exit parsing goroutine 514 if err != nil { 515 return nil, ConcatenateError(err, stderr.String()) 516 } 517 518 <-done 519 return fileStatus, nil 520 } 521 522 // GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository. 523 func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) { 524 commitID, _, err := NewCommand(ctx, "rev-parse").AddDynamicArguments(shortID).RunStdString(&RunOpts{Dir: repoPath}) 525 if err != nil { 526 if strings.Contains(err.Error(), "exit status 128") { 527 return "", ErrNotExist{shortID, ""} 528 } 529 return "", err 530 } 531 return strings.TrimSpace(commitID), nil 532 } 533 534 // GetRepositoryDefaultPublicGPGKey returns the default public key for this commit 535 func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) { 536 if c.repo == nil { 537 return nil, nil 538 } 539 return c.repo.GetDefaultPublicGPGKey(forceUpdate) 540 }