How do I update the working tree when checking out a branch with libgit2?

StackOverflow https://stackoverflow.com/questions/19034971

  •  29-06-2022
  •  | 
  •  

Pergunta

I'm trying to implement checkout-like functionality with an old version of libgit2 (no checkout.h).

To start, I'm on branch A, which looks like:

Branch:
A         A0 --- A1
         /
Master  M

Each commit creates a file with the same name, e.g., the commit labelled A1 creates a file A1. If I look at gitk at this point, everything looks exactly the way I want it to.

Now I create a new branch, B, and I want to add a commit to that:

Branch:
A         A0 --- A1
         /
Master  M
         \
B         B0

However, when I use my code to "checkout" B, it makes A0 and A1 untracked instead of deleting them, as I'd expect:

(B)$ git status
# On branch B
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#       A0
#       A1
nothing added to commit but untracked files present (use "git add" to track)

So, I think there's something missing with my checkout code, which is this:

void Checkout(const char *branch_name, git_commit *branch_tip) {
  // Update index & tree                                                                                                                                                                                                                                                       
  git_tree *tree;
  git_commit_tree(&tree, branch_tip);
  git_index_read_tree(index_, tree);
  git_index_write(index_);
  git_tree_free(tree);

  // Reset head
  string branch_ref = string("refs/heads/") + branch_name;
  git_reference *head;
  git_reference_lookup(&head, repo_, kGitHeadFile);
  git_reference_set_target(head, branch_ref.c_str());
  git_reference_free(head);
}

(Note that I'm actually checking return codes every line in the real code and everything's returning 0, just didn't want to clutter things up here.)

As far as I can tell, this code matches what the git documentation describes git checkout <branch>:

To prepare for working on <branch>, switch to it by updating
the index and the files in the working tree, and by pointing
HEAD at the branch. 

Is there some... "update the working tree" command I need to run?

Foi útil?

Solução

If you must write this yourself, you can look at the basic strategy that the existing implementation in libgit2 uses. Let's just think about implementing a forced checkout (i.e. ignoring any modified files in the working directory) because that is a much simpler case.

You haven't mentioned how old your libgit2 is. I'm going to write the following assuming you have access to the diff functionality and I'll even use some of the somewhat more recent accessor functions for diff data. If those accessor functions aren't available in your version, you may have to rework this to use callback functions. If the core diff functionality isn't available, then your libgit2 is too old for this purpose, I believe.

You need to consider the old HEAD you are coming from and the new HEAD you are moving to in order to know about which files are going to be deleted (vs files that are simply untracked in the working directory). The easiest thing to do in libgit2 is something like:

git_diff_list *diff;
git_diff_delta *delta;
git_blob *blob;
size_t i;
FILE *fp;

git_diff_tree_to_tree(&diff, repo, from_tree, to_tree, NULL);

for (i = 0; i < git_diff_num_deltas(diff); ++i) {
    git_diff_get_patch(NULL, &delta, diff, i);

    switch (delta->status) {
    case GIT_DELTA_ADDED:
    case GIT_DELTA_MODIFIED:
        /* file was added or modified between the two commits */
        git_blob_lookup(&blob, repo, &delta->new_file.oid);

        fp = fopen(delta->new_file.path, "w");
        fwrite(git_blob_rawdata(blob), git_blob_rawsize(blob), 1, fp);
        fclose(fp);

        git_blob_free(blob);
        break;

    case GIT_DELTA_DELETED:
        /* file was removed between the two commits */
        unlink(delta->old_file.path);
        break;

    default:
        /* no change required */
    }
}

git_diff_list_free(diff);

/* now update the index with the tree we just wrote out */
git_index_read_tree(index, to_tree);
git_index_write(index);

/* and do the other stuff you have to update the HEAD */

There are lots of problems with the actual code above that you will have to resolve:

  1. The paths in delta->new_file.path and delta->old_file.path are relative to the working directory of the repository, not the current working directory of the process, so the calls to open and unlink the files will need to adjust the paths accordingly
  2. The code doesn't deal with directories at all. Before opening a file, you will have to make the directories containing the file. After deleting a file, you will have to delete the directory containing the file if it was the last file in the directory. If you have branches where a directory turns into a regular file or vice versa, you will have to process deletes before adds.
  3. The code doesn't do any error checking which is a bad idea
  4. This code ignores pending changes in the index and modifications in the working directory. But we're talking about a forced checkout, so you get what you get.
  5. I just wrote the above code off the top of my head, so there are likely typos, etc.

Depending on your use case, maybe it will be okay to ignore type changes (i.e. directories that become blobs, etc) and maybe emulating --force will be acceptable. If not, then this really starts to turn into a lot of code.

Outras dicas

You didn't specify why you couldn't just upgrade to a recent libgit2 that supports checkout, so that you could just call:

git_checkout_head(repo, NULL);

So I'm going to say: upgrade your libgit2 to something more recent. You will be very dissatisfied continuing to use an old version. And checkout, in particular, is not a function that you would want to implement yourself. (Take a look at libgit2's checkout.c.)

But to answer your question, the basic methodology is:

  1. Compare the working directory to the tree pointed to by HEAD. Collect a list of any items that differ, ideally by using the cache so that you need not compute hashes on files that are obviously unchanged between the index and the workdir. Be sure to load the filters out of the config and apply your own version of them, as appropriate, since the filters were not publicly exposed in any version of libgit2 that lacked checkout.

  2. For every item in that list, write the data to disk. Be sure to apply any filters as appropriate. Again, you'll have to roll your own filters.

  3. Update the index to reflect what you wrote to the filesystem using git_index_add_bypath.

A very detailed set of information is available in the libgit2 checkout documentation.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top