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