Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
357 lines
7.7 KiB
357 lines
7.7 KiB
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) |
|
}
|
|
|