github.com/driusan/dgit@v0.0.0-20221118233547-f39f0c15edbb/git/checkout.go (about)

     1  package git
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  )
     9  
    10  // CheckoutOptions represents the options that may be passed to
    11  // "git checkout"
    12  type CheckoutOptions struct {
    13  	// Not implemented
    14  	Quiet bool
    15  	// Not implemented
    16  	Progress bool
    17  	// Not implemented
    18  	Force bool
    19  
    20  	// Check out the named stage for unnamed paths.
    21  	// Stage2 is equivalent to --ours, Stage3 to --theirs
    22  	// Not implemented
    23  	Stage Stage
    24  
    25  	Branch      string // -b
    26  	ForceBranch bool   // use branch as -B
    27  
    28  	// Not implemented
    29  	OrphanBranch bool // use branch as --orphan
    30  
    31  	// Not implemented
    32  	Track string
    33  	// Not implemented
    34  	CreateReflog bool // -l
    35  
    36  	// Not implemented
    37  	Detach bool
    38  
    39  	IgnoreSkipWorktreeBits bool
    40  
    41  	// Not implemented
    42  	Merge bool
    43  
    44  	// Not implemented.
    45  	ConflictStyle string
    46  
    47  	Patch bool
    48  
    49  	// Not implemented
    50  	IgnoreOtherWorktrees bool
    51  }
    52  
    53  // Implements the "git checkout" subcommand of git. Variations in the man-page
    54  // are:
    55  //
    56  //     git checkout [-q] [-f] [-m] [<branch>]
    57  //     git checkout [-q] [-f] [-m] --detach [<branch>]
    58  //     git checkout [-q] [-f] [-m] [--detach] <commit>
    59  //     git checkout [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]
    60  //     git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <paths>...
    61  //     git checkout [-p|--patch] [<tree-ish>] [--] [<paths>...]
    62  //
    63  // This will just check the options and call the appropriate variation. You
    64  // can avoid the overhead by calling the proper variation directly.
    65  //
    66  // "thing" is the thing that the user entered on the command line to be checked out. It
    67  // might be a branch, a commit, or a treeish, depending on the variation above.
    68  func Checkout(c *Client, opts CheckoutOptions, thing string, files []File) error {
    69  	if thing == "" {
    70  		thing = "HEAD"
    71  	}
    72  
    73  	if opts.Patch {
    74  		diffs, err := DiffFiles(c, DiffFilesOptions{}, files)
    75  		if err != nil {
    76  			return err
    77  		}
    78  		var patchbuf bytes.Buffer
    79  		if err := GeneratePatch(c, DiffCommonOptions{Patch: true}, diffs, &patchbuf); err != nil {
    80  			return err
    81  		}
    82  		hunks, err := splitPatch(patchbuf.String(), false)
    83  		if err != nil {
    84  			return err
    85  		}
    86  		hunks, err = filterHunks("discard this hunk from the work tree", hunks)
    87  		if err == userAborted {
    88  			return nil
    89  		} else if err != nil {
    90  			return err
    91  		}
    92  
    93  		patch, err := ioutil.TempFile("", "checkoutpatch")
    94  		if err != nil {
    95  			return err
    96  		}
    97  		defer os.Remove(patch.Name())
    98  		recombinePatch(patch, hunks)
    99  
   100  		return Apply(c, ApplyOptions{Reverse: true}, []File{File(patch.Name())})
   101  	}
   102  
   103  	if len(files) == 0 {
   104  		cmt, err := RevParseCommitish(c, &RevParseOptions{}, thing)
   105  		if err != nil {
   106  			return err
   107  		}
   108  		return CheckoutCommit(c, opts, cmt)
   109  	}
   110  
   111  	b, err := RevParseTreeish(c, &RevParseOptions{}, thing)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	return CheckoutFiles(c, opts, b, files)
   116  }
   117  
   118  // Implements the "git checkout" subcommand of git for variations:
   119  //     git checkout [-q] [-f] [-m] [<branch>]
   120  //     git checkout [-q] [-f] [-m] --detach [<branch>]
   121  //     git checkout [-q] [-f] [-m] [--detach] <commit>
   122  //     git checkout [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]
   123  func CheckoutCommit(c *Client, opts CheckoutOptions, commit Commitish) error {
   124  	// RefSpec for new branch with -b/-B variety
   125  	var newRefspec RefSpec
   126  	if opts.Branch != "" {
   127  		// Handle the -b/-B variety.
   128  		// commit is the startpoint in the last variation, otherwise
   129  		// Checkout() already set it to the commit of "HEAD"
   130  		newRefspec = RefSpec("refs/heads/" + opts.Branch)
   131  		refspecfile := newRefspec.File(c)
   132  		if refspecfile.Exists() && !opts.ForceBranch {
   133  			return fmt.Errorf("fatal: A branch named '%v' already exists.", opts.Branch)
   134  		}
   135  	}
   136  	// Get the original HEAD for the reflog
   137  	var head Commitish
   138  	head, err := SymbolicRefGet(c, SymbolicRefOptions{}, "HEAD")
   139  	switch err {
   140  	case DetachedHead:
   141  		head, err = c.GetHeadCommit()
   142  		if err != nil {
   143  			return err
   144  		}
   145  	case nil:
   146  	default:
   147  		return err
   148  	}
   149  
   150  	// Convert from Commitish to Treeish for ReadTree and LsTree
   151  	cid, err := commit.CommitID(c)
   152  	if err != nil {
   153  		return err
   154  	}
   155  
   156  	if !opts.Force {
   157  		// Check that nothing would be lost
   158  		lstree, err := LsTree(c, LsTreeOptions{Recurse: true}, cid, nil)
   159  		if err != nil {
   160  			return err
   161  		}
   162  		newfiles := make([]File, 0, len(lstree))
   163  		for _, entry := range lstree {
   164  			f, err := entry.PathName.FilePath(c)
   165  			if err != nil {
   166  				return err
   167  			}
   168  			newfiles = append(newfiles, f)
   169  		}
   170  		untracked, err := LsFiles(c, LsFilesOptions{Others: true}, newfiles)
   171  		if err != nil {
   172  			return err
   173  		}
   174  		if len(untracked) > 0 {
   175  			err := "error: The following untracked working tree files would be overwritten by checkout:\n"
   176  			for _, f := range untracked {
   177  				err += "\t" + f.IndexEntry.PathName.String() + "\n"
   178  			}
   179  			err += "Please move or remove them before you switch branches.\nAborting"
   180  			return fmt.Errorf("%v", err)
   181  		}
   182  	}
   183  
   184  	// "head" is a Commitish, but we need a Treeish, so just resolve it
   185  	// to a commit.
   186  	hc, err := head.CommitID(c)
   187  	if err != nil {
   188  		return err
   189  	}
   190  	staged, err := DiffIndex(c, DiffIndexOptions{}, nil, hc, nil)
   191  	// Now actually read the tree into the index
   192  	readtreeopts := ReadTreeOptions{Update: true, Merge: true}
   193  	if opts.Force {
   194  		readtreeopts.Merge = false
   195  		readtreeopts.Reset = true
   196  	}
   197  	if opts.IgnoreSkipWorktreeBits {
   198  		readtreeopts.NoSparseCheckout = true
   199  	}
   200  	idx, err := ReadTree(c, readtreeopts, cid)
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	// Put back changes that were staged before doing read-tree -u
   206  	for _, diff := range staged {
   207  		if diff.Dst.Sha1 == (Sha1{}) {
   208  			continue
   209  		}
   210  		if err := idx.AddStage(c, diff.Name, diff.Dst.FileMode, diff.Dst.Sha1, Stage0, uint32(diff.DstSize), 0, UpdateIndexOptions{}); err != nil {
   211  			return err
   212  		}
   213  		content, err := CatFile(c, "blob", diff.Dst.Sha1, CatFileOptions{})
   214  		if err != nil {
   215  			return err
   216  		}
   217  		f, err := diff.Name.FilePath(c)
   218  		if err != nil {
   219  			return err
   220  		}
   221  		if err := ioutil.WriteFile(f.String(), []byte(content), os.FileMode(diff.Dst.FileMode)); err != nil {
   222  			return err
   223  		}
   224  	}
   225  
   226  	f, err := c.GitDir.Create("index")
   227  	if err != nil {
   228  		return err
   229  	}
   230  	defer f.Close()
   231  
   232  	if err := idx.WriteIndex(f); err != nil {
   233  		return err
   234  	}
   235  
   236  	var origB string
   237  	// Get the original HEAD branchname for the reflog
   238  	//origB = Branch(head).BranchName()
   239  	switch h := head.(type) {
   240  	case RefSpec:
   241  		origB = Branch(h).BranchName()
   242  	default:
   243  		if h, err := head.CommitID(c); err == nil {
   244  			origB = h.String()
   245  		}
   246  	}
   247  
   248  	if opts.Branch != "" {
   249  		// In the case of -B (ForceBranch) this will slam in the new branch based on the provided commit ID
   250  		if err := c.CreateBranch(opts.Branch, cid); err != nil {
   251  			return err
   252  		}
   253  		refmsg := fmt.Sprintf("checkout: moving from %s to %s (dgit)", origB, opts.Branch)
   254  		return SymbolicRefUpdate(c, SymbolicRefOptions{}, "HEAD", RefSpec("refs/heads/"+opts.Branch), refmsg)
   255  	}
   256  	if b, ok := commit.(Branch); ok && !opts.Detach {
   257  		// We're checking out a branch, first read the new tree, and
   258  		// then update the SymbolicRef for HEAD, if that succeeds.
   259  		refmsg := fmt.Sprintf("checkout: moving from %s to %s (dgit)", origB, b.BranchName())
   260  		return SymbolicRefUpdate(c, SymbolicRefOptions{}, "HEAD", RefSpec(b), refmsg)
   261  	}
   262  	refmsg := fmt.Sprintf("checkout: moving from %s to %s (dgit)", origB, cid)
   263  	if err := UpdateRef(c, UpdateRefOptions{NoDeref: true, OldValue: head}, "HEAD", cid, refmsg); err != nil {
   264  		return err
   265  	}
   266  	return nil
   267  }
   268  
   269  // Implements "git checkout" subcommand of git for variations:
   270  //     git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <paths>...
   271  //     git checkout [-p|--patch] [<tree-ish>] [--] [<paths>...]
   272  func CheckoutFiles(c *Client, opts CheckoutOptions, tree Treeish, files []File) error {
   273  	// If files were specified, we don't want ReadTree to update the workdir,
   274  	// because we only want to (force) update the specified files.
   275  	//
   276  	// If they weren't, we want to checkout a treeish, so let ReadTree update
   277  	// the workdir so that we don't lose any changes.
   278  	// Load the index so that we can check the skip worktree bit if applicable
   279  	index, err := c.GitDir.ReadIndex()
   280  	if err != nil {
   281  		return err
   282  	}
   283  	imap := index.GetMap()
   284  	expandedfiles, err := LsTree(c, LsTreeOptions{Recurse: true}, tree, files)
   285  	if err != nil {
   286  		return err
   287  	}
   288  	files = make([]File, 0, len(files))
   289  	for _, entry := range expandedfiles {
   290  		f, err := entry.PathName.FilePath(c)
   291  		if err != nil {
   292  			return err
   293  		}
   294  		if opts.IgnoreSkipWorktreeBits {
   295  			files = append(files, f)
   296  			continue
   297  		}
   298  		if entry, ok := imap[entry.PathName]; ok && entry.SkipWorktree() {
   299  			continue
   300  		}
   301  		files = append(files, f)
   302  	}
   303  
   304  	// We just want to load the tree as an index so that CheckoutIndexUncommited, so we
   305  	// specify DryRun.
   306  	treeidx, err := ReadTree(c, ReadTreeOptions{DryRun: true}, tree)
   307  	if err != nil {
   308  		return err
   309  	}
   310  
   311  	return CheckoutIndexUncommited(c, treeidx, CheckoutIndexOptions{Force: true, UpdateStat: true}, files)
   312  }