github.com/nfisher/gitit@v0.0.7-0.20240131193748-bc8dd26542cc/cmd/exec.go (about) 1 package cmd 2 3 import ( 4 "fmt" 5 "github.com/go-git/go-git/v5" 6 "github.com/go-git/go-git/v5/config" 7 "github.com/go-git/go-git/v5/plumbing" 8 "github.com/go-git/go-git/v5/plumbing/transport" 9 "github.com/go-git/go-git/v5/plumbing/transport/ssh" 10 "github.com/go-git/go-git/v5/storage/memory" 11 "io" 12 "log" 13 "os" 14 "runtime/debug" 15 "sort" 16 "strconv" 17 "strings" 18 "text/template" 19 ) 20 21 type Flags struct { 22 SubCommand string 23 Name string 24 } 25 26 const ( 27 Success = iota 28 ErrHead 29 ErrMissingArguments 30 ErrMissingSubCommand 31 ErrInvalidArgument 32 ErrInvalidStack 33 ErrUnknownBranch 34 ErrNotRepository 35 ErrOutputWriter 36 ErrInvalidSequence 37 ErrCreatingBranch 38 ErrPushingStack 39 ) 40 41 const ( 42 stackName = 2 43 stackBranch = 3 44 ) 45 46 func Exec(input Flags, w io.Writer) int { 47 log.SetFlags(log.LstdFlags | log.Lshortfile) 48 49 switch input.SubCommand { 50 case "branch": 51 return Branch(input) 52 53 case "checkout": 54 return Checkout(input) 55 56 case "init": 57 return Init(input) 58 59 case "push": 60 return Push(input) 61 62 case "rebase": 63 return Rebase(input) 64 65 case "squash": 66 return Squash(input) 67 68 case "status": 69 return Status(input, w) 70 71 case "version": 72 return Version(w) 73 74 default: 75 usage(w) 76 return ErrMissingSubCommand 77 } 78 } 79 80 func Version(w io.Writer) int { 81 buildInfo, ok := debug.ReadBuildInfo() 82 if !ok { 83 os.Exit(1) 84 } 85 isDirty := false 86 rev := "devel" 87 for _, s := range buildInfo.Settings { 88 switch s.Key { 89 case "vcs.modified": 90 isDirty = s.Value == "true" 91 case "vcs.revision": 92 rev = s.Value 93 } 94 } 95 fmt.Fprintf(w, "gitit@%v isDirty=%v\n", rev, isDirty) 96 97 return Success 98 } 99 100 func Branch(input Flags) int { 101 if input.Name == "" { 102 log.Printf("call=Name err=`branch name is empty, must be specified`\n") 103 return ErrMissingArguments 104 } 105 106 repo, wt, err := openWorkTree() 107 if err != nil { 108 log.Printf("call=openWorkTree err=`%v`\n", err) 109 return ErrNotRepository 110 } 111 112 parts, err := headParts(repo) 113 if err != nil { 114 log.Printf("call=headParts err=`%v`\n", err) 115 return ErrHead 116 } 117 118 if !isStack(parts) { 119 log.Printf("call=Split err=`want 4 parts, got %d`\n", len(parts)) 120 return ErrInvalidStack 121 } 122 123 var a []string 124 fn := func(reference *plumbing.Reference) error { 125 p := splitRef(reference) 126 if len(p) == 4 && p[stackName] == parts[stackName] { 127 a = append(a, p[stackBranch]) 128 } 129 return nil 130 } 131 132 err = branchesApply(repo, fn) 133 if err != nil { 134 return ErrUnknownBranch 135 } 136 137 sort.Strings(a) 138 139 last := a[len(a)-1][:3] 140 i, err := strconv.Atoi(last) 141 if err != nil { 142 log.Printf("call=Atoi err=`%v`\n", err) 143 return ErrInvalidSequence 144 } 145 146 name := fmt.Sprintf("%s/%03d_%s", parts[stackName], i+1, input.Name) 147 148 err = wt.Checkout(&git.CheckoutOptions{ 149 Branch: plumbing.NewBranchReferenceName(name), 150 Create: true, 151 Keep: true, 152 }) 153 if err != nil { 154 log.Printf("call=Checkout err=`%v`\n", err) 155 return ErrCreatingBranch 156 } 157 158 fmt.Println("Created branch", name) 159 return Success 160 } 161 162 func branchesApply(repo *git.Repository, fn func(reference *plumbing.Reference) error) error { 163 iter, err := repo.Branches() 164 if err != nil { 165 return fmt.Errorf("call=Branches err=`%w`", err) 166 } 167 168 err = iter.ForEach(fn) 169 if err != nil { 170 return fmt.Errorf("call=ForEach err=`%w`", err) 171 } 172 173 return nil 174 } 175 176 func isStack(parts []string) bool { 177 return len(parts) == 4 178 } 179 180 func splitRef(reference *plumbing.Reference) []string { 181 s := reference.Name().String() 182 return strings.Split(s, "/") 183 } 184 185 func usage(w io.Writer) { 186 w.Write([]byte(`usage: git stack <command> [<name>] 187 188 These are common Stack commands used in various situations: 189 190 start a new stack 191 init Create a new stack 192 193 examine the stack state 194 status Show the stack status 195 196 grow, mark and tweak your stack 197 branch Create a new stack branch 198 checkout Switch branches within the stack using the index ID 199 200 collaborate 201 pull Fetch stack from and integrate with a local stack 202 push Update remote refs for stack along with associated objects 203 `)) 204 } 205 206 func Squash(_ Flags) int { 207 return Success 208 } 209 210 func Rebase(_ Flags) int { 211 return Success 212 } 213 214 func Push(_ Flags) int { 215 repo, _, err := openWorkTree() 216 if err != nil { 217 log.Printf("call=openWorkTree err=`%v`\n", err) 218 return ErrNotRepository 219 } 220 221 parts, err := headParts(repo) 222 if err != nil { 223 log.Printf("call=headParts err=`%v`\n", err) 224 return ErrHead 225 } 226 if !isStack(parts) { 227 log.Printf("call=Split err=`want 4 parts, got %d`\n", len(parts)) 228 return ErrInvalidStack 229 } 230 231 remotes, err := repo.Remotes() 232 if err != nil { 233 log.Printf("call=Remotes err=`%v`\n", err) 234 return ErrInvalidStack 235 } 236 237 if len(remotes) < 1 { 238 log.Printf("call=Split err=`want 4 parts, got %d`\n", len(parts)) 239 return ErrInvalidStack 240 } 241 242 var authcb transport.AuthMethod 243 u := remotes[0].Config().URLs[0] 244 if strings.HasPrefix(u, "http://") { 245 246 } else { 247 authcb, err = ssh.NewSSHAgentAuth("git") 248 if err != nil { 249 log.Printf("call=NewSSHAgentAuth err=`%v`\n", err) 250 return ErrInvalidStack 251 } 252 } 253 254 spec := config.RefSpec(fmt.Sprintf("refs/heads/%[1]s/*:refs/heads/%[1]s/*", parts[stackName])) 255 err = repo.Push(&git.PushOptions{ 256 Auth: authcb, 257 Progress: os.Stdout, 258 RemoteName: "origin", 259 RefSpecs: []config.RefSpec{spec}, 260 }) 261 if err != nil { 262 log.Printf("call=Push spec=%v err=`%v`\n", spec, err) 263 return ErrPushingStack 264 } 265 // TODO: Open PR's. 266 267 return Success 268 } 269 270 func Checkout(input Flags) int { 271 if input.Name == "" { 272 log.Printf("call=Checkout err=`branch name empty`\n") 273 return ErrMissingArguments 274 } 275 276 repo, wt, err := openWorkTree() 277 if err != nil { 278 return ErrNotRepository 279 } 280 281 parts, err := headParts(repo) 282 if err != nil { 283 return ErrHead 284 } 285 if !isStack(parts) { 286 log.Printf("call=Split err=`want 4 parts, got %d`\n", len(parts)) 287 return ErrInvalidStack 288 } 289 290 var target = "" 291 fn := func(reference *plumbing.Reference) error { 292 p := splitRef(reference) 293 if isCurrentStack(p, parts) && strings.HasPrefix(p[stackBranch], input.Name) { 294 target = strings.Join(p[stackName:], "/") 295 } 296 return nil 297 } 298 299 err = branchesApply(repo, fn) 300 if err != nil { 301 return ErrOutputWriter 302 } 303 304 if target == "" { 305 log.Printf("call=ForEach err=`%v not found`\n", input.Name) 306 return ErrUnknownBranch 307 } 308 309 err = wt.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName(target), Keep: true}) 310 if err != nil { 311 log.Printf("call=Checkout err=`%v`\n", err) 312 return ErrUnknownBranch 313 } 314 315 return Success 316 } 317 318 func Init(input Flags) int { 319 if input.Name == "" { 320 return ErrMissingArguments 321 } 322 323 _, wt, err := openWorkTree() 324 if err != nil { 325 return ErrNotRepository 326 } 327 328 parts := strings.Split(input.Name, "/") 329 if len(parts) != 2 { 330 log.Printf("call=Split err=`%v`\n", err) 331 return ErrInvalidArgument 332 } 333 name := fmt.Sprintf("%s/%03d_%s", parts[0], 1, parts[1]) 334 335 err = wt.Checkout(&git.CheckoutOptions{ 336 Branch: plumbing.NewBranchReferenceName(name), 337 Create: true, 338 Keep: true, 339 }) 340 if err != nil { 341 log.Printf("call=Checkout err=`%v`\n", err) 342 return ErrNotRepository 343 } 344 return Success 345 } 346 347 type Stack struct { 348 Branch string 349 Branches branches 350 Name string 351 Remote string 352 } 353 354 func Status(_ Flags, w io.Writer) int { 355 repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true}) 356 if err == git.ErrRepositoryNotExists { 357 log.Printf("call=PlainOpen err=`%v`\n", err) 358 return ErrNotRepository 359 } 360 361 parts, err := headParts(repo) 362 if err != nil { 363 return ErrHead 364 } 365 366 if isStack(parts) { 367 var defaultRemote *config.RemoteConfig 368 remotes, err := repo.Remotes() 369 if err != nil { 370 log.Printf("call=Remotes err=`%v`\n", err) 371 return ErrOutputWriter 372 } 373 var remoteShas = map[string]string{} 374 if len(remotes) > 0 { 375 defaultRemote = remotes[0].Config() 376 remote := git.NewRemote(memory.NewStorage(), defaultRemote) 377 refs, err := remote.List(&git.ListOptions{}) 378 if err != nil { 379 log.Printf("call=List err=`%v`\n", err) 380 return ErrOutputWriter 381 } 382 var prefix = strings.Join(parts[:3], "/") 383 for _, r := range refs { 384 s := r.Name().String() 385 if strings.HasPrefix(s, prefix) { 386 remoteShas[s] = r.Hash().String() 387 } 388 } 389 } 390 391 var b branches 392 fn := func(reference *plumbing.Reference) error { 393 p := splitRef(reference) 394 s := reference.Name().String() 395 if isCurrentStack(p, parts) { 396 var status = "" 397 if len(remoteShas) > 0 { 398 sha, ok := remoteShas[s] 399 if !ok { 400 status = "+" 401 } else if sha == reference.Hash().String() { 402 status = "=" 403 } else { 404 // TODO: change to walk branch for now test for presence in local repo. 405 _, err := repo.CommitObject(plumbing.NewHash(sha)) 406 if err != nil { 407 status = "∇" 408 } else { 409 status = "+" 410 } 411 } 412 } 413 b = append(b, branch{Name: p[3], Status: status}) 414 } 415 return nil 416 } 417 418 err = branchesApply(repo, fn) 419 if err != nil { 420 return ErrOutputWriter 421 } 422 423 sort.Sort(b) 424 425 stack := &Stack{ 426 Name: parts[2], 427 Branch: parts[3], 428 Branches: b, 429 } 430 if defaultRemote != nil { 431 stack.Remote = defaultRemote.Name 432 } 433 err = stackTpl.Execute(w, stack) 434 if err != nil { 435 // TODO: if w is stdout this is likely to fail as well. 436 log.Printf("call=tpl.Execute err=`%v`\n", err) 437 return ErrOutputWriter 438 } 439 } else if len(parts) == 3 { 440 branch := parts[2] 441 _, err = fmt.Fprintf(w, simpleBranch, branch) 442 if err != nil { 443 // TODO: if w is stdout this is likely to fail as well. 444 log.Printf("call=Fprintf err=`%v`\n", err) 445 return ErrOutputWriter 446 } 447 } 448 449 return Success 450 } 451 452 type branch struct { 453 Name string 454 Status string 455 } 456 457 type branches []branch 458 459 func (b branches) Swap(i, j int) { 460 b[i], b[j] = b[j], b[i] 461 } 462 463 func (b branches) Len() int { 464 return len(b) 465 } 466 467 func (b branches) Less(i, j int) bool { 468 return b[i].Name < b[j].Name 469 } 470 471 func isCurrentStack(p []string, cur []string) bool { 472 return isStack(p) && p[stackName] == cur[stackName] 473 } 474 475 func headParts(repo *git.Repository) ([]string, error) { 476 ref, err := repo.Head() 477 if err != nil { 478 // TODO: how do we get here? Detached head? 479 log.Printf("call=Head err=`%v`\n", err) 480 return nil, err 481 } 482 return splitRef(ref), nil 483 } 484 485 func openWorkTree() (*git.Repository, *git.Worktree, error) { 486 repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true}) 487 if err == git.ErrRepositoryNotExists { 488 log.Printf("call=PlainOpen err=`%v`\n", err) 489 return nil, nil, err 490 } 491 492 wt, err := repo.Worktree() 493 if err != nil { 494 log.Printf("call=WorkTree err=`%v`\n", err) 495 return nil, nil, err 496 } 497 498 return repo, wt, nil 499 } 500 501 var stackTpl = template.Must(template.New("stack").Parse(`In stack {{ .Name }} 502 On branch {{ .Name }}/{{ .Branch }} 503 {{ if .Remote }}Remote {{ .Remote }} 504 {{ end }} 505 Local Stack{{ if .Remote }} (+ ahead, = same, ∇ diverged){{ end }}: 506 {{- range .Branches }} 507 {{ if .Status }}({{ .Status }}) {{ end }}{{ .Name }}{{ end }} 508 `)) 509 510 const simpleBranch = `Not in a stack 511 On branch %s 512 `