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