1package git
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "net/url"
9 "path"
10
11 "github.com/go-git/go-billy/v5"
12 "github.com/go-git/go-git/v5/config"
13 "github.com/go-git/go-git/v5/plumbing"
14 "github.com/go-git/go-git/v5/plumbing/format/index"
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 moduleURL, err := url.Parse(s.c.URL)
137 if err != nil {
138 return nil, err
139 }
140
141 if !path.IsAbs(moduleURL.Path) {
142 remotes, err := s.w.r.Remotes()
143 if err != nil {
144 return nil, err
145 }
146
147 rootURL, err := url.Parse(remotes[0].c.URLs[0])
148 if err != nil {
149 return nil, err
150 }
151
152 rootURL.Path = path.Join(rootURL.Path, moduleURL.Path)
153 *moduleURL = *rootURL
154 }
155
156 _, err = r.CreateRemote(&config.RemoteConfig{
157 Name: DefaultRemoteName,
158 URLs: []string{moduleURL.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})
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 })
269 if err != nil && err != NoErrAlreadyUpToDate && err != ErrExactSHA1NotSupported {
270 return err
271 }
272 }
273 }
274
275 if err := w.Checkout(&CheckoutOptions{Hash: hash}); err != nil {
276 return err
277 }
278
279 head := plumbing.NewHashReference(plumbing.HEAD, hash)
280 return r.Storer.SetReference(head)
281}
282
283// Submodules list of several submodules from the same repository.
284type Submodules []*Submodule
285
286// Init initializes the submodules in this list.
287func (s Submodules) Init() error {
288 for _, sub := range s {
289 if err := sub.Init(); err != nil {
290 return err
291 }
292 }
293
294 return nil
295}
296
297// Update updates all the submodules in this list.
298func (s Submodules) Update(o *SubmoduleUpdateOptions) error {
299 return s.UpdateContext(context.Background(), o)
300}
301
302// UpdateContext updates all the submodules in this list.
303//
304// The provided Context must be non-nil. If the context expires before the
305// operation is complete, an error is returned. The context only affects the
306// transport operations.
307func (s Submodules) UpdateContext(ctx context.Context, o *SubmoduleUpdateOptions) error {
308 for _, sub := range s {
309 if err := sub.UpdateContext(ctx, o); err != nil {
310 return err
311 }
312 }
313
314 return nil
315}
316
317// Status returns the status of the submodules.
318func (s Submodules) Status() (SubmodulesStatus, error) {
319 var list SubmodulesStatus
320
321 var r *Repository
322 for _, sub := range s {
323 if r == nil {
324 r = sub.w.r
325 }
326
327 idx, err := r.Storer.Index()
328 if err != nil {
329 return nil, err
330 }
331
332 status, err := sub.status(idx)
333 if err != nil {
334 return nil, err
335 }
336
337 list = append(list, status)
338 }
339
340 return list, nil
341}
342
343// SubmodulesStatus contains the status for all submodiles in the worktree
344type SubmodulesStatus []*SubmoduleStatus
345
346// String is equivalent to `git submodule status`
347func (s SubmodulesStatus) String() string {
348 buf := bytes.NewBuffer(nil)
349 for _, sub := range s {
350 fmt.Fprintln(buf, sub)
351 }
352
353 return buf.String()
354}
355
356// SubmoduleStatus contains the status for a submodule in the worktree
357type SubmoduleStatus struct {
358 Path string
359 Current plumbing.Hash
360 Expected plumbing.Hash
361 Branch plumbing.ReferenceName
362}
363
364// IsClean is the HEAD of the submodule is equals to the expected commit
365func (s *SubmoduleStatus) IsClean() bool {
366 return s.Current == s.Expected
367}
368
369// String is equivalent to `git submodule status <submodule>`
370//
371// This will print the SHA-1 of the currently checked out commit for a
372// submodule, along with the submodule path and the output of git describe fo
373// the SHA-1. Each SHA-1 will be prefixed with - if the submodule is not
374// initialized, + if the currently checked out submodule commit does not match
375// the SHA-1 found in the index of the containing repository.
376func (s *SubmoduleStatus) String() string {
377 var extra string
378 var status = ' '
379
380 if s.Current.IsZero() {
381 status = '-'
382 } else if !s.IsClean() {
383 status = '+'
384 }
385
386 if len(s.Branch) != 0 {
387 extra = string(s.Branch[5:])
388 } else if !s.Current.IsZero() {
389 extra = s.Current.String()[:7]
390 }
391
392 if extra != "" {
393 extra = fmt.Sprintf(" (%s)", extra)
394 }
395
396 return fmt.Sprintf("%c%s %s%s", status, s.Expected, s.Path, extra)
397}