Home > Software design >  git2 restore staged file
git2 restore staged file

Time:09-20

I am trying to build a hobby app to provide a UI for Git. I am trying to perform the equivalent of git restore --staged <file>. The below seems to successfully perform the correct action in the case where the file is newly added to the index, so I can just remove it form the index, but I am not sure how to handle the case where the file has previously been added to the index and has been modified. I have a limited understanding of both Git and C which makes it hard for me to work this out myself.

use git2;

fn main() {
    let repo = git2::Repository::open("myrepo").unwrap();
    let mut index = repo.index().unwrap();
    let statuses = repo.statuses(None).unwrap();
    let file_status_entry = statuses.get(0).unwrap();
    let ct_path = std::path::Path::new(file_status_entry.path().unwrap());
    let file_status = file_status_entry.status();
    if file_status.is_index_new() {
        index.remove(ct_path, 0).unwrap();
    } else {
        // not sure what to do here
        index.remove(ct_path, 0).unwrap();
    }
    index.write().unwrap();
}

CodePudding user response:

The git restore command is ... complicated. Emulating it exactly is therefore also complicated, regardless of what language and library you use. However, if you limit yourself to just one instance of git restore's behavior (e.g., git restore --staged), that simplifies things.

I'm not familiar with the Rust library version here, so all I can tell you is what CGit does with git restore --staged path. The goal of this operation is to copy some path into Git's index, from some source commit. When --staged is used, the source commit defaults to HEAD (unless overridden with --source). The working tree is left untouched by this operation (unless you add --worktree as well of course).

The Rust equivalent will amount to:

  • open and read the repository (as you did);
  • open and read the index (as you did), but gaining a lock as well;
  • open and read the current commit's tree to find the given path.

At this point you're ready for the logic. Note: for git restore, the argument is not a path name but rather a pathspec, which requires iterating over all matching path names in various cases (e.g., git restore --staged "*"). This can affect the logic below (if you're git restore-ing */foo and there are six directories that could have a foo but only four of them do have a foo, the "no match" case should not error out for the other two directories). If you're handling only a single path name your job is simplified and is what is listed below.

  • If the named file exists in the current commit, copy its data to the index at that path, removing any unmerged index entries (nonzero staging entries) and proceed to the "rewrite index" step.
  • Otherwise (the named file does not exist in the current commit):
    • If the named file exists in the index, remove all those entries and proceed to the rewrite index step.
    • Otherwise, error-exit with a no-matches style error (error: pathspec 'foo' did not match any file(s) know to git).

Once you've done this for a single file (no pathspec magic, just one file listed) or all appropriate files (globbing or whatever), if you've updated the index, it is now time to put it back. This involves writing the new content index to the index.lock file obtained as the lock in the second initial step, flushing that to disk (be sure to fsync it if the OS has fsync), and renaming it to release the lock and put it in place as the index.

Remember, too, to handle any git worktree add complications (these result in a different default index file) and/or GIT_INDEX_FILE environment complications. These may (or may not) be covered by the library.

  • Related