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} 398