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