gitea源码

repo_commit.go 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. // Copyright 2015 The Gogs Authors. All rights reserved.
  2. // Copyright 2019 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package git
  5. import (
  6. "bytes"
  7. "io"
  8. "os"
  9. "strconv"
  10. "strings"
  11. "code.gitea.io/gitea/modules/cache"
  12. "code.gitea.io/gitea/modules/git/gitcmd"
  13. "code.gitea.io/gitea/modules/setting"
  14. )
  15. // GetBranchCommitID returns last commit ID string of given branch.
  16. func (repo *Repository) GetBranchCommitID(name string) (string, error) {
  17. return repo.GetRefCommitID(BranchPrefix + name)
  18. }
  19. // GetTagCommitID returns last commit ID string of given tag.
  20. func (repo *Repository) GetTagCommitID(name string) (string, error) {
  21. return repo.GetRefCommitID(TagPrefix + name)
  22. }
  23. // GetCommit returns commit object of by ID string.
  24. func (repo *Repository) GetCommit(commitID string) (*Commit, error) {
  25. id, err := repo.ConvertToGitID(commitID)
  26. if err != nil {
  27. return nil, err
  28. }
  29. return repo.getCommit(id)
  30. }
  31. // GetBranchCommit returns the last commit of given branch.
  32. func (repo *Repository) GetBranchCommit(name string) (*Commit, error) {
  33. commitID, err := repo.GetBranchCommitID(name)
  34. if err != nil {
  35. return nil, err
  36. }
  37. return repo.GetCommit(commitID)
  38. }
  39. // GetTagCommit get the commit of the specific tag via name
  40. func (repo *Repository) GetTagCommit(name string) (*Commit, error) {
  41. commitID, err := repo.GetTagCommitID(name)
  42. if err != nil {
  43. return nil, err
  44. }
  45. return repo.GetCommit(commitID)
  46. }
  47. func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Commit, error) {
  48. // File name starts with ':' must be escaped.
  49. if relpath[0] == ':' {
  50. relpath = `\` + relpath
  51. }
  52. stdout, _, runErr := gitcmd.NewCommand("log", "-1", prettyLogFormat).AddDynamicArguments(id.String()).AddDashesAndList(relpath).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  53. if runErr != nil {
  54. return nil, runErr
  55. }
  56. id, err := NewIDFromString(stdout)
  57. if err != nil {
  58. return nil, err
  59. }
  60. return repo.getCommit(id)
  61. }
  62. // GetCommitByPath returns the last commit of relative path.
  63. func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) {
  64. stdout, _, runErr := gitcmd.NewCommand("log", "-1", prettyLogFormat).AddDashesAndList(relpath).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  65. if runErr != nil {
  66. return nil, runErr
  67. }
  68. commits, err := repo.parsePrettyFormatLogToList(stdout)
  69. if err != nil {
  70. return nil, err
  71. }
  72. if len(commits) == 0 {
  73. return nil, ErrNotExist{ID: relpath}
  74. }
  75. return commits[0], nil
  76. }
  77. // commitsByRangeWithTime returns the specific page commits before current revision, with not, since, until support
  78. func (repo *Repository) commitsByRangeWithTime(id ObjectID, page, pageSize int, not, since, until string) ([]*Commit, error) {
  79. cmd := gitcmd.NewCommand("log").
  80. AddOptionFormat("--skip=%d", (page-1)*pageSize).
  81. AddOptionFormat("--max-count=%d", pageSize).
  82. AddArguments(prettyLogFormat).
  83. AddDynamicArguments(id.String())
  84. if not != "" {
  85. cmd.AddOptionValues("--not", not)
  86. }
  87. if since != "" {
  88. cmd.AddOptionFormat("--since=%s", since)
  89. }
  90. if until != "" {
  91. cmd.AddOptionFormat("--until=%s", until)
  92. }
  93. stdout, _, err := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  94. if err != nil {
  95. return nil, err
  96. }
  97. return repo.parsePrettyFormatLogToList(stdout)
  98. }
  99. func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([]*Commit, error) {
  100. // add common arguments to git command
  101. addCommonSearchArgs := func(c *gitcmd.Command) {
  102. // ignore case
  103. c.AddArguments("-i")
  104. // add authors if present in search query
  105. for _, v := range opts.Authors {
  106. c.AddOptionFormat("--author=%s", v)
  107. }
  108. // add committers if present in search query
  109. for _, v := range opts.Committers {
  110. c.AddOptionFormat("--committer=%s", v)
  111. }
  112. // add time constraints if present in search query
  113. if len(opts.After) > 0 {
  114. c.AddOptionFormat("--after=%s", opts.After)
  115. }
  116. if len(opts.Before) > 0 {
  117. c.AddOptionFormat("--before=%s", opts.Before)
  118. }
  119. }
  120. // create new git log command with limit of 100 commits
  121. cmd := gitcmd.NewCommand("log", "-100", prettyLogFormat).AddDynamicArguments(id.String())
  122. // pretend that all refs along with HEAD were listed on command line as <commis>
  123. // https://git-scm.com/docs/git-log#Documentation/git-log.txt---all
  124. // note this is done only for command created above
  125. if opts.All {
  126. cmd.AddArguments("--all")
  127. }
  128. // interpret search string keywords as string instead of regex
  129. cmd.AddArguments("--fixed-strings")
  130. // add remaining keywords from search string
  131. // note this is done only for command created above
  132. for _, v := range opts.Keywords {
  133. cmd.AddOptionFormat("--grep=%s", v)
  134. }
  135. // search for commits matching given constraints and keywords in commit msg
  136. addCommonSearchArgs(cmd)
  137. stdout, _, err := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  138. if err != nil {
  139. return nil, err
  140. }
  141. if len(stdout) != 0 {
  142. stdout = append(stdout, '\n')
  143. }
  144. // if there are any keywords (ie not committer:, author:, time:)
  145. // then let's iterate over them
  146. for _, v := range opts.Keywords {
  147. // ignore anything not matching a valid sha pattern
  148. if id.Type().IsValid(v) {
  149. // create new git log command with 1 commit limit
  150. hashCmd := gitcmd.NewCommand("log", "-1", prettyLogFormat)
  151. // add previous arguments except for --grep and --all
  152. addCommonSearchArgs(hashCmd)
  153. // add keyword as <commit>
  154. hashCmd.AddDynamicArguments(v)
  155. // search with given constraints for commit matching sha hash of v
  156. hashMatching, _, err := hashCmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  157. if err != nil || bytes.Contains(stdout, hashMatching) {
  158. continue
  159. }
  160. stdout = append(stdout, hashMatching...)
  161. stdout = append(stdout, '\n')
  162. }
  163. }
  164. return repo.parsePrettyFormatLogToList(bytes.TrimSuffix(stdout, []byte{'\n'}))
  165. }
  166. // FileChangedBetweenCommits Returns true if the file changed between commit IDs id1 and id2
  167. // You must ensure that id1 and id2 are valid commit ids.
  168. func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bool, error) {
  169. stdout, _, err := gitcmd.NewCommand("diff", "--name-only", "-z").AddDynamicArguments(id1, id2).AddDashesAndList(filename).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  170. if err != nil {
  171. return false, err
  172. }
  173. return len(strings.TrimSpace(string(stdout))) > 0, nil
  174. }
  175. // FileCommitsCount return the number of files at a revision
  176. func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) {
  177. return CommitsCount(repo.Ctx,
  178. CommitsCountOptions{
  179. RepoPath: repo.Path,
  180. Revision: []string{revision},
  181. RelPath: []string{file},
  182. })
  183. }
  184. type CommitsByFileAndRangeOptions struct {
  185. Revision string
  186. File string
  187. Not string
  188. Page int
  189. Since string
  190. Until string
  191. }
  192. // CommitsByFileAndRange return the commits according revision file and the page
  193. func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) {
  194. stdoutReader, stdoutWriter := io.Pipe()
  195. defer func() {
  196. _ = stdoutReader.Close()
  197. _ = stdoutWriter.Close()
  198. }()
  199. go func() {
  200. stderr := strings.Builder{}
  201. gitCmd := gitcmd.NewCommand("rev-list").
  202. AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize).
  203. AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize)
  204. gitCmd.AddDynamicArguments(opts.Revision)
  205. if opts.Not != "" {
  206. gitCmd.AddOptionValues("--not", opts.Not)
  207. }
  208. if opts.Since != "" {
  209. gitCmd.AddOptionFormat("--since=%s", opts.Since)
  210. }
  211. if opts.Until != "" {
  212. gitCmd.AddOptionFormat("--until=%s", opts.Until)
  213. }
  214. gitCmd.AddDashesAndList(opts.File)
  215. err := gitCmd.Run(repo.Ctx, &gitcmd.RunOpts{
  216. Dir: repo.Path,
  217. Stdout: stdoutWriter,
  218. Stderr: &stderr,
  219. })
  220. if err != nil {
  221. _ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String()))
  222. } else {
  223. _ = stdoutWriter.Close()
  224. }
  225. }()
  226. objectFormat, err := repo.GetObjectFormat()
  227. if err != nil {
  228. return nil, err
  229. }
  230. length := objectFormat.FullLength()
  231. commits := []*Commit{}
  232. shaline := make([]byte, length+1)
  233. for {
  234. n, err := io.ReadFull(stdoutReader, shaline)
  235. if err != nil || n < length {
  236. if err == io.EOF {
  237. err = nil
  238. }
  239. return commits, err
  240. }
  241. objectID, err := NewIDFromString(string(shaline[0:length]))
  242. if err != nil {
  243. return nil, err
  244. }
  245. commit, err := repo.getCommit(objectID)
  246. if err != nil {
  247. return nil, err
  248. }
  249. commits = append(commits, commit)
  250. }
  251. }
  252. // FilesCountBetween return the number of files changed between two commits
  253. func (repo *Repository) FilesCountBetween(startCommitID, endCommitID string) (int, error) {
  254. stdout, _, err := gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(startCommitID+"..."+endCommitID).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  255. if err != nil && strings.Contains(err.Error(), "no merge base") {
  256. // git >= 2.28 now returns an error if startCommitID and endCommitID have become unrelated.
  257. // previously it would return the results of git diff --name-only startCommitID endCommitID so let's try that...
  258. stdout, _, err = gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(startCommitID, endCommitID).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  259. }
  260. if err != nil {
  261. return 0, err
  262. }
  263. return len(strings.Split(stdout, "\n")) - 1, nil
  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.
  267. func (repo *Repository) CommitsBetween(last, before *Commit) ([]*Commit, error) {
  268. var stdout []byte
  269. var err error
  270. if before == nil {
  271. stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  272. } else {
  273. stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(before.ID.String()+".."+last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: 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 = gitcmd.NewCommand("rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  278. }
  279. }
  280. if err != nil {
  281. return nil, err
  282. }
  283. return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout))
  284. }
  285. // CommitsBetweenLimit returns a list that contains at most limit commits skipping the first skip commits between [before, last)
  286. func (repo *Repository) CommitsBetweenLimit(last, before *Commit, limit, skip int) ([]*Commit, error) {
  287. var stdout []byte
  288. var err error
  289. if before == nil {
  290. stdout, _, err = gitcmd.NewCommand("rev-list").
  291. AddOptionValues("--max-count", strconv.Itoa(limit)).
  292. AddOptionValues("--skip", strconv.Itoa(skip)).
  293. AddDynamicArguments(last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  294. } else {
  295. stdout, _, err = gitcmd.NewCommand("rev-list").
  296. AddOptionValues("--max-count", strconv.Itoa(limit)).
  297. AddOptionValues("--skip", strconv.Itoa(skip)).
  298. AddDynamicArguments(before.ID.String()+".."+last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  299. if err != nil && strings.Contains(err.Error(), "no merge base") {
  300. // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
  301. // previously it would return the results of git rev-list --max-count n before last so let's try that...
  302. stdout, _, err = gitcmd.NewCommand("rev-list").
  303. AddOptionValues("--max-count", strconv.Itoa(limit)).
  304. AddOptionValues("--skip", strconv.Itoa(skip)).
  305. AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  306. }
  307. }
  308. if err != nil {
  309. return nil, err
  310. }
  311. return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout))
  312. }
  313. // CommitsBetweenNotBase returns a list that contains commits between [before, last), excluding commits in baseBranch.
  314. // If before is detached (removed by reset + push) it is not included.
  315. func (repo *Repository) CommitsBetweenNotBase(last, before *Commit, baseBranch string) ([]*Commit, error) {
  316. var stdout []byte
  317. var err error
  318. if before == nil {
  319. stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  320. } else {
  321. stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(before.ID.String()+".."+last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  322. if err != nil && strings.Contains(err.Error(), "no merge base") {
  323. // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
  324. // previously it would return the results of git rev-list before last so let's try that...
  325. stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  326. }
  327. }
  328. if err != nil {
  329. return nil, err
  330. }
  331. return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout))
  332. }
  333. // CommitsBetweenIDs return commits between twoe commits
  334. func (repo *Repository) CommitsBetweenIDs(last, before string) ([]*Commit, error) {
  335. lastCommit, err := repo.GetCommit(last)
  336. if err != nil {
  337. return nil, err
  338. }
  339. if before == "" {
  340. return repo.CommitsBetween(lastCommit, nil)
  341. }
  342. beforeCommit, err := repo.GetCommit(before)
  343. if err != nil {
  344. return nil, err
  345. }
  346. return repo.CommitsBetween(lastCommit, beforeCommit)
  347. }
  348. // CommitsCountBetween return numbers of commits between two commits
  349. func (repo *Repository) CommitsCountBetween(start, end string) (int64, error) {
  350. count, err := CommitsCount(repo.Ctx, CommitsCountOptions{
  351. RepoPath: repo.Path,
  352. Revision: []string{start + ".." + end},
  353. })
  354. if err != nil && strings.Contains(err.Error(), "no merge base") {
  355. // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
  356. // previously it would return the results of git rev-list before last so let's try that...
  357. return CommitsCount(repo.Ctx, CommitsCountOptions{
  358. RepoPath: repo.Path,
  359. Revision: []string{start, end},
  360. })
  361. }
  362. return count, err
  363. }
  364. // commitsBefore the limit is depth, not total number of returned commits.
  365. func (repo *Repository) commitsBefore(id ObjectID, limit int) ([]*Commit, error) {
  366. cmd := gitcmd.NewCommand("log", prettyLogFormat)
  367. if limit > 0 {
  368. cmd.AddOptionFormat("-%d", limit)
  369. }
  370. cmd.AddDynamicArguments(id.String())
  371. stdout, _, runErr := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  372. if runErr != nil {
  373. return nil, runErr
  374. }
  375. formattedLog, err := repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout))
  376. if err != nil {
  377. return nil, err
  378. }
  379. commits := make([]*Commit, 0, len(formattedLog))
  380. for _, commit := range formattedLog {
  381. branches, err := repo.getBranches(os.Environ(), commit.ID.String(), 2)
  382. if err != nil {
  383. return nil, err
  384. }
  385. if len(branches) > 1 {
  386. break
  387. }
  388. commits = append(commits, commit)
  389. }
  390. return commits, nil
  391. }
  392. func (repo *Repository) getCommitsBefore(id ObjectID) ([]*Commit, error) {
  393. return repo.commitsBefore(id, 0)
  394. }
  395. func (repo *Repository) getCommitsBeforeLimit(id ObjectID, num int) ([]*Commit, error) {
  396. return repo.commitsBefore(id, num)
  397. }
  398. func (repo *Repository) getBranches(env []string, commitID string, limit int) ([]string, error) {
  399. if DefaultFeatures().CheckVersionAtLeast("2.7.0") {
  400. stdout, _, err := gitcmd.NewCommand("for-each-ref", "--format=%(refname:strip=2)").
  401. AddOptionFormat("--count=%d", limit).
  402. AddOptionValues("--contains", commitID, BranchPrefix).
  403. RunStdString(repo.Ctx, &gitcmd.RunOpts{
  404. Dir: repo.Path,
  405. Env: env,
  406. })
  407. if err != nil {
  408. return nil, err
  409. }
  410. branches := strings.Fields(stdout)
  411. return branches, nil
  412. }
  413. stdout, _, err := gitcmd.NewCommand("branch").AddOptionValues("--contains", commitID).RunStdString(repo.Ctx, &gitcmd.RunOpts{
  414. Dir: repo.Path,
  415. Env: env,
  416. })
  417. if err != nil {
  418. return nil, err
  419. }
  420. refs := strings.Split(stdout, "\n")
  421. var maxNum int
  422. if len(refs) > limit {
  423. maxNum = limit
  424. } else {
  425. maxNum = len(refs) - 1
  426. }
  427. branches := make([]string, maxNum)
  428. for i, ref := range refs[:maxNum] {
  429. parts := strings.Fields(ref)
  430. branches[i] = parts[len(parts)-1]
  431. }
  432. return branches, nil
  433. }
  434. // GetCommitsFromIDs get commits from commit IDs
  435. func (repo *Repository) GetCommitsFromIDs(commitIDs []string) []*Commit {
  436. commits := make([]*Commit, 0, len(commitIDs))
  437. for _, commitID := range commitIDs {
  438. commit, err := repo.GetCommit(commitID)
  439. if err == nil && commit != nil {
  440. commits = append(commits, commit)
  441. }
  442. }
  443. return commits
  444. }
  445. // IsCommitInBranch check if the commit is on the branch
  446. func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err error) {
  447. stdout, _, err := gitcmd.NewCommand("branch", "--contains").AddDynamicArguments(commitID, branch).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
  448. if err != nil {
  449. return false, err
  450. }
  451. return len(stdout) > 0, err
  452. }
  453. func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error {
  454. if repo.LastCommitCache == nil {
  455. commitsCount, err := cache.GetInt64(cacheKey, func() (int64, error) {
  456. commit, err := repo.GetCommit(sha)
  457. if err != nil {
  458. return 0, err
  459. }
  460. return commit.CommitsCount()
  461. })
  462. if err != nil {
  463. return err
  464. }
  465. repo.LastCommitCache = NewLastCommitCache(commitsCount, fullName, repo, cache.GetCache())
  466. }
  467. return nil
  468. }
  469. // GetCommitBranchStart returns the commit where the branch diverged
  470. func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) {
  471. cmd := gitcmd.NewCommand("log", prettyLogFormat)
  472. cmd.AddDynamicArguments(endCommitID)
  473. stdout, _, runErr := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{
  474. Dir: repo.Path,
  475. Env: env,
  476. })
  477. if runErr != nil {
  478. return "", runErr
  479. }
  480. parts := bytes.SplitSeq(bytes.TrimSpace(stdout), []byte{'\n'})
  481. // check the commits one by one until we find a commit contained by another branch
  482. // and we think this commit is the divergence point
  483. for commitID := range parts {
  484. branches, err := repo.getBranches(env, string(commitID), 2)
  485. if err != nil {
  486. return "", err
  487. }
  488. for _, b := range branches {
  489. if b != branch {
  490. return string(commitID), nil
  491. }
  492. }
  493. }
  494. return "", nil
  495. }