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