Search code examples
gitgit-checkoutgit-plumbing

How to replicate git-checkout using only plumbing commands?


I would like to avoid calling porcelain commands from my scripts, but is there a way to get some of the behavior of git checkout <commit> using only plumbing commands like checkout-index? I'm particularly interested in the effect on the working copy: assuming everything is clean, checkout deletes files that were tracked in the old HEAD and absent in the new one. checkout-index doesn't seem to have any concept of deleting files. The closest thing I can think of would be to call

git diff-tree -p <old> <new> | git apply

but computing the whole diff seems unnecessarily expensive. Is there a better way?


Solution

  • You're looking for the two-tree git read-tree -um. It uses a base tree, (generally you feed it HEAD), a target tree, and (implicitly) the index and worktree. The table describing its behavior was hard for me to understand so I have my own cheatsheet for it, a reformatted one that makes more sense to me, anyway. At any rate, it implements git checkout.

    git read-tree -um H M  # `I` is the (implicit) index, GIT_INDEX_FILE
    
                        Legend
    
            H       Original tree (usually HEAD:)
            I       Indexed tree
            M       Merge target tree
    
            H->I     \
            H->M      } status in second relative to first
            I->M     /
    
            "-"     file exists in neither
           new      exists only in second
           deleted  exists only in first
           same     exists in both, unchanged
           changed  exists in both, different
          (blank)   irrelevant or all cases not otherwise given
    
            keep    keep current version
            fail    whole command fails, no changes
           delete   delete worktree file
    
    
          H->I      H->M      I->M
    
                              same     keep
    
         deleted   changed             fail
         deleted   deleted             delete
         deleted    same               delete unless Index empty, else use M
                    same               keep
    
          same     changed             worktree clean: use M; dirty: fail
          same     deleted             worktree clean: deleted; dirty: fail
    
          new        -                 keep
          new       new      changed   fail
         changed   changed   changed   fail
         changed   deleted             fail
    
    
    note: "index empty" identifies an initial checkout, where HEAD has been
    set but never loaded.  git can't currently distinguish between a
    delete-everything index and an initial-checkout index.