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