Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

repo_commit.go 16KB

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