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.

357 lines
7.7 KiB

  1. package git
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "gopkg.in/src-d/go-billy.v4"
  8. "gopkg.in/src-d/go-git.v4/config"
  9. "gopkg.in/src-d/go-git.v4/plumbing"
  10. "gopkg.in/src-d/go-git.v4/plumbing/format/index"
  11. )
  12. var (
  13. ErrSubmoduleAlreadyInitialized = errors.New("submodule already initialized")
  14. ErrSubmoduleNotInitialized = errors.New("submodule not initialized")
  15. )
  16. // Submodule a submodule allows you to keep another Git repository in a
  17. // subdirectory of your repository.
  18. type Submodule struct {
  19. // initialized defines if a submodule was already initialized.
  20. initialized bool
  21. c *config.Submodule
  22. w *Worktree
  23. }
  24. // Config returns the submodule config
  25. func (s *Submodule) Config() *config.Submodule {
  26. return s.c
  27. }
  28. // Init initialize the submodule reading the recorded Entry in the index for
  29. // the given submodule
  30. func (s *Submodule) Init() error {
  31. cfg, err := s.w.r.Storer.Config()
  32. if err != nil {
  33. return err
  34. }
  35. _, ok := cfg.Submodules[s.c.Name]
  36. if ok {
  37. return ErrSubmoduleAlreadyInitialized
  38. }
  39. s.initialized = true
  40. cfg.Submodules[s.c.Name] = s.c
  41. return s.w.r.Storer.SetConfig(cfg)
  42. }
  43. // Status returns the status of the submodule.
  44. func (s *Submodule) Status() (*SubmoduleStatus, error) {
  45. idx, err := s.w.r.Storer.Index()
  46. if err != nil {
  47. return nil, err
  48. }
  49. return s.status(idx)
  50. }
  51. func (s *Submodule) status(idx *index.Index) (*SubmoduleStatus, error) {
  52. status := &SubmoduleStatus{
  53. Path: s.c.Path,
  54. }
  55. e, err := idx.Entry(s.c.Path)
  56. if err != nil && err != index.ErrEntryNotFound {
  57. return nil, err
  58. }
  59. if e != nil {
  60. status.Expected = e.Hash
  61. }
  62. if !s.initialized {
  63. return status, nil
  64. }
  65. r, err := s.Repository()
  66. if err != nil {
  67. return nil, err
  68. }
  69. head, err := r.Head()
  70. if err == nil {
  71. status.Current = head.Hash()
  72. }
  73. if err != nil && err == plumbing.ErrReferenceNotFound {
  74. err = nil
  75. }
  76. return status, err
  77. }
  78. // Repository returns the Repository represented by this submodule
  79. func (s *Submodule) Repository() (*Repository, error) {
  80. if !s.initialized {
  81. return nil, ErrSubmoduleNotInitialized
  82. }
  83. storer, err := s.w.r.Storer.Module(s.c.Name)
  84. if err != nil {
  85. return nil, err
  86. }
  87. _, err = storer.Reference(plumbing.HEAD)
  88. if err != nil && err != plumbing.ErrReferenceNotFound {
  89. return nil, err
  90. }
  91. var exists bool
  92. if err == nil {
  93. exists = true
  94. }
  95. var worktree billy.Filesystem
  96. if worktree, err = s.w.Filesystem.Chroot(s.c.Path); err != nil {
  97. return nil, err
  98. }
  99. if exists {
  100. return Open(storer, worktree)
  101. }
  102. r, err := Init(storer, worktree)
  103. if err != nil {
  104. return nil, err
  105. }
  106. _, err = r.CreateRemote(&config.RemoteConfig{
  107. Name: DefaultRemoteName,
  108. URLs: []string{s.c.URL},
  109. })
  110. return r, err
  111. }
  112. // Update the registered submodule to match what the superproject expects, the
  113. // submodule should be initialized first calling the Init method or setting in
  114. // the options SubmoduleUpdateOptions.Init equals true
  115. func (s *Submodule) Update(o *SubmoduleUpdateOptions) error {
  116. return s.UpdateContext(context.Background(), o)
  117. }
  118. // UpdateContext the registered submodule to match what the superproject
  119. // expects, the submodule should be initialized first calling the Init method or
  120. // setting in the options SubmoduleUpdateOptions.Init equals true.
  121. //
  122. // The provided Context must be non-nil. If the context expires before the
  123. // operation is complete, an error is returned. The context only affects to the
  124. // transport operations.
  125. func (s *Submodule) UpdateContext(ctx context.Context, o *SubmoduleUpdateOptions) error {
  126. return s.update(ctx, o, plumbing.ZeroHash)
  127. }
  128. func (s *Submodule) update(ctx context.Context, o *SubmoduleUpdateOptions, forceHash plumbing.Hash) error {
  129. if !s.initialized && !o.Init {
  130. return ErrSubmoduleNotInitialized
  131. }
  132. if !s.initialized && o.Init {
  133. if err := s.Init(); err != nil {
  134. return err
  135. }
  136. }
  137. idx, err := s.w.r.Storer.Index()
  138. if err != nil {
  139. return err
  140. }
  141. hash := forceHash
  142. if hash.IsZero() {
  143. e, err := idx.Entry(s.c.Path)
  144. if err != nil {
  145. return err
  146. }
  147. hash = e.Hash
  148. }
  149. r, err := s.Repository()
  150. if err != nil {
  151. return err
  152. }
  153. if err := s.fetchAndCheckout(ctx, r, o, hash); err != nil {
  154. return err
  155. }
  156. return s.doRecursiveUpdate(r, o)
  157. }
  158. func (s *Submodule) doRecursiveUpdate(r *Repository, o *SubmoduleUpdateOptions) error {
  159. if o.RecurseSubmodules == NoRecurseSubmodules {
  160. return nil
  161. }
  162. w, err := r.Worktree()
  163. if err != nil {
  164. return err
  165. }
  166. l, err := w.Submodules()
  167. if err != nil {
  168. return err
  169. }
  170. new := &SubmoduleUpdateOptions{}
  171. *new = *o
  172. new.RecurseSubmodules--
  173. return l.Update(new)
  174. }
  175. func (s *Submodule) fetchAndCheckout(
  176. ctx context.Context, r *Repository, o *SubmoduleUpdateOptions, hash plumbing.Hash,
  177. ) error {
  178. if !o.NoFetch {
  179. err := r.FetchContext(ctx, &FetchOptions{Auth: o.Auth})
  180. if err != nil && err != NoErrAlreadyUpToDate {
  181. return err
  182. }
  183. }
  184. w, err := r.Worktree()
  185. if err != nil {
  186. return err
  187. }
  188. if err := w.Checkout(&CheckoutOptions{Hash: hash}); err != nil {
  189. return err
  190. }
  191. head := plumbing.NewHashReference(plumbing.HEAD, hash)
  192. return r.Storer.SetReference(head)
  193. }
  194. // Submodules list of several submodules from the same repository.
  195. type Submodules []*Submodule
  196. // Init initializes the submodules in this list.
  197. func (s Submodules) Init() error {
  198. for _, sub := range s {
  199. if err := sub.Init(); err != nil {
  200. return err
  201. }
  202. }
  203. return nil
  204. }
  205. // Update updates all the submodules in this list.
  206. func (s Submodules) Update(o *SubmoduleUpdateOptions) error {
  207. return s.UpdateContext(context.Background(), o)
  208. }
  209. // UpdateContext updates all the submodules in this list.
  210. //
  211. // The provided Context must be non-nil. If the context expires before the
  212. // operation is complete, an error is returned. The context only affects to the
  213. // transport operations.
  214. func (s Submodules) UpdateContext(ctx context.Context, o *SubmoduleUpdateOptions) error {
  215. for _, sub := range s {
  216. if err := sub.UpdateContext(ctx, o); err != nil {
  217. return err
  218. }
  219. }
  220. return nil
  221. }
  222. // Status returns the status of the submodules.
  223. func (s Submodules) Status() (SubmodulesStatus, error) {
  224. var list SubmodulesStatus
  225. var r *Repository
  226. for _, sub := range s {
  227. if r == nil {
  228. r = sub.w.r
  229. }
  230. idx, err := r.Storer.Index()
  231. if err != nil {
  232. return nil, err
  233. }
  234. status, err := sub.status(idx)
  235. if err != nil {
  236. return nil, err
  237. }
  238. list = append(list, status)
  239. }
  240. return list, nil
  241. }
  242. // SubmodulesStatus contains the status for all submodiles in the worktree
  243. type SubmodulesStatus []*SubmoduleStatus
  244. // String is equivalent to `git submodule status`
  245. func (s SubmodulesStatus) String() string {
  246. buf := bytes.NewBuffer(nil)
  247. for _, sub := range s {
  248. fmt.Fprintln(buf, sub)
  249. }
  250. return buf.String()
  251. }
  252. // SubmoduleStatus contains the status for a submodule in the worktree
  253. type SubmoduleStatus struct {
  254. Path string
  255. Current plumbing.Hash
  256. Expected plumbing.Hash
  257. Branch plumbing.ReferenceName
  258. }
  259. // IsClean is the HEAD of the submodule is equals to the expected commit
  260. func (s *SubmoduleStatus) IsClean() bool {
  261. return s.Current == s.Expected
  262. }
  263. // String is equivalent to `git submodule status <submodule>`
  264. //
  265. // This will print the SHA-1 of the currently checked out commit for a
  266. // submodule, along with the submodule path and the output of git describe fo
  267. // the SHA-1. Each SHA-1 will be prefixed with - if the submodule is not
  268. // initialized, + if the currently checked out submodule commit does not match
  269. // the SHA-1 found in the index of the containing repository.
  270. func (s *SubmoduleStatus) String() string {
  271. var extra string
  272. var status = ' '
  273. if s.Current.IsZero() {
  274. status = '-'
  275. } else if !s.IsClean() {
  276. status = '+'
  277. }
  278. if len(s.Branch) != 0 {
  279. extra = string(s.Branch[5:])
  280. } else if !s.Current.IsZero() {
  281. extra = s.Current.String()[:7]
  282. }
  283. if extra != "" {
  284. extra = fmt.Sprintf(" (%s)", extra)
  285. }
  286. return fmt.Sprintf("%c%s %s%s", status, s.Expected, s.Path, extra)
  287. }