| package git |
| |
| import ( |
| "bytes" |
| "context" |
| "errors" |
| "fmt" |
| |
| "gopkg.in/src-d/go-billy.v4" |
| "gopkg.in/src-d/go-git.v4/config" |
| "gopkg.in/src-d/go-git.v4/plumbing" |
| "gopkg.in/src-d/go-git.v4/plumbing/format/index" |
| ) |
| |
| var ( |
| ErrSubmoduleAlreadyInitialized = errors.New("submodule already initialized") |
| ErrSubmoduleNotInitialized = errors.New("submodule not initialized") |
| ) |
| |
| // Submodule a submodule allows you to keep another Git repository in a |
| // subdirectory of your repository. |
| type Submodule struct { |
| // initialized defines if a submodule was already initialized. |
| initialized bool |
| |
| c *config.Submodule |
| w *Worktree |
| } |
| |
| // Config returns the submodule config |
| func (s *Submodule) Config() *config.Submodule { |
| return s.c |
| } |
| |
| // Init initialize the submodule reading the recorded Entry in the index for |
| // the given submodule |
| func (s *Submodule) Init() error { |
| cfg, err := s.w.r.Storer.Config() |
| if err != nil { |
| return err |
| } |
| |
| _, ok := cfg.Submodules[s.c.Name] |
| if ok { |
| return ErrSubmoduleAlreadyInitialized |
| } |
| |
| s.initialized = true |
| |
| cfg.Submodules[s.c.Name] = s.c |
| return s.w.r.Storer.SetConfig(cfg) |
| } |
| |
| // Status returns the status of the submodule. |
| func (s *Submodule) Status() (*SubmoduleStatus, error) { |
| idx, err := s.w.r.Storer.Index() |
| if err != nil { |
| return nil, err |
| } |
| |
| return s.status(idx) |
| } |
| |
| func (s *Submodule) status(idx *index.Index) (*SubmoduleStatus, error) { |
| status := &SubmoduleStatus{ |
| Path: s.c.Path, |
| } |
| |
| e, err := idx.Entry(s.c.Path) |
| if err != nil && err != index.ErrEntryNotFound { |
| return nil, err |
| } |
| |
| if e != nil { |
| status.Expected = e.Hash |
| } |
| |
| if !s.initialized { |
| return status, nil |
| } |
| |
| r, err := s.Repository() |
| if err != nil { |
| return nil, err |
| } |
| |
| head, err := r.Head() |
| if err == nil { |
| status.Current = head.Hash() |
| } |
| |
| if err != nil && err == plumbing.ErrReferenceNotFound { |
| err = nil |
| } |
| |
| return status, err |
| } |
| |
| // Repository returns the Repository represented by this submodule |
| func (s *Submodule) Repository() (*Repository, error) { |
| if !s.initialized { |
| return nil, ErrSubmoduleNotInitialized |
| } |
| |
| storer, err := s.w.r.Storer.Module(s.c.Name) |
| if err != nil { |
| return nil, err |
| } |
| |
| _, err = storer.Reference(plumbing.HEAD) |
| if err != nil && err != plumbing.ErrReferenceNotFound { |
| return nil, err |
| } |
| |
| var exists bool |
| if err == nil { |
| exists = true |
| } |
| |
| var worktree billy.Filesystem |
| if worktree, err = s.w.Filesystem.Chroot(s.c.Path); err != nil { |
| return nil, err |
| } |
| |
| if exists { |
| return Open(storer, worktree) |
| } |
| |
| r, err := Init(storer, worktree) |
| if err != nil { |
| return nil, err |
| } |
| |
| _, err = r.CreateRemote(&config.RemoteConfig{ |
| Name: DefaultRemoteName, |
| URLs: []string{s.c.URL}, |
| }) |
| |
| return r, err |
| } |
| |
| // Update the registered submodule to match what the superproject expects, the |
| // submodule should be initialized first calling the Init method or setting in |
| // the options SubmoduleUpdateOptions.Init equals true |
| func (s *Submodule) Update(o *SubmoduleUpdateOptions) error { |
| return s.UpdateContext(context.Background(), o) |
| } |
| |
| // UpdateContext the registered submodule to match what the superproject |
| // expects, the submodule should be initialized first calling the Init method or |
| // setting in the options SubmoduleUpdateOptions.Init equals true. |
| // |
| // The provided Context must be non-nil. If the context expires before the |
| // operation is complete, an error is returned. The context only affects to the |
| // transport operations. |
| func (s *Submodule) UpdateContext(ctx context.Context, o *SubmoduleUpdateOptions) error { |
| return s.update(ctx, o, plumbing.ZeroHash) |
| } |
| |
| func (s *Submodule) update(ctx context.Context, o *SubmoduleUpdateOptions, forceHash plumbing.Hash) error { |
| if !s.initialized && !o.Init { |
| return ErrSubmoduleNotInitialized |
| } |
| |
| if !s.initialized && o.Init { |
| if err := s.Init(); err != nil { |
| return err |
| } |
| } |
| |
| idx, err := s.w.r.Storer.Index() |
| if err != nil { |
| return err |
| } |
| |
| hash := forceHash |
| if hash.IsZero() { |
| e, err := idx.Entry(s.c.Path) |
| if err != nil { |
| return err |
| } |
| |
| hash = e.Hash |
| } |
| |
| r, err := s.Repository() |
| if err != nil { |
| return err |
| } |
| |
| if err := s.fetchAndCheckout(ctx, r, o, hash); err != nil { |
| return err |
| } |
| |
| return s.doRecursiveUpdate(r, o) |
| } |
| |
| func (s *Submodule) doRecursiveUpdate(r *Repository, o *SubmoduleUpdateOptions) error { |
| if o.RecurseSubmodules == NoRecurseSubmodules { |
| return nil |
| } |
| |
| w, err := r.Worktree() |
| if err != nil { |
| return err |
| } |
| |
| l, err := w.Submodules() |
| if err != nil { |
| return err |
| } |
| |
| new := &SubmoduleUpdateOptions{} |
| *new = *o |
| |
| new.RecurseSubmodules-- |
| return l.Update(new) |
| } |
| |
| func (s *Submodule) fetchAndCheckout( |
| ctx context.Context, r *Repository, o *SubmoduleUpdateOptions, hash plumbing.Hash, |
| ) error { |
| if !o.NoFetch { |
| err := r.FetchContext(ctx, &FetchOptions{Auth: o.Auth}) |
| if err != nil && err != NoErrAlreadyUpToDate { |
| return err |
| } |
| } |
| |
| w, err := r.Worktree() |
| if err != nil { |
| return err |
| } |
| |
| if err := w.Checkout(&CheckoutOptions{Hash: hash}); err != nil { |
| return err |
| } |
| |
| head := plumbing.NewHashReference(plumbing.HEAD, hash) |
| return r.Storer.SetReference(head) |
| } |
| |
| // Submodules list of several submodules from the same repository. |
| type Submodules []*Submodule |
| |
| // Init initializes the submodules in this list. |
| func (s Submodules) Init() error { |
| for _, sub := range s { |
| if err := sub.Init(); err != nil { |
| return err |
| } |
| } |
| |
| return nil |
| } |
| |
| // Update updates all the submodules in this list. |
| func (s Submodules) Update(o *SubmoduleUpdateOptions) error { |
| return s.UpdateContext(context.Background(), o) |
| } |
| |
| // UpdateContext updates all the submodules in this list. |
| // |
| // The provided Context must be non-nil. If the context expires before the |
| // operation is complete, an error is returned. The context only affects to the |
| // transport operations. |
| func (s Submodules) UpdateContext(ctx context.Context, o *SubmoduleUpdateOptions) error { |
| for _, sub := range s { |
| if err := sub.UpdateContext(ctx, o); err != nil { |
| return err |
| } |
| } |
| |
| return nil |
| } |
| |
| // Status returns the status of the submodules. |
| func (s Submodules) Status() (SubmodulesStatus, error) { |
| var list SubmodulesStatus |
| |
| var r *Repository |
| for _, sub := range s { |
| if r == nil { |
| r = sub.w.r |
| } |
| |
| idx, err := r.Storer.Index() |
| if err != nil { |
| return nil, err |
| } |
| |
| status, err := sub.status(idx) |
| if err != nil { |
| return nil, err |
| } |
| |
| list = append(list, status) |
| } |
| |
| return list, nil |
| } |
| |
| // SubmodulesStatus contains the status for all submodiles in the worktree |
| type SubmodulesStatus []*SubmoduleStatus |
| |
| // String is equivalent to `git submodule status` |
| func (s SubmodulesStatus) String() string { |
| buf := bytes.NewBuffer(nil) |
| for _, sub := range s { |
| fmt.Fprintln(buf, sub) |
| } |
| |
| return buf.String() |
| } |
| |
| // SubmoduleStatus contains the status for a submodule in the worktree |
| type SubmoduleStatus struct { |
| Path string |
| Current plumbing.Hash |
| Expected plumbing.Hash |
| Branch plumbing.ReferenceName |
| } |
| |
| // IsClean is the HEAD of the submodule is equals to the expected commit |
| func (s *SubmoduleStatus) IsClean() bool { |
| return s.Current == s.Expected |
| } |
| |
| // String is equivalent to `git submodule status <submodule>` |
| // |
| // This will print the SHA-1 of the currently checked out commit for a |
| // submodule, along with the submodule path and the output of git describe fo |
| // the SHA-1. Each SHA-1 will be prefixed with - if the submodule is not |
| // initialized, + if the currently checked out submodule commit does not match |
| // the SHA-1 found in the index of the containing repository. |
| func (s *SubmoduleStatus) String() string { |
| var extra string |
| var status = ' ' |
| |
| if s.Current.IsZero() { |
| status = '-' |
| } else if !s.IsClean() { |
| status = '+' |
| } |
| |
| if len(s.Branch) != 0 { |
| extra = string(s.Branch[5:]) |
| } else if !s.Current.IsZero() { |
| extra = s.Current.String()[:7] |
| } |
| |
| if extra != "" { |
| extra = fmt.Sprintf(" (%s)", extra) |
| } |
| |
| return fmt.Sprintf("%c%s %s%s", status, s.Expected, s.Path, extra) |
| } |