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  `