fork of go-git with some jj specific features
at v0.1.0 9.1 kB view raw
1package git 2 3import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "path" 9 10 "github.com/go-git/go-billy/v5" 11 "github.com/go-git/go-git/v5/config" 12 "github.com/go-git/go-git/v5/plumbing" 13 "github.com/go-git/go-git/v5/plumbing/format/index" 14 "github.com/go-git/go-git/v5/plumbing/transport" 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 moduleEndpoint, err := transport.NewEndpoint(s.c.URL) 137 if err != nil { 138 return nil, err 139 } 140 141 if !path.IsAbs(moduleEndpoint.Path) && moduleEndpoint.Protocol == "file" { 142 remotes, err := s.w.r.Remotes() 143 if err != nil { 144 return nil, err 145 } 146 147 rootEndpoint, err := transport.NewEndpoint(remotes[0].c.URLs[0]) 148 if err != nil { 149 return nil, err 150 } 151 152 rootEndpoint.Path = path.Join(rootEndpoint.Path, moduleEndpoint.Path) 153 *moduleEndpoint = *rootEndpoint 154 } 155 156 _, err = r.CreateRemote(&config.RemoteConfig{ 157 Name: DefaultRemoteName, 158 URLs: []string{moduleEndpoint.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(ctx, r, o) 218} 219 220func (s *Submodule) doRecursiveUpdate(ctx context.Context, 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.UpdateContext(ctx, 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, Depth: o.Depth}) 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 Depth: o.Depth, 269 }) 270 if err != nil && err != NoErrAlreadyUpToDate && err != ErrExactSHA1NotSupported { 271 return err 272 } 273 } 274 } 275 276 if err := w.Checkout(&CheckoutOptions{Hash: hash}); err != nil { 277 return err 278 } 279 280 head := plumbing.NewHashReference(plumbing.HEAD, hash) 281 return r.Storer.SetReference(head) 282} 283 284// Submodules list of several submodules from the same repository. 285type Submodules []*Submodule 286 287// Init initializes the submodules in this list. 288func (s Submodules) Init() error { 289 for _, sub := range s { 290 if err := sub.Init(); err != nil { 291 return err 292 } 293 } 294 295 return nil 296} 297 298// Update updates all the submodules in this list. 299func (s Submodules) Update(o *SubmoduleUpdateOptions) error { 300 return s.UpdateContext(context.Background(), o) 301} 302 303// UpdateContext updates all the submodules in this list. 304// 305// The provided Context must be non-nil. If the context expires before the 306// operation is complete, an error is returned. The context only affects the 307// transport operations. 308func (s Submodules) UpdateContext(ctx context.Context, o *SubmoduleUpdateOptions) error { 309 for _, sub := range s { 310 if err := sub.UpdateContext(ctx, o); err != nil { 311 return err 312 } 313 } 314 315 return nil 316} 317 318// Status returns the status of the submodules. 319func (s Submodules) Status() (SubmodulesStatus, error) { 320 var list SubmodulesStatus 321 322 var r *Repository 323 for _, sub := range s { 324 if r == nil { 325 r = sub.w.r 326 } 327 328 idx, err := r.Storer.Index() 329 if err != nil { 330 return nil, err 331 } 332 333 status, err := sub.status(idx) 334 if err != nil { 335 return nil, err 336 } 337 338 list = append(list, status) 339 } 340 341 return list, nil 342} 343 344// SubmodulesStatus contains the status for all submodiles in the worktree 345type SubmodulesStatus []*SubmoduleStatus 346 347// String is equivalent to `git submodule status` 348func (s SubmodulesStatus) String() string { 349 buf := bytes.NewBuffer(nil) 350 for _, sub := range s { 351 fmt.Fprintln(buf, sub) 352 } 353 354 return buf.String() 355} 356 357// SubmoduleStatus contains the status for a submodule in the worktree 358type SubmoduleStatus struct { 359 Path string 360 Current plumbing.Hash 361 Expected plumbing.Hash 362 Branch plumbing.ReferenceName 363} 364 365// IsClean is the HEAD of the submodule is equals to the expected commit 366func (s *SubmoduleStatus) IsClean() bool { 367 return s.Current == s.Expected 368} 369 370// String is equivalent to `git submodule status <submodule>` 371// 372// This will print the SHA-1 of the currently checked out commit for a 373// submodule, along with the submodule path and the output of git describe fo 374// the SHA-1. Each SHA-1 will be prefixed with - if the submodule is not 375// initialized, + if the currently checked out submodule commit does not match 376// the SHA-1 found in the index of the containing repository. 377func (s *SubmoduleStatus) String() string { 378 var extra string 379 var status = ' ' 380 381 if s.Current.IsZero() { 382 status = '-' 383 } else if !s.IsClean() { 384 status = '+' 385 } 386 387 if len(s.Branch) != 0 { 388 extra = string(s.Branch[5:]) 389 } else if !s.Current.IsZero() { 390 extra = s.Current.String()[:7] 391 } 392 393 if extra != "" { 394 extra = fmt.Sprintf(" (%s)", extra) 395 } 396 397 return fmt.Sprintf("%c%s %s%s", status, s.Expected, s.Path, extra) 398}