history.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. package git
  2. /*
  3. history.go
  4. Read-only history queries plus branch management: the data behind the
  5. History tab and the branch switcher in the GitApp toolbar.
  6. */
  7. import (
  8. "errors"
  9. "sort"
  10. "strings"
  11. gogit "github.com/go-git/go-git/v5"
  12. "github.com/go-git/go-git/v5/plumbing"
  13. "github.com/go-git/go-git/v5/plumbing/object"
  14. )
  15. // defaultLogLimit bounds Log when the caller passes a non-positive limit.
  16. const defaultLogLimit = 50
  17. // Log returns up to limit commits reachable from HEAD, newest first.
  18. func (m *Manager) Log(realpath string, limit int) ([]CommitInfo, error) {
  19. if limit <= 0 {
  20. limit = defaultLogLimit
  21. }
  22. repo, err := m.open(realpath)
  23. if err != nil {
  24. return nil, err
  25. }
  26. head, err := repo.Head()
  27. if err != nil {
  28. if errors.Is(err, plumbing.ErrReferenceNotFound) {
  29. //An unborn branch simply has no history yet
  30. return []CommitInfo{}, nil
  31. }
  32. return nil, err
  33. }
  34. iter, err := repo.Log(&gogit.LogOptions{From: head.Hash()})
  35. if err != nil {
  36. return nil, err
  37. }
  38. defer iter.Close()
  39. //Index tags once so each commit can be labelled without a per-commit lookup
  40. tagMap, _ := buildTagMap(repo)
  41. commits := []CommitInfo{}
  42. err = iter.ForEach(func(commit *object.Commit) error {
  43. info := commitToInfo(commit)
  44. if tags, ok := tagMap[commit.Hash]; ok {
  45. info.Tags = tags
  46. }
  47. commits = append(commits, *info)
  48. if len(commits) >= limit {
  49. return storerStop
  50. }
  51. return nil
  52. })
  53. if err != nil && !errors.Is(err, storerStop) {
  54. return nil, err
  55. }
  56. return commits, nil
  57. }
  58. // Branches lists local and remote-tracking branches. The current branch is
  59. // flagged so the switcher can render it as selected.
  60. func (m *Manager) Branches(realpath string) ([]BranchInfo, error) {
  61. repo, err := m.open(realpath)
  62. if err != nil {
  63. return nil, err
  64. }
  65. currentBranch := ""
  66. if head, herr := repo.Head(); herr == nil && head.Name().IsBranch() {
  67. currentBranch = head.Name().Short()
  68. }
  69. branches := []BranchInfo{}
  70. localIter, err := repo.Branches()
  71. if err != nil {
  72. return nil, err
  73. }
  74. err = localIter.ForEach(func(ref *plumbing.Reference) error {
  75. branches = append(branches, BranchInfo{
  76. Name: ref.Name().Short(),
  77. FullRef: ref.Name().String(),
  78. Hash: ref.Hash().String(),
  79. IsRemote: false,
  80. IsCurrent: ref.Name().Short() == currentBranch,
  81. //Short mirrors Name for a local branch so callers can always use it
  82. Short: ref.Name().Short(),
  83. })
  84. return nil
  85. })
  86. localIter.Close()
  87. if err != nil {
  88. return nil, err
  89. }
  90. remoteIter, err := repo.References()
  91. if err != nil {
  92. return nil, err
  93. }
  94. err = remoteIter.ForEach(func(ref *plumbing.Reference) error {
  95. if !ref.Name().IsRemote() || ref.Type() == plumbing.SymbolicReference {
  96. return nil
  97. }
  98. //HEAD pointers such as refs/remotes/origin/HEAD are not branches
  99. if strings.HasSuffix(ref.Name().Short(), "/HEAD") {
  100. return nil
  101. }
  102. remote, short := splitRemoteRef(ref.Name().String())
  103. branches = append(branches, BranchInfo{
  104. Name: ref.Name().Short(),
  105. FullRef: ref.Name().String(),
  106. Hash: ref.Hash().String(),
  107. IsRemote: true,
  108. Remote: remote,
  109. Short: short,
  110. })
  111. return nil
  112. })
  113. remoteIter.Close()
  114. if err != nil {
  115. return nil, err
  116. }
  117. sort.Slice(branches, func(i, j int) bool {
  118. if branches[i].IsRemote != branches[j].IsRemote {
  119. return !branches[i].IsRemote
  120. }
  121. return branches[i].Name < branches[j].Name
  122. })
  123. return branches, nil
  124. }
  125. // Checkout switches to an existing branch, or creates it from the current HEAD
  126. // when create is set.
  127. func (m *Manager) Checkout(realpath string, branch string, create bool) error {
  128. branch = strings.TrimSpace(branch)
  129. if branch == "" {
  130. return errors.New("branch name cannot be empty")
  131. }
  132. if err := validateBranchName(branch); err != nil {
  133. return err
  134. }
  135. repo, tree, err := m.worktree(realpath)
  136. if err != nil {
  137. return err
  138. }
  139. options := &gogit.CheckoutOptions{
  140. Branch: plumbing.NewBranchReferenceName(branch),
  141. Create: create,
  142. Keep: true,
  143. }
  144. //Checking out a remote-tracking branch by its "origin/x" name creates the
  145. //matching local branch, which is what a user picking it from the dropdown
  146. //expects.
  147. if !create {
  148. if _, rerr := repo.Reference(plumbing.NewBranchReferenceName(branch), true); rerr != nil {
  149. if remoteRef, terr := findRemoteBranch(repo, branch); terr == nil {
  150. options.Branch = plumbing.NewBranchReferenceName(shortLocalName(branch))
  151. options.Create = true
  152. options.Hash = remoteRef.Hash()
  153. }
  154. }
  155. }
  156. return tree.Checkout(options)
  157. }
  158. // findRemoteBranch resolves a "remote/branch" style name to its reference.
  159. func findRemoteBranch(repo *gogit.Repository, name string) (*plumbing.Reference, error) {
  160. parts := strings.SplitN(name, "/", 2)
  161. if len(parts) != 2 {
  162. return nil, errors.New("not a remote branch name")
  163. }
  164. return repo.Reference(plumbing.NewRemoteReferenceName(parts[0], parts[1]), true)
  165. }
  166. // shortLocalName strips the remote prefix so "origin/feature" becomes "feature".
  167. func shortLocalName(name string) string {
  168. parts := strings.SplitN(name, "/", 2)
  169. if len(parts) == 2 {
  170. return parts[1]
  171. }
  172. return name
  173. }
  174. // validateBranchName rejects the ref name characters git itself forbids, since
  175. // the value arrives from a browser text field.
  176. func validateBranchName(branch string) error {
  177. //An empty name must never reach a ref operation: it would build a malformed
  178. //ref such as "refs/heads/", which as a push refspec is silently accepted.
  179. if strings.TrimSpace(branch) == "" {
  180. return errors.New("branch name cannot be empty")
  181. }
  182. if strings.HasPrefix(branch, "-") || strings.HasPrefix(branch, "/") || strings.HasSuffix(branch, "/") {
  183. return errors.New("invalid branch name: " + branch)
  184. }
  185. if strings.Contains(branch, "..") || strings.Contains(branch, "@{") {
  186. return errors.New("invalid branch name: " + branch)
  187. }
  188. for _, forbidden := range []string{" ", "~", "^", ":", "?", "*", "[", "\\", "\x7f"} {
  189. if strings.Contains(branch, forbidden) {
  190. return errors.New("invalid branch name: " + branch)
  191. }
  192. }
  193. for _, r := range branch {
  194. if r < 0x20 {
  195. return errors.New("invalid branch name: " + branch)
  196. }
  197. }
  198. return nil
  199. }