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.

git.go 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. // Copyright 2014 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 models
  5. import (
  6. "bufio"
  7. "bytes"
  8. "container/list"
  9. "errors"
  10. "fmt"
  11. "io"
  12. "os"
  13. "os/exec"
  14. "path"
  15. "strings"
  16. "github.com/Unknwon/com"
  17. "github.com/gogits/git"
  18. "github.com/gogits/gogs/modules/base"
  19. "github.com/gogits/gogs/modules/log"
  20. )
  21. // RepoFile represents a file object in git repository.
  22. type RepoFile struct {
  23. *git.TreeEntry
  24. Path string
  25. Size int64
  26. Repo *git.Repository
  27. Commit *git.Commit
  28. }
  29. // LookupBlob returns the content of an object.
  30. func (file *RepoFile) LookupBlob() (*git.Blob, error) {
  31. if file.Repo == nil {
  32. return nil, ErrRepoFileNotLoaded
  33. }
  34. return file.Repo.LookupBlob(file.Id)
  35. }
  36. // GetBranches returns all branches of given repository.
  37. func GetBranches(userName, repoName string) ([]string, error) {
  38. repo, err := git.OpenRepository(RepoPath(userName, repoName))
  39. if err != nil {
  40. return nil, err
  41. }
  42. refs, err := repo.AllReferences()
  43. if err != nil {
  44. return nil, err
  45. }
  46. brs := make([]string, len(refs))
  47. for i, ref := range refs {
  48. brs[i] = ref.BranchName()
  49. }
  50. return brs, nil
  51. }
  52. // GetTags returns all tags of given repository.
  53. func GetTags(userName, repoName string) ([]string, error) {
  54. repo, err := git.OpenRepository(RepoPath(userName, repoName))
  55. if err != nil {
  56. return nil, err
  57. }
  58. refs, err := repo.AllTags()
  59. if err != nil {
  60. return nil, err
  61. }
  62. tags := make([]string, len(refs))
  63. for i, ref := range refs {
  64. tags[i] = ref.Name
  65. }
  66. return tags, nil
  67. }
  68. func IsBranchExist(userName, repoName, branchName string) bool {
  69. repo, err := git.OpenRepository(RepoPath(userName, repoName))
  70. if err != nil {
  71. return false
  72. }
  73. return repo.IsBranchExist(branchName)
  74. }
  75. func GetTargetFile(userName, repoName, branchName, commitId, rpath string) (*RepoFile, error) {
  76. repo, err := git.OpenRepository(RepoPath(userName, repoName))
  77. if err != nil {
  78. return nil, err
  79. }
  80. commit, err := repo.GetCommitOfBranch(branchName)
  81. if err != nil {
  82. commit, err = repo.GetCommit(commitId)
  83. if err != nil {
  84. return nil, err
  85. }
  86. }
  87. parts := strings.Split(path.Clean(rpath), "/")
  88. var entry *git.TreeEntry
  89. tree := commit.Tree
  90. for i, part := range parts {
  91. if i == len(parts)-1 {
  92. entry = tree.EntryByName(part)
  93. if entry == nil {
  94. return nil, ErrRepoFileNotExist
  95. }
  96. } else {
  97. tree, err = repo.SubTree(tree, part)
  98. if err != nil {
  99. return nil, err
  100. }
  101. }
  102. }
  103. size, err := repo.ObjectSize(entry.Id)
  104. if err != nil {
  105. return nil, err
  106. }
  107. repoFile := &RepoFile{
  108. entry,
  109. rpath,
  110. size,
  111. repo,
  112. commit,
  113. }
  114. return repoFile, nil
  115. }
  116. // GetReposFiles returns a list of file object in given directory of repository.
  117. // func GetReposFilesOfBranch(userName, repoName, branchName, rpath string) ([]*RepoFile, error) {
  118. // return getReposFiles(userName, repoName, commitId, rpath)
  119. // }
  120. // GetReposFiles returns a list of file object in given directory of repository.
  121. func GetReposFiles(userName, repoName, commitId, rpath string) ([]*RepoFile, error) {
  122. return getReposFiles(userName, repoName, commitId, rpath)
  123. }
  124. func getReposFiles(userName, repoName, commitId string, rpath string) ([]*RepoFile, error) {
  125. repopath := RepoPath(userName, repoName)
  126. repo, err := git.OpenRepository(repopath)
  127. if err != nil {
  128. return nil, err
  129. }
  130. commit, err := repo.GetCommit(commitId)
  131. if err != nil {
  132. return nil, err
  133. }
  134. var repodirs []*RepoFile
  135. var repofiles []*RepoFile
  136. commit.Tree.Walk(func(dirname string, entry *git.TreeEntry) int {
  137. if dirname == rpath {
  138. // TODO: size get method shoule be improved
  139. size, err := repo.ObjectSize(entry.Id)
  140. if err != nil {
  141. return 0
  142. }
  143. stdout, _, err := com.ExecCmdDir(repopath, "git", "log", "-1", "--pretty=format:%H", commitId, "--", path.Join(dirname, entry.Name))
  144. if err != nil {
  145. return 0
  146. }
  147. filecm, err := repo.GetCommit(string(stdout))
  148. if err != nil {
  149. return 0
  150. }
  151. rp := &RepoFile{
  152. entry,
  153. path.Join(dirname, entry.Name),
  154. size,
  155. repo,
  156. filecm,
  157. }
  158. if entry.IsFile() {
  159. repofiles = append(repofiles, rp)
  160. } else if entry.IsDir() {
  161. repodirs = append(repodirs, rp)
  162. }
  163. }
  164. return 0
  165. })
  166. return append(repodirs, repofiles...), nil
  167. }
  168. func GetCommit(userName, repoName, commitId string) (*git.Commit, error) {
  169. repo, err := git.OpenRepository(RepoPath(userName, repoName))
  170. if err != nil {
  171. return nil, err
  172. }
  173. return repo.GetCommit(commitId)
  174. }
  175. // GetCommitsByBranch returns all commits of given branch of repository.
  176. func GetCommitsByBranch(userName, repoName, branchName string) (*list.List, error) {
  177. repo, err := git.OpenRepository(RepoPath(userName, repoName))
  178. if err != nil {
  179. return nil, err
  180. }
  181. r, err := repo.LookupReference(fmt.Sprintf("refs/heads/%s", branchName))
  182. if err != nil {
  183. return nil, err
  184. }
  185. return r.AllCommits()
  186. }
  187. // GetCommitsByCommitId returns all commits of given commitId of repository.
  188. func GetCommitsByCommitId(userName, repoName, commitId string) (*list.List, error) {
  189. repo, err := git.OpenRepository(RepoPath(userName, repoName))
  190. if err != nil {
  191. return nil, err
  192. }
  193. oid, err := git.NewOidFromString(commitId)
  194. if err != nil {
  195. return nil, err
  196. }
  197. return repo.CommitsBefore(oid)
  198. }
  199. // Diff line types.
  200. const (
  201. DIFF_LINE_PLAIN = iota + 1
  202. DIFF_LINE_ADD
  203. DIFF_LINE_DEL
  204. DIFF_LINE_SECTION
  205. )
  206. const (
  207. DIFF_FILE_ADD = iota + 1
  208. DIFF_FILE_CHANGE
  209. DIFF_FILE_DEL
  210. )
  211. type DiffLine struct {
  212. LeftIdx int
  213. RightIdx int
  214. Type int
  215. Content string
  216. }
  217. func (d DiffLine) GetType() int {
  218. return d.Type
  219. }
  220. type DiffSection struct {
  221. Name string
  222. Lines []*DiffLine
  223. }
  224. type DiffFile struct {
  225. Name string
  226. Addition, Deletion int
  227. Type int
  228. Sections []*DiffSection
  229. }
  230. type Diff struct {
  231. TotalAddition, TotalDeletion int
  232. Files []*DiffFile
  233. }
  234. func (diff *Diff) NumFiles() int {
  235. return len(diff.Files)
  236. }
  237. const DIFF_HEAD = "diff --git "
  238. func ParsePatch(reader io.Reader) (*Diff, error) {
  239. scanner := bufio.NewScanner(reader)
  240. var (
  241. curFile *DiffFile
  242. curSection = &DiffSection{
  243. Lines: make([]*DiffLine, 0, 10),
  244. }
  245. leftLine, rightLine int
  246. )
  247. diff := &Diff{Files: make([]*DiffFile, 0)}
  248. var i int
  249. for scanner.Scan() {
  250. line := scanner.Text()
  251. // fmt.Println(i, line)
  252. if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") {
  253. continue
  254. }
  255. i = i + 1
  256. // Diff data too large.
  257. if i == 5000 {
  258. log.Warn("Diff data too large")
  259. return &Diff{}, nil
  260. }
  261. if line == "" {
  262. continue
  263. }
  264. if line[0] == ' ' {
  265. diffLine := &DiffLine{Type: DIFF_LINE_PLAIN, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
  266. leftLine++
  267. rightLine++
  268. curSection.Lines = append(curSection.Lines, diffLine)
  269. continue
  270. } else if line[0] == '@' {
  271. curSection = &DiffSection{}
  272. curFile.Sections = append(curFile.Sections, curSection)
  273. ss := strings.Split(line, "@@")
  274. diffLine := &DiffLine{Type: DIFF_LINE_SECTION, Content: line}
  275. curSection.Lines = append(curSection.Lines, diffLine)
  276. // Parse line number.
  277. ranges := strings.Split(ss[len(ss)-2][1:], " ")
  278. leftLine, _ = base.StrTo(strings.Split(ranges[0], ",")[0][1:]).Int()
  279. rightLine, _ = base.StrTo(strings.Split(ranges[1], ",")[0]).Int()
  280. continue
  281. } else if line[0] == '+' {
  282. curFile.Addition++
  283. diff.TotalAddition++
  284. diffLine := &DiffLine{Type: DIFF_LINE_ADD, Content: line, RightIdx: rightLine}
  285. rightLine++
  286. curSection.Lines = append(curSection.Lines, diffLine)
  287. continue
  288. } else if line[0] == '-' {
  289. curFile.Deletion++
  290. diff.TotalDeletion++
  291. diffLine := &DiffLine{Type: DIFF_LINE_DEL, Content: line, LeftIdx: leftLine}
  292. if leftLine > 0 {
  293. leftLine++
  294. }
  295. curSection.Lines = append(curSection.Lines, diffLine)
  296. continue
  297. }
  298. // Get new file.
  299. if strings.HasPrefix(line, DIFF_HEAD) {
  300. fs := strings.Split(line[len(DIFF_HEAD):], " ")
  301. a := fs[0]
  302. curFile = &DiffFile{
  303. Name: a[strings.Index(a, "/")+1:],
  304. Type: DIFF_FILE_CHANGE,
  305. Sections: make([]*DiffSection, 0, 10),
  306. }
  307. diff.Files = append(diff.Files, curFile)
  308. // Check file diff type.
  309. for scanner.Scan() {
  310. switch {
  311. case strings.HasPrefix(scanner.Text(), "new file"):
  312. curFile.Type = DIFF_FILE_ADD
  313. case strings.HasPrefix(scanner.Text(), "deleted"):
  314. curFile.Type = DIFF_FILE_DEL
  315. case strings.HasPrefix(scanner.Text(), "index"):
  316. curFile.Type = DIFF_FILE_CHANGE
  317. }
  318. if curFile.Type > 0 {
  319. break
  320. }
  321. }
  322. }
  323. }
  324. return diff, nil
  325. }
  326. func GetDiff(repoPath, commitid string) (*Diff, error) {
  327. repo, err := git.OpenRepository(repoPath)
  328. if err != nil {
  329. return nil, err
  330. }
  331. commit, err := repo.GetCommit(commitid)
  332. if err != nil {
  333. return nil, err
  334. }
  335. // First commit of repository.
  336. if commit.ParentCount() == 0 {
  337. rd, wr := io.Pipe()
  338. go func() {
  339. cmd := exec.Command("git", "show", commitid)
  340. cmd.Dir = repoPath
  341. cmd.Stdout = wr
  342. cmd.Stdin = os.Stdin
  343. cmd.Stderr = os.Stderr
  344. cmd.Run()
  345. wr.Close()
  346. }()
  347. defer rd.Close()
  348. return ParsePatch(rd)
  349. }
  350. rd, wr := io.Pipe()
  351. go func() {
  352. cmd := exec.Command("git", "diff", commit.Parent(0).Oid.String(), commitid)
  353. cmd.Dir = repoPath
  354. cmd.Stdout = wr
  355. cmd.Stdin = os.Stdin
  356. cmd.Stderr = os.Stderr
  357. cmd.Run()
  358. wr.Close()
  359. }()
  360. defer rd.Close()
  361. return ParsePatch(rd)
  362. }
  363. const prettyLogFormat = `--pretty=format:%H%n%an <%ae> %at%n%s`
  364. func parsePrettyFormatLog(logByts []byte) (*list.List, error) {
  365. l := list.New()
  366. buf := bytes.NewBuffer(logByts)
  367. if buf.Len() == 0 {
  368. return l, nil
  369. }
  370. idx := 0
  371. var commit *git.Commit
  372. for {
  373. line, err := buf.ReadString('\n')
  374. if err != nil && err != io.EOF {
  375. return nil, err
  376. }
  377. line = strings.TrimSpace(line)
  378. // fmt.Println(line)
  379. var parseErr error
  380. switch idx {
  381. case 0: // SHA1.
  382. commit = &git.Commit{}
  383. commit.Oid, parseErr = git.NewOidFromString(line)
  384. case 1: // Signature.
  385. commit.Author, parseErr = git.NewSignatureFromCommitline([]byte(line + " "))
  386. case 2: // Commit message.
  387. commit.CommitMessage = line
  388. l.PushBack(commit)
  389. idx = -1
  390. }
  391. if parseErr != nil {
  392. return nil, parseErr
  393. }
  394. idx++
  395. if err == io.EOF {
  396. break
  397. }
  398. }
  399. return l, nil
  400. }
  401. // SearchCommits searches commits in given branch and keyword of repository.
  402. func SearchCommits(repoPath, branch, keyword string) (*list.List, error) {
  403. stdout, stderr, err := com.ExecCmdDirBytes(repoPath, "git", "log", branch, "-100",
  404. "-i", "--grep="+keyword, prettyLogFormat)
  405. if err != nil {
  406. return nil, err
  407. } else if len(stderr) > 0 {
  408. return nil, errors.New(string(stderr))
  409. }
  410. return parsePrettyFormatLog(stdout)
  411. }
  412. // GetCommitsByRange returns certain number of commits with given page of repository.
  413. func GetCommitsByRange(repoPath, branch string, page int) (*list.List, error) {
  414. stdout, stderr, err := com.ExecCmdDirBytes(repoPath, "git", "log", branch,
  415. "--skip="+base.ToStr((page-1)*50), "--max-count=50", prettyLogFormat)
  416. if err != nil {
  417. return nil, err
  418. } else if len(stderr) > 0 {
  419. return nil, errors.New(string(stderr))
  420. }
  421. return parsePrettyFormatLog(stdout)
  422. }
  423. // GetCommitsCount returns the commits count of given branch of repository.
  424. func GetCommitsCount(repoPath, branch string) (int, error) {
  425. stdout, stderr, err := com.ExecCmdDir(repoPath, "git", "rev-list", "--count", branch)
  426. if err != nil {
  427. return 0, err
  428. } else if len(stderr) > 0 {
  429. return 0, errors.New(stderr)
  430. }
  431. return base.StrTo(strings.TrimSpace(stdout)).Int()
  432. }