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