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 }