diff options
-rw-r--r-- | docs/checkout-internals.md | 203 | ||||
-rw-r--r-- | include/git2/checkout.h | 219 | ||||
-rw-r--r-- | src/checkout.c | 184 |
3 files changed, 304 insertions, 302 deletions
diff --git a/docs/checkout-internals.md b/docs/checkout-internals.md new file mode 100644 index 000000000..cb646da5d --- /dev/null +++ b/docs/checkout-internals.md @@ -0,0 +1,203 @@ +Checkout Internals +================== + +Checkout has to handle a lot of different cases. It examines the +differences between the target tree, the baseline tree and the working +directory, plus the contents of the index, and groups files into five +categories: + +1. UNMODIFIED - Files that match in all places. +2. SAFE - Files where the working directory and the baseline content + match that can be safely updated to the target. +3. DIRTY/MISSING - Files where the working directory differs from the + baseline but there is no conflicting change with the target. One + example is a file that doesn't exist in the working directory - no + data would be lost as a result of writing this file. Which action + will be taken with these files depends on the options you use. +4. CONFLICTS - Files where changes in the working directory conflict + with changes to be applied by the target. If conflicts are found, + they prevent any other modifications from being made (although there + are options to override that and force the update, of course). +5. UNTRACKED/IGNORED - Files in the working directory that are untracked + or ignored (i.e. only in the working directory, not the other places). + +Right now, this classification is done via 3 iterators (for the three +trees), with a final lookup in the index. At some point, this may move to +a 4 iterator version to incorporate the index better. + +The actual checkout is done in five phases (at least right now). + +1. The diff between the baseline and the target tree is used as a base + list of possible updates to be applied. +2. Iterate through the diff and the working directory, building a list of + actions to be taken (and sending notifications about conflicts and + dirty files). +3. Remove any files / directories as needed (because alphabetical + iteration means that an untracked directory will end up sorted *after* + a blob that should be checked out with the same name). +4. Update all blobs. +5. Update all submodules (after 4 in case a new .gitmodules blob was + checked out) + +Checkout could be driven either off a target-to-workdir diff or a +baseline-to-target diff. There are pros and cons of each. + +Target-to-workdir means the diff includes every file that could be +modified, which simplifies bookkeeping, but the code to constantly refer +back to the baseline gets complicated. + +Baseline-to-target has simpler code because the diff defines the action to +take, but needs special handling for untracked and ignored files, if they +need to be removed. + +The current checkout implementation is based on a baseline-to-target diff. + + +Picking Actions +=============== + +The most interesting aspect of this is phase 2, picking the actions that +should be taken. There are a lot of corner cases, so it may be easier to +start by looking at the rules for a simple 2-iterator diff: + +Key +--- +- B1,B2,B3 - blobs with different SHAs, +- Bi - ignored blob (WD only) +- T1,T2,T3 - trees with different SHAs, +- Ti - ignored tree (WD only) +- x - nothing + +Diff with 2 non-workdir iterators +--------------------------------- + + Old New + --- --- + 0 x x - nothing + 1 x B1 - added blob + 2 x T1 - added tree + 3 B1 x - removed blob + 4 B1 B1 - unmodified blob + 5 B1 B2 - modified blob + 6 B1 T1 - typechange blob -> tree + 7 T1 x - removed tree + 8 T1 B1 - typechange tree -> blob + 9 T1 T1 - unmodified tree + 10 T1 T2 - modified tree (implies modified/added/removed blob inside) + + +Now, let's make the "New" iterator into a working directory iterator, so +we replace "added" items with either untracked or ignored, like this: + +Diff with non-work & workdir iterators +-------------------------------------- + + Old New-WD + --- ------ + 0 x x - nothing + 1 x B1 - untracked blob + 2 x Bi - ignored file + 3 x T1 - untracked tree + 4 x Ti - ignored tree + 5 B1 x - removed blob + 6 B1 B1 - unmodified blob + 7 B1 B2 - modified blob + 8 B1 T1 - typechange blob -> tree + 9 B1 Ti - removed blob AND ignored tree as separate items + 10 T1 x - removed tree + 11 T1 B1 - typechange tree -> blob + 12 T1 Bi - removed tree AND ignored blob as separate items + 13 T1 T1 - unmodified tree + 14 T1 T2 - modified tree (implies modified/added/removed blob inside) + +Note: if there is a corresponding entry in the old tree, then a working +directory item won't be ignored (i.e. no Bi or Ti for tracked items). + + +Now, expand this to three iterators: a baseline tree, a target tree, and +an actual working directory tree: + +Checkout From 3 Iterators (2 not workdir, 1 workdir) +---------------------------------------------------- + +(base == old HEAD; target == what to checkout; actual == working dir) + + base target actual/workdir + ---- ------ ------ + 0 x x x - nothing + 1 x x B1/Bi/T1/Ti - untracked/ignored blob/tree (SAFE) + 2+ x B1 x - add blob (SAFE) + 3 x B1 B1 - independently added blob (FORCEABLE-2) + 4* x B1 B2/Bi/T1/Ti - add blob with content conflict (FORCEABLE-2) + 5+ x T1 x - add tree (SAFE) + 6* x T1 B1/Bi - add tree with blob conflict (FORCEABLE-2) + 7 x T1 T1/i - independently added tree (SAFE+MISSING) + 8 B1 x x - independently deleted blob (SAFE+MISSING) + 9- B1 x B1 - delete blob (SAFE) + 10- B1 x B2 - delete of modified blob (FORCEABLE-1) + 11 B1 x T1/Ti - independently deleted blob AND untrack/ign tree (SAFE+MISSING !!!) + 12 B1 B1 x - locally deleted blob (DIRTY || SAFE+CREATE) + 13+ B1 B2 x - update to deleted blob (SAFE+MISSING) + 14 B1 B1 B1 - unmodified file (SAFE) + 15 B1 B1 B2 - locally modified file (DIRTY) + 16+ B1 B2 B1 - update unmodified blob (SAFE) + 17 B1 B2 B2 - independently updated blob (FORCEABLE-1) + 18+ B1 B2 B3 - update to modified blob (FORCEABLE-1) + 19 B1 B1 T1/Ti - locally deleted blob AND untrack/ign tree (DIRTY) + 20* B1 B2 T1/Ti - update to deleted blob AND untrack/ign tree (F-1) + 21+ B1 T1 x - add tree with locally deleted blob (SAFE+MISSING) + 22* B1 T1 B1 - add tree AND deleted blob (SAFE) + 23* B1 T1 B2 - add tree with delete of modified blob (F-1) + 24 B1 T1 T1 - add tree with deleted blob (F-1) + 25 T1 x x - independently deleted tree (SAFE+MISSING) + 26 T1 x B1/Bi - independently deleted tree AND untrack/ign blob (F-1) + 27- T1 x T1 - deleted tree (MAYBE SAFE) + 28+ T1 B1 x - deleted tree AND added blob (SAFE+MISSING) + 29 T1 B1 B1 - independently typechanged tree -> blob (F-1) + 30+ T1 B1 B2 - typechange tree->blob with conflicting blob (F-1) + 31* T1 B1 T1/T2 - typechange tree->blob (MAYBE SAFE) + 32+ T1 T1 x - restore locally deleted tree (SAFE+MISSING) + 33 T1 T1 B1/Bi - locally typechange tree->untrack/ign blob (DIRTY) + 34 T1 T1 T1/T2 - unmodified tree (MAYBE SAFE) + 35+ T1 T2 x - update locally deleted tree (SAFE+MISSING) + 36* T1 T2 B1/Bi - update to tree with typechanged tree->blob conflict (F-1) + 37 T1 T2 T1/T2/T3 - update to existing tree (MAYBE SAFE) + +The number is followed by ' ' if no change is needed or '+' if the case +needs to write to disk or '-' if something must be deleted and '*' if +there should be a delete followed by an write. + +There are four tiers of safe cases: + +- SAFE == completely safe to update +- SAFE+MISSING == safe except the workdir is missing the expect content +- MAYBE SAFE == safe if workdir tree matches (or is missing) baseline + content, which is unknown at this point +- FORCEABLE == conflict unless FORCE is given +- DIRTY == no conflict but change is not applied unless FORCE + +Some slightly unusual circumstances: + + 8 - parent dir is only deleted when file is, so parent will be left if + empty even though it would be deleted if the file were present + 11 - core git does not consider this a conflict but attempts to delete T1 + and gives "unable to unlink file" error yet does not skip the rest + of the operation + 12 - without FORCE file is left deleted (i.e. not restored) so new wd is + dirty (and warning message "D file" is printed), with FORCE, file is + restored. + 24 - This should be considered MAYBE SAFE since effectively it is 7 and 8 + combined, but core git considers this a conflict unless forced. + 26 - This combines two cases (1 & 25) (and also implied 8 for tree content) + which are ok on their own, but core git treat this as a conflict. + If not forced, this is a conflict. If forced, this actually doesn't + have to write anything and leaves the new blob as an untracked file. + 32 - This is the only case where the baseline and target values match + and yet we will still write to the working directory. In all other + cases, if baseline == target, we don't touch the workdir (it is + either already right or is "dirty"). However, since this case also + implies that a ?/B1/x case will exist as well, it can be skipped. + +Cases 3, 17, 24, 26, and 29 are all considered conflicts even though +none of them will require making any updates to the working directory. + diff --git a/include/git2/checkout.h b/include/git2/checkout.h index 884ea27f6..12fffebad 100644 --- a/include/git2/checkout.h +++ b/include/git2/checkout.h @@ -23,97 +23,82 @@ GIT_BEGIN_DECL /** * Checkout behavior flags * - * In libgit2, the function of checkout is to update the working directory - * to match a target tree. It does not move the HEAD commit - you do that - * separately. To safely perform the update, checkout relies on a baseline - * tree (generally the current HEAD) as a reference for the unmodified - * content expected in the working directory. + * In libgit2, checkout is used to update the working directory and index + * to match a target tree. Unlike git checkout, it does not move the HEAD + * commit for you - use `git_repository_set_head` or the like to do that. * - * Checkout examines the differences between the target tree, the baseline - * tree and the working directory, and groups files into five categories: + * Checkout looks at (up to) four things: the "target" tree you want to + * check out, the "baseline" tree of what was checked out previously, the + * working directory for actual files, and the index for staged changes. * - * 1. UNMODIFIED - Files that match in all places. - * 2. SAFE - Files where the working directory and the baseline content - * match that can be safely updated to the target. - * 3. DIRTY/MISSING - Files where the working directory differs from the - * baseline but there is no conflicting change with the target. One - * example is a file that doesn't exist in the working directory - no - * data would be lost as a result of writing this file. Which action - * will be taken with these files depends on the options you use. - * 4. CONFLICTS - Files where changes in the working directory conflict - * with changes to be applied by the target. If conflicts are found, - * they prevent any other modifications from being made (although there - * are options to override that and force the update, of course). - * 5. UNTRACKED/IGNORED - Files in the working directory that are untracked - * or ignored (i.e. only in the working directory, not the other places). + * You give checkout one of four strategies for update: * + * - `GIT_CHECKOUT_NONE` is a dry-run strategy that checks for conflicts, + * etc., but doesn't make any actual changes. * - * You control the actions checkout takes with one of four base strategies: + * - `GIT_CHECKOUT_FORCE` is at the opposite extreme, taking any action to + * make the working directory match the target (including potentially + * discarding modified files). * - * - `GIT_CHECKOUT_NONE` is the default and applies no changes. It is a dry - * run that you can use to find conflicts, etc. if you wish. + * In between those are `GIT_CHECKOUT_SAFE` and `GIT_CHECKOUT_SAFE_CREATE` + * both of which only make modifications that will not lose changes. * - * - `GIT_CHECKOUT_SAFE` is like `git checkout` and only applies changes - * between the baseline and target trees to files in category 2. + * | target == baseline | target != baseline | + * ---------------------|-----------------------|----------------------| + * workdir == baseline | no action | create, update, or | + * | | delete file | + * ---------------------|-----------------------|----------------------| + * workdir exists and | no action | conflict (notify | + * is != baseline | notify dirty MODIFIED | and cancel checkout) | + * ---------------------|-----------------------|----------------------| + * workdir missing, | create if SAFE_CREATE | create file | + * baseline present | notify dirty DELETED | | + * ---------------------|-----------------------|----------------------| * - * - `GIT_CHECKOUT_SAFE_CREATE` also creates files that are missing from the - * working directory (category 3), even if there is no change between the - * baseline and target trees for those files. See notes below on - * emulating `git checkout-index` for some of the subtleties of this. + * The only difference between SAFE and SAFE_CREATE is that SAFE_CREATE + * will cause a file to be checked out if it is missing from the working + * directory even if it is not modified between the target and baseline. * - * - `GIT_CHECKOUT_FORCE` is like `git checkout -f` and will update the - * working directory to match the target content regardless of conflicts, - * overwriting dirty and conflicting files. * + * To emulate `git checkout`, use `GIT_CHECKOUT_SAFE` with a checkout + * notification callback (see below) that displays information about dirty + * files. The default behavior will cancel checkout on conflicts. * - * There are some additional flags to modified the behavior of checkout: + * To emulate `git checkout-index`, use `GIT_CHECKOUT_SAFE_CREATE` with a + * notification callback that cancels the operation if a dirty-but-existing + * file is found in the working directory. This core git command isn't + * quite "force" but is sensitive about some types of changes. * - * - GIT_CHECKOUT_ALLOW_CONFLICTS can be added to apply safe file updates - * even if there are conflicts. Normally, the entire checkout will be - * cancelled if any files are in category 4. With this flag, conflicts - * will be skipped (though the notification callback will still be invoked - * on the conflicting files if requested). + * To emulate `git checkout -f`, use `GIT_CHECKOUT_FORCE`. * - * - GIT_CHECKOUT_REMOVE_UNTRACKED means that files in the working directory - * that are untracked (but not ignored) should be deleted. The are not - * considered conflicts and would normally be ignored by checkout. + * To emulate `git clone` use `GIT_CHECKOUT_SAFE_CREATE` in the options. * - * - GIT_CHECKOUT_REMOVE_IGNORED means to remove ignored files from the - * working directory as well. Obviously, these would normally be ignored. * - * - GIT_CHECKOUT_UPDATE_ONLY means to only update the content of files that - * already exist. Files will not be created nor deleted. This does not - * make adds and deletes into conflicts - it just skips applying those - * changes. This will also skip updates to typechanged files (since that - * would involve deleting the old and creating the new). - * - * - Unmerged entries in the index are also considered conflicts. The - * GIT_CHECKOUT_SKIP_UNMERGED flag causes us to skip files with unmerged - * index entries. You can also use GIT_CHECKOUT_USE_OURS and - * GIT_CHECKOUT_USE_THEIRS to proceeed with the checkout using either the - * stage 2 ("ours") or stage 3 ("theirs") version of files in the index. + * There are some additional flags to modified the behavior of checkout: * + * - GIT_CHECKOUT_ALLOW_CONFLICTS makes SAFE mode apply safe file updates + * even if there are conflicts (instead of cancelling the checkout). * - * To emulate `git checkout`, use `GIT_CHECKOUT_SAFE` with a checkout - * notification callback (see below) that displays information about dirty - * files (i.e. files that don't need an update but that no longer match the - * baseline content). The default behavior will cancel on conflicts. + * - GIT_CHECKOUT_REMOVE_UNTRACKED means remove untracked files (i.e. not + * in target, baseline, or index, and not ignored) from the working dir. * - * To emulate `git checkout-index`, use `GIT_CHECKOUT_SAFE_CREATE` with a - * notification callback that cancels the operation if a dirty-but-existing - * file is found in the working directory. This core git command isn't - * quite "force" but is sensitive about some types of changes. + * - GIT_CHECKOUT_REMOVE_IGNORED means remove ignored files (that are also + * unrtacked) from the working directory as well. + * + * - GIT_CHECKOUT_UPDATE_ONLY means to only update the content of files that + * already exist. Files will not be created nor deleted. This just skips + * applying adds, deletes, and typechanges. * - * To emulate `git checkout -f`, you use `GIT_CHECKOUT_FORCE`. + * - GIT_CHECKOUT_DONT_UPDATE_INDEX prevents checkout from writing the + * updated files' information to the index. * + * - Normally, checkout will reload the index and git attributes from disk + * before any operations. GIT_CHECKOUT_NO_REFRESH prevents this reload. * - * Checkout is "semi-atomic" as in it will go through the work to be done - * before making any changes and if may decide to abort if there are - * conflicts, or you can use the notification callback to explicitly abort - * the action before any updates are made. Despite this, if a second - * process is modifying the filesystem while checkout is running, it can't - * guarantee that the choices is makes while initially examining the - * filesystem are still going to be correct as it applies them. + * - Unmerged index entries are conflicts. GIT_CHECKOUT_SKIP_UNMERGED skips + * files with unmerged index entries instead. GIT_CHECKOUT_USE_OURS and + * GIT_CHECKOUT_USE_THEIRS to proceeed with the checkout using either the + * stage 2 ("ours") or stage 3 ("theirs") version of files in the index. */ typedef enum { GIT_CHECKOUT_NONE = 0, /** default is a dry run, no actual updates */ @@ -167,45 +152,23 @@ typedef enum { /** * Checkout notification flags * - * When running a checkout, you can set a notification callback (`notify_cb`) - * to be invoked for some or all files to be checked out. Which files - * receive a callback depend on the `notify_flags` value which is a - * combination of these flags. - * - * - GIT_CHECKOUT_NOTIFY_CONFLICT means that conflicting files that would - * prevent the checkout from occurring will receive callbacks. If you - * used GIT_CHECKOUT_ALLOW_CONFLICTS, the callbacks are still done, but - * the checkout will not be blocked. The callback `status_flags` will - * have both index and work tree change bits set (see `git_status_t`). - * - * - GIT_CHECKOUT_NOTIFY_DIRTY means to notify about "dirty" files, i.e. - * those that do not need to be updated but no longer match the baseline - * content. Core git displays these files when checkout runs, but does - * not stop the checkout. For these, `status_flags` will have only work - * tree bits set (i.e. GIT_STATUS_WT_MODIFIED, etc). - * - * - GIT_CHECKOUT_NOTIFY_UPDATED sends notification for any file changed by - * the checkout. Callback `status_flags` will have only index bits set. - * - * - GIT_CHECKOUT_NOTIFY_UNTRACKED notifies for all untracked files that - * are not ignored. Passing GIT_CHECKOUT_REMOVE_UNTRACKED would remove - * these files. The `status_flags` will be GIT_STATUS_WT_NEW. - * - * - GIT_CHECKOUT_NOTIFY_IGNORED notifies for the ignored files. Passing - * GIT_CHECKOUT_REMOVE_IGNORED will remove these. The `status_flags` - * will be to GIT_STATUS_IGNORED. - * - * If you return a non-zero value from the notify callback, the checkout - * will be canceled. Notification callbacks are made prior to making any - * modifications, so returning non-zero will cancel the entire checkout. - * If you are do not use GIT_CHECKOUT_ALLOW_CONFLICTS and there are - * conflicts, you don't need to explicitly cancel from the callback. - * Checkout itself will abort after all files are processed. - * - * To emulate core git checkout output, use GIT_CHECKOUT_NOTIFY_CONFLICTS - * and GIT_CHECKOUT_NOTIFY_DIRTY. Conflicts will have `status_flags` with - * changes in both the index and work tree (see the `git_status_t` values). - * Dirty files will only have work tree flags set. + * Checkout will invoke an options notification callback (`notify_cb`) for + * certain cases - you pick which ones via `notify_flags`: + * + * - GIT_CHECKOUT_NOTIFY_CONFLICT invokes checkout on conflicting paths. + * + * - GIT_CHECKOUT_NOTIFY_DIRTY notifies about "dirty" files, i.e. those that + * do not need an update but no longer match the baseline. Core git + * displays these files when checkout runs, but won't stop the checkout. + * + * - GIT_CHECKOUT_NOTIFY_UPDATED sends notification for any file changed. + * + * - GIT_CHECKOUT_NOTIFY_UNTRACKED notifies about untracked files. + * + * - GIT_CHECKOUT_NOTIFY_IGNORED notifies about ignored files. + * + * Returning a non-zero value from this callback will cancel the checkout. + * Notification callbacks are made prior to modifying any files on disk. */ typedef enum { GIT_CHECKOUT_NOTIFY_NONE = 0, @@ -216,13 +179,27 @@ typedef enum { GIT_CHECKOUT_NOTIFY_IGNORED = (1u << 4), } git_checkout_notify_t; +/** Checkout notification callback function */ +typedef int (*git_checkout_notify_cb)( + git_checkout_notify_t why, + const char *path, + const git_diff_file *baseline, + const git_diff_file *target, + const git_diff_file *workdir, + void *payload); + +/** Checkout progress notification function */ +typedef void (*git_checkout_progress_cb)( + const char *path, + size_t completed_steps, + size_t total_steps, + void *payload); + /** * Checkout options structure * - * Use zeros to indicate default settings. - * - * This should be initialized with the `GIT_CHECKOUT_OPTS_INIT` macro to - * correctly set the `version` field. + * Zero out for defaults. Initialize with `GIT_CHECKOUT_OPTS_INIT` macro to + * correctly set the `version` field. E.g. * * git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; */ @@ -237,21 +214,11 @@ typedef struct git_checkout_opts { int file_open_flags; /** default is O_CREAT | O_TRUNC | O_WRONLY */ unsigned int notify_flags; /** see `git_checkout_notify_t` above */ - int (*notify_cb)( - git_checkout_notify_t why, - const char *path, - const git_diff_file *baseline, - const git_diff_file *target, - const git_diff_file *workdir, - void *payload); + git_checkout_notify_cb notify_cb; void *notify_payload; /* Optional callback to notify the consumer of checkout progress. */ - void (*progress_cb)( - const char *path, - size_t completed_steps, - size_t total_steps, - void *payload); + git_checkout_progress_cb progress_cb; void *progress_payload; /** When not zeroed out, array of fnmatch patterns specifying which diff --git a/src/checkout.c b/src/checkout.c index a26f007b1..2e132947d 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -24,157 +24,7 @@ #include "diff.h" #include "pathspec.h" -/* Key - * === - * B1,B2,B3 - blobs with different SHAs, - * Bi - ignored blob (WD only) - * T1,T2,T3 - trees with different SHAs, - * Ti - ignored tree (WD only) - * x - nothing - */ - -/* Diff with 2 non-workdir iterators - * ================================= - * Old New - * --- --- - * 0 x x - nothing - * 1 x B1 - added blob - * 2 x T1 - added tree - * 3 B1 x - removed blob - * 4 B1 B1 - unmodified blob - * 5 B1 B2 - modified blob - * 6 B1 T1 - typechange blob -> tree - * 7 T1 x - removed tree - * 8 T1 B1 - typechange tree -> blob - * 9 T1 T1 - unmodified tree - * 10 T1 T2 - modified tree (implies modified/added/removed blob inside) - */ - -/* Diff with non-work & workdir iterators - * ====================================== - * Old New-WD - * --- ------ - * 0 x x - nothing - * 1 x B1 - added blob - * 2 x Bi - ignored file - * 3 x T1 - added tree - * 4 x Ti - ignored tree - * 5 B1 x - removed blob - * 6 B1 B1 - unmodified blob - * 7 B1 B2 - modified blob - * 8 B1 T1 - typechange blob -> tree - * 9 B1 Ti - removed blob AND ignored tree as separate items - * 10 T1 x - removed tree - * 11 T1 B1 - typechange tree -> blob - * 12 T1 Bi - removed tree AND ignored blob as separate items - * 13 T1 T1 - unmodified tree - * 14 T1 T2 - modified tree (implies modified/added/removed blob inside) - * - * If there is a corresponding blob in the old, Bi is irrelevant - * If there is a corresponding tree in the old, Ti is irrelevant - */ - -/* Checkout From 3 Iterators (2 not workdir, 1 workdir) - * ==================================================== - * - * (Expect == Old HEAD / Desire == What To Checkout / Actual == Workdir) - * - * Expect Desire Actual-WD - * ------ ------ ------ - * 0 x x x - nothing - * 1 x x B1/Bi/T1/Ti - untracked/ignored blob/tree (SAFE) - * 2+ x B1 x - add blob (SAFE) - * 3 x B1 B1 - independently added blob (FORCEABLE-2) - * 4* x B1 B2/Bi/T1/Ti - add blob with content conflict (FORCEABLE-2) - * 5+ x T1 x - add tree (SAFE) - * 6* x T1 B1/Bi - add tree with blob conflict (FORCEABLE-2) - * 7 x T1 T1/i - independently added tree (SAFE+MISSING) - * 8 B1 x x - independently deleted blob (SAFE+MISSING) - * 9- B1 x B1 - delete blob (SAFE) - * 10- B1 x B2 - delete of modified blob (FORCEABLE-1) - * 11 B1 x T1/Ti - independently deleted blob AND untrack/ign tree (SAFE+MISSING !!!) - * 12 B1 B1 x - locally deleted blob (DIRTY || SAFE+CREATE) - * 13+ B1 B2 x - update to deleted blob (SAFE+MISSING) - * 14 B1 B1 B1 - unmodified file (SAFE) - * 15 B1 B1 B2 - locally modified file (DIRTY) - * 16+ B1 B2 B1 - update unmodified blob (SAFE) - * 17 B1 B2 B2 - independently updated blob (FORCEABLE-1) - * 18+ B1 B2 B3 - update to modified blob (FORCEABLE-1) - * 19 B1 B1 T1/Ti - locally deleted blob AND untrack/ign tree (DIRTY) - * 20* B1 B2 T1/Ti - update to deleted blob AND untrack/ign tree (F-1) - * 21+ B1 T1 x - add tree with locally deleted blob (SAFE+MISSING) - * 22* B1 T1 B1 - add tree AND deleted blob (SAFE) - * 23* B1 T1 B2 - add tree with delete of modified blob (F-1) - * 24 B1 T1 T1 - add tree with deleted blob (F-1) - * 25 T1 x x - independently deleted tree (SAFE+MISSING) - * 26 T1 x B1/Bi - independently deleted tree AND untrack/ign blob (F-1) - * 27- T1 x T1 - deleted tree (MAYBE SAFE) - * 28+ T1 B1 x - deleted tree AND added blob (SAFE+MISSING) - * 29 T1 B1 B1 - independently typechanged tree -> blob (F-1) - * 30+ T1 B1 B2 - typechange tree->blob with conflicting blob (F-1) - * 31* T1 B1 T1/T2 - typechange tree->blob (MAYBE SAFE) - * 32+ T1 T1 x - restore locally deleted tree (SAFE+MISSING) - * 33 T1 T1 B1/Bi - locally typechange tree->untrack/ign blob (DIRTY) - * 34 T1 T1 T1/T2 - unmodified tree (MAYBE SAFE) - * 35+ T1 T2 x - update locally deleted tree (SAFE+MISSING) - * 36* T1 T2 B1/Bi - update to tree with typechanged tree->blob conflict (F-1) - * 37 T1 T2 T1/T2/T3 - update to existing tree (MAYBE SAFE) - * - * The number will be followed by ' ' if no change is needed or '+' if the - * case needs to write to disk or '-' if something must be deleted and '*' - * if there should be a delete followed by an write. - * - * There are four tiers of safe cases: - * - SAFE == completely safe to update - * - SAFE+MISSING == safe except the workdir is missing the expect content - * - MAYBE SAFE == safe if workdir tree matches (or is missing) baseline - * content, which is unknown at this point - * - FORCEABLE == conflict unless FORCE is given - * - DIRTY == no conflict but change is not applied unless FORCE - * - * Some slightly unusual circumstances: - * 8 - parent dir is only deleted when file is, so parent will be left if - * empty even though it would be deleted if the file were present - * 11 - core git does not consider this a conflict but attempts to delete T1 - * and gives "unable to unlink file" error yet does not skip the rest - * of the operation - * 12 - without FORCE file is left deleted (i.e. not restored) so new wd is - * dirty (and warning message "D file" is printed), with FORCE, file is - * restored. - * 24 - This should be considered MAYBE SAFE since effectively it is 7 and 8 - * combined, but core git considers this a conflict unless forced. - * 26 - This combines two cases (1 & 25) (and also implied 8 for tree content) - * which are ok on their own, but core git treat this as a conflict. - * If not forced, this is a conflict. If forced, this actually doesn't - * have to write anything and leaves the new blob as an untracked file. - * 32 - This is the only case where the baseline and target values match - * and yet we will still write to the working directory. In all other - * cases, if baseline == target, we don't touch the workdir (it is - * either already right or is "dirty"). However, since this case also - * implies that a ?/B1/x case will exist as well, it can be skipped. - * - * Cases 3, 17, 24, 26, and 29 are all considered conflicts even though - * none of them will require making any updates to the working directory. - */ - -/* expect desire wd - * 1 x x T -> ignored dir OR untracked dir OR parent dir - * 2 x x I -> ignored file - * 3 x x A -> untracked file - * 4 x A x -> add from index (no conflict) - * 5 x A A -> independently added file - * 6 x A B -> add with conflicting file - * 7 A x x -> independently deleted file - * 8 A x A -> delete from index (no conflict) - * 9 A x B -> delete of modified file - * 10 A A x -> locally deleted file - * 11 A A A -> unmodified file (no conflict) - * 12 A A B -> locally modified - * 13 A B x -> update of deleted file - * 14 A B A -> update of unmodified file (no conflict) - * 15 A B B -> independently updated file - * 16 A B C -> update of modified file - */ +/* See docs/checkout-internals.md for more information */ enum { CHECKOUT_ACTION__NONE = 0, @@ -1317,34 +1167,15 @@ int git_checkout_iterator( goto cleanup; } - /* Checkout can be driven either off a target-to-workdir diff or a - * baseline-to-target diff. There are pros and cons of each. - * - * Target-to-workdir means the diff includes every file that could be - * modified, which simplifies bookkeeping, but the code to constantly - * refer back to the baseline gets complicated. - * - * Baseline-to-target has simpler code because the diff defines the - * action to take, but needs special handling for untracked and ignored - * files, if they need to be removed. - * - * I've implemented both versions and opted for the second. + /* Generate baseline-to-target diff which will include an entry for + * every possible update that might need to be made. */ if ((error = git_diff__from_iterators( &data.diff, data.repo, baseline, target, &diff_opts)) < 0) goto cleanup; - /* In order to detect conflicts prior to performing any operations, - * and in order to deal with some order dependencies, checkout is best - * performed with up to four passes through the diff. - * - * 0. Figure out the actions to be taken, - * 1. Remove any files / directories as needed (because alphabetical - * iteration means that an untracked directory will end up sorted - * *after* a blob that should be checked out with the same name), - * 2. Then update all blobs, - * 3. Then update all submodules in case a new .gitmodules blob was - * checked out during pass #2. + /* Loop through diff (and working directory iterator) building a list of + * actions to be taken, plus look for conflicts and send notifications. */ if ((error = checkout_get_actions(&actions, &counts, &data, workdir)) < 0) goto cleanup; @@ -1355,8 +1186,9 @@ int git_checkout_iterator( report_progress(&data, NULL); /* establish 0 baseline */ - /* TODO: add ability to update index entries while checking out */ - + /* To deal with some order dependencies, perform remaining checkout + * in three passes: removes, then update blobs, then update submodules. + */ if (counts[CHECKOUT_ACTION__REMOVE] > 0 && (error = checkout_remove_the_old(actions, &data)) < 0) goto cleanup; |