1package git
2
3import (
4	"bytes"
5	"context"
6	"errors"
7	"fmt"
8
9	"github.com/go-git/go-billy/v5"
10	"github.com/jesseduffield/go-git/v5/config"
11	"github.com/jesseduffield/go-git/v5/plumbing"
12	"github.com/jesseduffield/go-git/v5/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.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}
358