You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

commit.go 15KB

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