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.

365 lines
10 KiB

  1. // Copyright 2015 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package git
  5. import (
  6. "bytes"
  7. "container/list"
  8. "strconv"
  9. "strings"
  10. "github.com/mcuadros/go-version"
  11. )
  12. // GetRefCommitID returns the last commit ID string of given reference (branch or tag).
  13. func (repo *Repository) GetRefCommitID(name string) (string, error) {
  14. stdout, err := NewCommand("show-ref", "--verify", name).RunInDir(repo.Path)
  15. if err != nil {
  16. if strings.Contains(err.Error(), "not a valid ref") {
  17. return "", ErrNotExist{name, ""}
  18. }
  19. return "", err
  20. }
  21. return strings.Split(stdout, " ")[0], nil
  22. }
  23. // GetBranchCommitID returns last commit ID string of given branch.
  24. func (repo *Repository) GetBranchCommitID(name string) (string, error) {
  25. return repo.GetRefCommitID(BranchPrefix + name)
  26. }
  27. // GetTagCommitID returns last commit ID string of given tag.
  28. func (repo *Repository) GetTagCommitID(name string) (string, error) {
  29. return repo.GetRefCommitID(TagPrefix + name)
  30. }
  31. // parseCommitData parses commit information from the (uncompressed) raw
  32. // data from the commit object.
  33. // \n\n separate headers from message
  34. func parseCommitData(data []byte) (*Commit, error) {
  35. commit := new(Commit)
  36. commit.parents = make([]SHA1, 0, 1)
  37. // we now have the contents of the commit object. Let's investigate...
  38. nextline := 0
  39. l:
  40. for {
  41. eol := bytes.IndexByte(data[nextline:], '\n')
  42. switch {
  43. case eol > 0:
  44. line := data[nextline : nextline+eol]
  45. spacepos := bytes.IndexByte(line, ' ')
  46. reftype := line[:spacepos]
  47. switch string(reftype) {
  48. case "tree", "object":
  49. id, err := NewIDFromString(string(line[spacepos+1:]))
  50. if err != nil {
  51. return nil, err
  52. }
  53. commit.Tree.ID = id
  54. case "parent":
  55. // A commit can have one or more parents
  56. oid, err := NewIDFromString(string(line[spacepos+1:]))
  57. if err != nil {
  58. return nil, err
  59. }
  60. commit.parents = append(commit.parents, oid)
  61. case "author", "tagger":
  62. sig, err := newSignatureFromCommitline(line[spacepos+1:])
  63. if err != nil {
  64. return nil, err
  65. }
  66. commit.Author = sig
  67. case "committer":
  68. sig, err := newSignatureFromCommitline(line[spacepos+1:])
  69. if err != nil {
  70. return nil, err
  71. }
  72. commit.Committer = sig
  73. case "gpgsig":
  74. sig, err := newGPGSignatureFromCommitline(data, nextline+spacepos+1, false)
  75. if err != nil {
  76. return nil, err
  77. }
  78. commit.Signature = sig
  79. }
  80. nextline += eol + 1
  81. case eol == 0:
  82. cm := string(data[nextline+1:])
  83. // Tag GPG signatures are stored below the commit message
  84. sigindex := strings.Index(cm, "-----BEGIN PGP SIGNATURE-----")
  85. if sigindex != -1 {
  86. sig, err := newGPGSignatureFromCommitline(data, (nextline+1)+sigindex, true)
  87. if err == nil && sig != nil {
  88. // remove signature from commit message
  89. cm = cm[:sigindex-1]
  90. commit.Signature = sig
  91. }
  92. }
  93. commit.CommitMessage = cm
  94. break l
  95. default:
  96. break l
  97. }
  98. }
  99. return commit, nil
  100. }
  101. func (repo *Repository) getCommit(id SHA1) (*Commit, error) {
  102. c, ok := repo.commitCache.Get(id.String())
  103. if ok {
  104. log("Hit cache: %s", id)
  105. return c.(*Commit), nil
  106. }
  107. data, err := NewCommand("cat-file", "-p", id.String()).RunInDirBytes(repo.Path)
  108. if err != nil {
  109. if strings.Contains(err.Error(), "fatal: Not a valid object name") {
  110. return nil, ErrNotExist{id.String(), ""}
  111. }
  112. return nil, err
  113. }
  114. commit, err := parseCommitData(data)
  115. if err != nil {
  116. return nil, err
  117. }
  118. commit.repo = repo
  119. commit.ID = id
  120. repo.commitCache.Set(id.String(), commit)
  121. return commit, nil
  122. }
  123. // GetCommit returns commit object of by ID string.
  124. func (repo *Repository) GetCommit(commitID string) (*Commit, error) {
  125. if len(commitID) != 40 {
  126. var err error
  127. commitID, err = NewCommand("rev-parse", commitID).RunInDir(repo.Path)
  128. if err != nil {
  129. return nil, err
  130. }
  131. }
  132. id, err := NewIDFromString(commitID)
  133. if err != nil {
  134. return nil, err
  135. }
  136. return repo.getCommit(id)
  137. }
  138. // GetBranchCommit returns the last commit of given branch.
  139. func (repo *Repository) GetBranchCommit(name string) (*Commit, error) {
  140. commitID, err := repo.GetBranchCommitID(name)
  141. if err != nil {
  142. return nil, err
  143. }
  144. return repo.GetCommit(commitID)
  145. }
  146. // GetTagCommit get the commit of the specific tag via name
  147. func (repo *Repository) GetTagCommit(name string) (*Commit, error) {
  148. commitID, err := repo.GetTagCommitID(name)
  149. if err != nil {
  150. return nil, err
  151. }
  152. return repo.GetCommit(commitID)
  153. }
  154. func (repo *Repository) getCommitByPathWithID(id SHA1, relpath string) (*Commit, error) {
  155. // File name starts with ':' must be escaped.
  156. if relpath[0] == ':' {
  157. relpath = `\` + relpath
  158. }
  159. stdout, err := NewCommand("log", "-1", prettyLogFormat, id.String(), "--", relpath).RunInDir(repo.Path)
  160. if err != nil {
  161. return nil, err
  162. }
  163. id, err = NewIDFromString(stdout)
  164. if err != nil {
  165. return nil, err
  166. }
  167. return repo.getCommit(id)
  168. }
  169. // GetCommitByPath returns the last commit of relative path.
  170. func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) {
  171. stdout, err := NewCommand("log", "-1", prettyLogFormat, "--", relpath).RunInDirBytes(repo.Path)
  172. if err != nil {
  173. return nil, err
  174. }
  175. commits, err := repo.parsePrettyFormatLogToList(stdout)
  176. if err != nil {
  177. return nil, err
  178. }
  179. return commits.Front().Value.(*Commit), nil
  180. }
  181. // CommitsRangeSize the default commits range size
  182. var CommitsRangeSize = 50
  183. func (repo *Repository) commitsByRange(id SHA1, page int) (*list.List, error) {
  184. stdout, err := NewCommand("log", id.String(), "--skip="+strconv.Itoa((page-1)*CommitsRangeSize),
  185. "--max-count="+strconv.Itoa(CommitsRangeSize), prettyLogFormat).RunInDirBytes(repo.Path)
  186. if err != nil {
  187. return nil, err
  188. }
  189. return repo.parsePrettyFormatLogToList(stdout)
  190. }
  191. func (repo *Repository) searchCommits(id SHA1, keyword string, all bool) (*list.List, error) {
  192. cmd := NewCommand("log", id.String(), "-100", "-i", "--grep="+keyword, prettyLogFormat)
  193. if all {
  194. cmd.AddArguments("--all")
  195. }
  196. stdout, err := cmd.RunInDirBytes(repo.Path)
  197. if err != nil {
  198. return nil, err
  199. }
  200. return repo.parsePrettyFormatLogToList(stdout)
  201. }
  202. func (repo *Repository) getFilesChanged(id1 string, id2 string) ([]string, error) {
  203. stdout, err := NewCommand("diff", "--name-only", id1, id2).RunInDirBytes(repo.Path)
  204. if err != nil {
  205. return nil, err
  206. }
  207. return strings.Split(string(stdout), "\n"), nil
  208. }
  209. // FileCommitsCount return the number of files at a revison
  210. func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) {
  211. return commitsCount(repo.Path, revision, file)
  212. }
  213. // CommitsByFileAndRange return the commits accroding revison file and the page
  214. func (repo *Repository) CommitsByFileAndRange(revision, file string, page int) (*list.List, error) {
  215. stdout, err := NewCommand("log", revision, "--follow", "--skip="+strconv.Itoa((page-1)*50),
  216. "--max-count="+strconv.Itoa(CommitsRangeSize), prettyLogFormat, "--", file).RunInDirBytes(repo.Path)
  217. if err != nil {
  218. return nil, err
  219. }
  220. return repo.parsePrettyFormatLogToList(stdout)
  221. }
  222. // FilesCountBetween return the number of files changed between two commits
  223. func (repo *Repository) FilesCountBetween(startCommitID, endCommitID string) (int, error) {
  224. stdout, err := NewCommand("diff", "--name-only", startCommitID+"..."+endCommitID).RunInDir(repo.Path)
  225. if err != nil {
  226. return 0, err
  227. }
  228. return len(strings.Split(stdout, "\n")) - 1, nil
  229. }
  230. // CommitsBetween returns a list that contains commits between [last, before).
  231. func (repo *Repository) CommitsBetween(last *Commit, before *Commit) (*list.List, error) {
  232. stdout, err := NewCommand("rev-list", before.ID.String()+"..."+last.ID.String()).RunInDirBytes(repo.Path)
  233. if err != nil {
  234. return nil, err
  235. }
  236. return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout))
  237. }
  238. // CommitsBetweenIDs return commits between twoe commits
  239. func (repo *Repository) CommitsBetweenIDs(last, before string) (*list.List, error) {
  240. lastCommit, err := repo.GetCommit(last)
  241. if err != nil {
  242. return nil, err
  243. }
  244. beforeCommit, err := repo.GetCommit(before)
  245. if err != nil {
  246. return nil, err
  247. }
  248. return repo.CommitsBetween(lastCommit, beforeCommit)
  249. }
  250. // CommitsCountBetween return numbers of commits between two commits
  251. func (repo *Repository) CommitsCountBetween(start, end string) (int64, error) {
  252. return commitsCount(repo.Path, start+"..."+end, "")
  253. }
  254. // commitsBefore the limit is depth, not total number of returned commits.
  255. func (repo *Repository) commitsBefore(id SHA1, limit int) (*list.List, error) {
  256. cmd := NewCommand("log")
  257. if limit > 0 {
  258. cmd.AddArguments("-"+strconv.Itoa(limit), prettyLogFormat, id.String())
  259. } else {
  260. cmd.AddArguments(prettyLogFormat, id.String())
  261. }
  262. stdout, err := cmd.RunInDirBytes(repo.Path)
  263. if err != nil {
  264. return nil, err
  265. }
  266. formattedLog, err := repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout))
  267. if err != nil {
  268. return nil, err
  269. }
  270. commits := list.New()
  271. for logEntry := formattedLog.Front(); logEntry != nil; logEntry = logEntry.Next() {
  272. commit := logEntry.Value.(*Commit)
  273. branches, err := repo.getBranches(commit, 2)
  274. if err != nil {
  275. return nil, err
  276. }
  277. if len(branches) > 1 {
  278. break
  279. }
  280. commits.PushBack(commit)
  281. }
  282. return commits, nil
  283. }
  284. func (repo *Repository) getCommitsBefore(id SHA1) (*list.List, error) {
  285. return repo.commitsBefore(id, 0)
  286. }
  287. func (repo *Repository) getCommitsBeforeLimit(id SHA1, num int) (*list.List, error) {
  288. return repo.commitsBefore(id, num)
  289. }
  290. func (repo *Repository) getBranches(commit *Commit, limit int) ([]string, error) {
  291. if version.Compare(gitVersion, "2.7.0", ">=") {
  292. stdout, err := NewCommand("for-each-ref", "--count="+strconv.Itoa(limit), "--format=%(refname:strip=2)", "--contains", commit.ID.String(), BranchPrefix).RunInDir(repo.Path)
  293. if err != nil {
  294. return nil, err
  295. }
  296. branches := strings.Fields(stdout)
  297. return branches, nil
  298. }
  299. stdout, err := NewCommand("branch", "--contains", commit.ID.String()).RunInDir(repo.Path)
  300. if err != nil {
  301. return nil, err
  302. }
  303. refs := strings.Split(stdout, "\n")
  304. var max int
  305. if len(refs) > limit {
  306. max = limit
  307. } else {
  308. max = len(refs) - 1
  309. }
  310. branches := make([]string, max)
  311. for i, ref := range refs[:max] {
  312. parts := strings.Fields(ref)
  313. branches[i] = parts[len(parts)-1]
  314. }
  315. return branches, nil
  316. }