diff --git a/include/git2/checkout.h b/include/git2/checkout.h index 5eedd7bfd..196962bb9 100644 --- a/include/git2/checkout.h +++ b/include/git2/checkout.h @@ -9,8 +9,7 @@ #include "common.h" #include "types.h" -#include "indexer.h" -#include "strarray.h" +#include "diff.h" /** * @file git2/checkout.h @@ -25,27 +24,28 @@ GIT_BEGIN_DECL * Checkout behavior flags * * In libgit2, the function of checkout is to update the working directory - * to match a target tree given an expected baseline tree. It does not move - * the HEAD commit - you do that separately. Typically the expected tree is - * the (to-be-moved) HEAD commit. + * 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. * - * Checkout examines the differences between the target and expected trees - * plus the current working directory and groups files into five categories: + * Checkout examines the differences between the target tree, the baseline + * tree and the working directory, and groups files into five categories: * * 1. UNMODIFIED - Files that match in all places. - * 2. SAFE - Files where the working directory and the expect content match - * that can be safely updated to the target. + * 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 - * expected content but there is no conflicting change with the target - * tree. An example is a file that doesn't exist in the working - * directory - no data would be lost as a result of writing this file. - * The action to take with these files depends on the options you elect. - * 4. CONFLICTS - Files where changes in the working directory conflicts + * 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. + * or ignored (i.e. only in the working directory, not the other places). * * * You control the actions checkout takes with one of four base strategies: @@ -54,11 +54,11 @@ GIT_BEGIN_DECL * run that you can use to find conflicts, etc. if you wish. * * - `GIT_CHECKOUT_SAFE` is like `git checkout` and only applies changes - * between the expected and target trees to files in category 2. + * between the baseline and target trees to files in category 2. * * - `GIT_CHECKOUT_SAFE_CREATE` also creates files that are missing from the * working directory (category 3), even if there is no change between the - * expected and target trees for those files. See notes below on + * baseline and target trees for those files. See notes below on * emulating `git checkout-index` for some of the subtleties of this. * * - `GIT_CHECKOUT_FORCE` is like `git checkout -f` and will update the @@ -97,7 +97,7 @@ GIT_BEGIN_DECL * 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 - * expected content). The default behavior will cancel on conflicts. + * baseline content). The default behavior will cancel on conflicts. * * To emulate `git checkout-index`, use `GIT_CHECKOUT_SAFE_CREATE` with a * notification callback that cancels the operation if a dirty-but-existing @@ -140,6 +140,9 @@ typedef enum { /** Only update existing files, don't create new ones */ GIT_CHECKOUT_UPDATE_ONLY = (1u << 7), + /** Don't refresh index/config/etc before doing checkout */ + GIT_CHECKOUT_NO_REFRESH = (1u << 8), + /** * THE FOLLOWING OPTIONS ARE NOT YET IMPLEMENTED */ @@ -166,14 +169,14 @@ typedef enum { * receive a callback depend on the `notify_flags` value which is a * combination of these flags. * - * - GIT_CHECKOUT_NOTIFY_CONFLICTS means that conflicting files that would + * - 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 expected + * 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). @@ -202,11 +205,12 @@ typedef enum { * Dirty files will only have work tree flags set. */ typedef enum { - GIT_CHECKOUT_NOTIFY_CONFLICTS = (1u << 0), - GIT_CHECKOUT_NOTIFY_DIRTY = (1u << 1), - GIT_CHECKOUT_NOTIFY_UPDATED = (1u << 2), + GIT_CHECKOUT_NOTIFY_NONE = 0, + GIT_CHECKOUT_NOTIFY_CONFLICT = (1u << 0), + GIT_CHECKOUT_NOTIFY_DIRTY = (1u << 1), + GIT_CHECKOUT_NOTIFY_UPDATED = (1u << 2), GIT_CHECKOUT_NOTIFY_UNTRACKED = (1u << 3), - GIT_CHECKOUT_NOTIFY_IGNORED = (1u << 4), + GIT_CHECKOUT_NOTIFY_IGNORED = (1u << 4), } git_checkout_notify_t; /** @@ -231,11 +235,11 @@ typedef struct git_checkout_opts { unsigned int notify_flags; /** see `git_checkout_notify_t` above */ int (*notify_cb)( + git_checkout_notify_t why, const char *path, - unsigned int status_flags, /** combo of git_status_t values */ - const git_oid *index_oid, - unsigned int checkout_mode, - unsigned int workdir_mode, + const git_diff_file *baseline, + const git_diff_file *target, + const git_diff_file *workdir, void *payload); void *notify_payload; diff --git a/src/checkout.c b/src/checkout.c index 8e8c41bd5..5aeb0624c 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -126,7 +126,7 @@ * 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) expected + * - 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 @@ -146,9 +146,9 @@ * 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 expected and desired values match + * 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 expected == desired, we don't touch the workdir (it is + * 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. * @@ -182,271 +182,460 @@ enum { CHECKOUT_ACTION__UPDATE_SUBMODULE = 4, CHECKOUT_ACTION__CONFLICT = 8, CHECKOUT_ACTION__MAX = 8, - CHECKOUT_ACTION__REMOVE_EMPTY = 16, + CHECKOUT_ACTION__DEFER_REMOVE = 16, + CHECKOUT_ACTION__REMOVE_AND_UPDATE = + (CHECKOUT_ACTION__UPDATE_BLOB | CHECKOUT_ACTION__REMOVE), }; typedef struct { git_repository *repo; git_diff_list *diff; - git_checkout_opts *opts; - const char *pfx; - git_buf *path; + git_checkout_opts opts; + bool opts_free_baseline; + char *pfx; + git_iterator *baseline; + git_pool pool; + git_vector removes; + git_buf path; size_t workdir_len; - bool can_symlink; - int error; + unsigned int strategy; + int can_symlink; size_t total_steps; size_t completed_steps; -} checkout_diff_data; +} checkout_data; static int checkout_notify( - checkout_diff_data *data, + checkout_data *data, git_checkout_notify_t why, const git_diff_delta *delta, - const git_index_entry *wditem) + const git_index_entry *baseitem) { - GIT_UNUSED(data); - GIT_UNUSED(why); - GIT_UNUSED(delta); - GIT_UNUSED(wditem); - return 0; + git_diff_file basefile; + const git_diff_file *baseline = NULL, *target = NULL, *workdir = NULL; + + if (!data->opts.notify_cb) + return 0; + + if ((why & data->opts.notify_flags) == 0) + return 0; + + if (baseitem) { + memset(&basefile, 0, sizeof(basefile)); + + git_oid_cpy(&basefile.oid, &baseitem->oid); + basefile.path = baseitem->path; + basefile.size = baseitem->file_size; + basefile.flags = GIT_DIFF_FILE_VALID_OID; + basefile.mode = baseitem->mode; + + baseline = &basefile; + } + + if ((why & GIT_CHECKOUT__NOTIFY_CONFLICT_TREE) != 0) { + /* baseitem is a blob that conflicts with a tree in the workdir */ + } else { + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: + case GIT_DELTA_MODIFIED: + case GIT_DELTA_TYPECHANGE: + default: + target = &delta->old_file; + workdir = &delta->new_file; + break; + case GIT_DELTA_ADDED: + case GIT_DELTA_IGNORED: + case GIT_DELTA_UNTRACKED: + workdir = &delta->new_file; + break; + case GIT_DELTA_DELETED: + target = &delta->old_file; + break; + } + } + + return data->opts.notify_cb( + why, delta->old_file.path, + baseline, target, workdir, + data->opts.notify_payload); } static bool checkout_is_workdir_modified( - checkout_diff_data *data, - const git_diff_file *item, - const git_index_entry *wditem) + checkout_data *data, + const git_diff_file *wditem, + const git_index_entry *baseitem) { git_oid oid; - if (item->size != wditem->file_size) + if (wditem->size != baseitem->file_size) return true; if (git_diff__oid_for_file( - data->repo, wditem->path, wditem->mode, - wditem->file_size, &oid) < 0) + data->repo, wditem->path, wditem->mode, wditem->size, &oid) < 0) return false; - return (git_oid_cmp(&item->oid, &oid) != 0); + return (git_oid_cmp(&baseitem->oid, &oid) != 0); } -static int checkout_action_for_delta( - checkout_diff_data *data, - const git_diff_delta *delta, - const git_index_entry *wditem) +#define CHECKOUT_ACTION_IF(FLAG,YES,NO) \ + ((data->strategy & GIT_CHECKOUT_##FLAG) ? CHECKOUT_ACTION__##YES : CHECKOUT_ACTION__##NO) + +static const char *checkout_action_name_debug(int act) { - int action = CHECKOUT_ACTION__NONE; - unsigned int strat = data->opts->checkout_strategy; - int safe = ((strat & GIT_CHECKOUT_SAFE) != 0) ? - CHECKOUT_ACTION__UPDATE_BLOB : CHECKOUT_ACTION__NONE; - int force = ((strat & GIT_CHECKOUT_FORCE) != 0) ? - CHECKOUT_ACTION__UPDATE_BLOB : CHECKOUT_ACTION__CONFLICT; + if (act & CHECKOUT_ACTION__CONFLICT) + return "CONFLICT"; - /* nothing in workdir, so this is pretty easy */ - if (!wditem) { - switch (delta->status) { - case GIT_DELTA_UNMODIFIED: /* case 12 */ - if ((strat & GIT_CHECKOUT_SAFE_CREATE) != 0) - action = CHECKOUT_ACTION__UPDATE_BLOB; - - if (checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, NULL)) - return GIT_EUSER; - break; - case GIT_DELTA_ADDED: /* case 2 or 28 (and 5 but not really) */ - case GIT_DELTA_MODIFIED: /* case 13 (and 35 but not really) */ - action = safe; - break; - case GIT_DELTA_TYPECHANGE: /* case 21 (B->T) and 28 (T->B)*/ - if (!S_ISDIR(delta->new_file.mode)) - action = safe; - break; - case GIT_DELTA_DELETED: /* case 8 or 25 */ - default: /* impossible */ break; - } + if (act & CHECKOUT_ACTION__REMOVE) { + if (act & CHECKOUT_ACTION__UPDATE_BLOB) + return "REMOVE+UPDATE"; + if (act & CHECKOUT_ACTION__UPDATE_SUBMODULE) + return "REMOVE+UPDATE SUB"; + return "REMOVE"; } - - /* workdir has a directory where this entry should be */ - else if (S_ISDIR(wditem->mode)) { - switch (delta->status) { - case GIT_DELTA_UNMODIFIED: /* case 19 or 24 (or 34 but not really) */ - if (checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, NULL) || - checkout_notify( - data, GIT_CHECKOUT_NOTIFY_UNTRACKED, NULL, wditem)) - return GIT_EUSER; - break; - case GIT_DELTA_ADDED:/* case 4 (and 7 for dir) */ - case GIT_DELTA_MODIFIED: /* case 20 (or 37 but not really) */ - if (!S_ISDIR(delta->new_file.mode)) - action = force; - break; - case GIT_DELTA_DELETED: /* case 11 (and 27 for dir) */ - if (!S_ISDIR(delta->old_file.mode) && - checkout_notify( - data, GIT_CHECKOUT_NOTIFY_UNTRACKED, NULL, wditem)) - return GIT_EUSER; - break; - case GIT_DELTA_TYPECHANGE: /* case 24 or 31 */ - /* For typechange to dir, dir is already created so no action */ - - /* For typechange to blob, remove dir and add blob, but it is - * not safe to remove dir if it contains modified files. - * However, safely removing child files will remove the parent - * directory if is it left empty, so we only need to remove dir - * if it is already empty and has no children to remove. - */ - if (S_ISDIR(delta->old_file.mode)) { - action = safe; - if (action != 0) - action |= CHECKOUT_ACTION__REMOVE | - CHECKOUT_ACTION__REMOVE_EMPTY; - } - break; - default: /* impossible */ break; - } + if (act & CHECKOUT_ACTION__DEFER_REMOVE) { + if (act & CHECKOUT_ACTION__UPDATE_BLOB) + return "UPDATE (WITH REMOVE)"; + if (act & CHECKOUT_ACTION__UPDATE_SUBMODULE) + return "UPDATE SUB (WITH REMOVE)"; + return "DEFERRED REMOVE"; } + if (act & CHECKOUT_ACTION__UPDATE_BLOB) + return "UPDATE"; + if (act & CHECKOUT_ACTION__UPDATE_SUBMODULE) + return "UPDATE SUB"; + assert(act == 0); + return "NONE"; +} - /* workdir has a blob (or submodule) */ - else { - switch (delta->status) { - case GIT_DELTA_UNMODIFIED: /* case 14/15 or 33 */ - if (S_ISDIR(delta->old_file.mode) || - checkout_is_workdir_modified(data, &delta->old_file, wditem)) - { - if (checkout_notify( - data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, wditem)) - return GIT_EUSER; +static int checkout_action_common( + checkout_data *data, + int action, + const git_diff_delta *delta, + const git_index_entry *wd) +{ + git_checkout_notify_t notify = GIT_CHECKOUT_NOTIFY_NONE; - if (force) - action = CHECKOUT_ACTION__UPDATE_BLOB; - } - break; - case GIT_DELTA_ADDED: /* case 3, 4 or 6 */ - action = force; - break; - case GIT_DELTA_DELETED: /* case 9 or 10 (or 26 but not really) */ - if (checkout_is_workdir_modified(data, &delta->old_file, wditem)) - action = force ? - CHECKOUT_ACTION__REMOVE : CHECKOUT_ACTION__CONFLICT; - else - action = safe ? - CHECKOUT_ACTION__REMOVE : CHECKOUT_ACTION__NONE; - break; - case GIT_DELTA_MODIFIED: /* case 16, 17, 18 (or 36 but not really) */ - if (checkout_is_workdir_modified(data, &delta->old_file, wditem)) - action = force; - else - action = safe; - break; - case GIT_DELTA_TYPECHANGE: /* case 22, 23, 29, 30 */ - if (S_ISDIR(delta->old_file.mode) || - checkout_is_workdir_modified(data, &delta->old_file, wditem)) - action = force; - else - action = safe; - break; - default: /* impossible */ break; - } - } + if (action <= 0) + return action; - if (action > 0 && (strat & GIT_CHECKOUT_UPDATE_ONLY) != 0) + if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) action = (action & ~CHECKOUT_ACTION__REMOVE); - if (action > 0 && (action & CHECKOUT_ACTION__UPDATE_BLOB) != 0) { + if ((action & CHECKOUT_ACTION__UPDATE_BLOB) != 0) { if (S_ISGITLINK(delta->new_file.mode)) action = (action & ~CHECKOUT_ACTION__UPDATE_BLOB) | CHECKOUT_ACTION__UPDATE_SUBMODULE; - if (checkout_notify(data, GIT_CHECKOUT_NOTIFY_UPDATED, delta, wditem)) - return GIT_EUSER; + notify = GIT_CHECKOUT_NOTIFY_UPDATED; } - if ((action & CHECKOUT_ACTION__CONFLICT) != 0) { - if (checkout_notify( - data, GIT_CHECKOUT_NOTIFY_CONFLICTS, delta, wditem)) - return GIT_EUSER; - } + if ((action & CHECKOUT_ACTION__CONFLICT) != 0) + notify = GIT_CHECKOUT_NOTIFY_CONFLICT; + + if (notify != GIT_CHECKOUT_NOTIFY_NONE && + checkout_notify(data, notify, delta, wd) != 0) + return GIT_EUSER; return action; } -static int checkout_track_wd( - int *cmp_out, - const git_index_entry **wditem_ptr, - checkout_diff_data *data, - git_iterator *actual, - git_diff_delta *delta, - git_vector *pathspec) +static int checkout_action_no_wd( + checkout_data *data, + const git_diff_delta *delta) { - int cmp = -1; - const git_index_entry *wditem = *wditem_ptr; + int action = CHECKOUT_ACTION__NONE; - while (wditem) { - bool notify = false; - - cmp = data->diff->strcomp(delta->new_file.path, wditem->path); - if (cmp >= 0) - break; - - if (!git_pathspec_match_path( - pathspec, wditem->path, false, actual->ignore_case)) - notify = false; - - else if (S_ISDIR(wditem->mode)) { - cmp = data->diff->pfxcomp(delta->new_file.path, wditem->path); - - if (cmp < 0) - notify = true; /* notify untracked/ignored tree */ - else if (!cmp) { - /* workdir is prefix of current, so dive in and continue */ - if (git_iterator_advance_into_directory(actual, &wditem) < 0) - return -1; - continue; - } - else /* how can the wditem->path be < 0 but a prefix be > 0 */ - assert(false); - } else - notify = true; /* notify untracked/ignored blob */ - - if (notify && checkout_notify( - data, git_iterator_current_is_ignored(actual) ? - GIT_CHECKOUT_NOTIFY_IGNORED : GIT_CHECKOUT_NOTIFY_UNTRACKED, - NULL, wditem)) + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: /* case 12 */ + if (checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, NULL)) return GIT_EUSER; - - if (git_iterator_advance(actual, wditem_ptr) < 0) - break; - - wditem = *wditem_ptr; - cmp = -1; + action = CHECKOUT_ACTION_IF(SAFE_CREATE, UPDATE_BLOB, NONE); + break; + case GIT_DELTA_ADDED: /* case 2 or 28 (and 5 but not really) */ + case GIT_DELTA_MODIFIED: /* case 13 (and 35 but not really) */ + action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + break; + case GIT_DELTA_TYPECHANGE: /* case 21 (B->T) and 28 (T->B)*/ + if (delta->new_file.mode == GIT_FILEMODE_TREE) + action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + break; + case GIT_DELTA_DELETED: /* case 8 or 25 */ + default: /* impossible */ + break; } - *cmp_out = cmp; + return checkout_action_common(data, action, delta, NULL); +} + +static int checkout_action_wd_only( + checkout_data *data, + git_iterator *workdir, + const git_index_entry *wd, + git_vector *pathspec) +{ + bool ignored, remove; + git_checkout_notify_t notify = GIT_CHECKOUT_NOTIFY_NONE; + + if (!git_pathspec_match_path( + pathspec, wd->path, false, workdir->ignore_case)) + return 0; + + ignored = git_iterator_current_is_ignored(workdir); + + if (ignored) { + notify = GIT_CHECKOUT_NOTIFY_IGNORED; + remove = ((data->strategy & GIT_CHECKOUT_REMOVE_IGNORED) != 0); + } else { + notify = GIT_CHECKOUT_NOTIFY_UNTRACKED; + remove = ((data->strategy & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0); + } + + if (checkout_notify(data, notify, NULL, wd)) + return GIT_EUSER; + + if (remove) { + char *path = git_pool_strdup(&data->pool, wd->path); + GITERR_CHECK_ALLOC(path); + + if (git_vector_insert(&data->removes, path) < 0) + return -1; + } return 0; } +static int checkout_action_with_wd( + checkout_data *data, + const git_diff_delta *delta, + const git_index_entry *wd) +{ + int action = CHECKOUT_ACTION__NONE; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: /* case 14/15 or 33 */ + if (S_ISDIR(delta->old_file.mode) || + checkout_is_workdir_modified(data, &delta->old_file, wd)) + { + if (checkout_notify( + data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, wd)) + return GIT_EUSER; + action = CHECKOUT_ACTION_IF(FORCE, UPDATE_BLOB, NONE); + } + break; + case GIT_DELTA_ADDED: /* case 3, 4 or 6 */ + action = CHECKOUT_ACTION_IF(FORCE, UPDATE_BLOB, CONFLICT); + break; + case GIT_DELTA_DELETED: /* case 9 or 10 (or 26 but not really) */ + if (checkout_is_workdir_modified(data, &delta->old_file, wd)) + action = CHECKOUT_ACTION_IF(FORCE, REMOVE, CONFLICT); + else + action = CHECKOUT_ACTION_IF(SAFE, REMOVE, NONE); + break; + case GIT_DELTA_MODIFIED: /* case 16, 17, 18 (or 36 but not really) */ + if (checkout_is_workdir_modified(data, &delta->old_file, wd)) + action = CHECKOUT_ACTION_IF(FORCE, UPDATE_BLOB, CONFLICT); + else + action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + break; + case GIT_DELTA_TYPECHANGE: /* case 22, 23, 29, 30 */ + if (delta->new_file.mode == GIT_FILEMODE_TREE) + action = CHECKOUT_ACTION_IF(FORCE, REMOVE, CONFLICT); + else if (checkout_is_workdir_modified(data, &delta->old_file, wd)) + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + else + action = CHECKOUT_ACTION_IF(SAFE, REMOVE_AND_UPDATE, NONE); + break; + default: /* impossible */ + break; + } + + return checkout_action_common(data, action, delta, wd); +} + +static int checkout_action_with_wd_blocker( + checkout_data *data, + const git_diff_delta *delta, + const git_index_entry *wd) +{ + int action = CHECKOUT_ACTION__NONE; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: + /* should show delta as dirty / deleted */ + if (checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, wd)) + return GIT_EUSER; + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, NONE); + break; + case GIT_DELTA_ADDED: + case GIT_DELTA_MODIFIED: + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + break; + case GIT_DELTA_DELETED: + action = CHECKOUT_ACTION_IF(FORCE, REMOVE, CONFLICT); + break; + case GIT_DELTA_TYPECHANGE: + /* not 100% certain about this... */ + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + break; + default: /* impossible */ + break; + } + + return checkout_action_common(data, action, delta, wd); +} + +static int checkout_action_with_wd_dir( + checkout_data *data, + const git_diff_delta *delta, + const git_index_entry *wd) +{ + int action = CHECKOUT_ACTION__NONE; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: /* case 19 or 24 (or 34 but not really) */ + if (checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, NULL) || + checkout_notify( + data, GIT_CHECKOUT_NOTIFY_UNTRACKED, NULL, wd)) + return GIT_EUSER; + break; + case GIT_DELTA_ADDED:/* case 4 (and 7 for dir) */ + case GIT_DELTA_MODIFIED: /* case 20 (or 37 but not really) */ + if (delta->new_file.mode != GIT_FILEMODE_TREE) + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + break; + case GIT_DELTA_DELETED: /* case 11 (and 27 for dir) */ + if (delta->old_file.mode != GIT_FILEMODE_TREE && + checkout_notify( + data, GIT_CHECKOUT_NOTIFY_UNTRACKED, NULL, wd)) + return GIT_EUSER; + break; + case GIT_DELTA_TYPECHANGE: /* case 24 or 31 */ + /* For typechange to dir, dir is already created so no action */ + + /* For typechange to blob, remove dir and add blob, but it is + * not safe to remove dir if it contains modified files. + * However, safely removing child files will remove the parent + * directory if is it left empty, so we can defer removing the + * dir and it will succeed if no children are left. + */ + if (delta->old_file.mode == GIT_FILEMODE_TREE) { + action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + if (action != CHECKOUT_ACTION__NONE) + action |= CHECKOUT_ACTION__DEFER_REMOVE; + } + break; + default: /* impossible */ + break; + } + + return checkout_action_common(data, action, delta, wd); +} + +static int checkout_action( + checkout_data *data, + git_diff_delta *delta, + git_iterator *workdir, + const git_index_entry **wditem_ptr, + git_vector *pathspec) +{ + const git_index_entry *wd = *wditem_ptr; + int cmp = -1, act; + int (*strcomp)(const char *, const char *) = data->diff->strcomp; + int (*pfxcomp)(const char *str, const char *pfx) = data->diff->pfxcomp; + + /* move workdir iterator to follow along with deltas */ + + while (1) { + if (!wd) + return checkout_action_no_wd(data, delta); + + cmp = strcomp(wd->path, delta->old_file.path); + + /* 1. wd before delta ("a/a" before "a/b") + * 2. wd prefixes delta & should expand ("a/" before "a/b") + * 3. wd prefixes delta & cannot expand ("a/b" before "a/b/c") + * 4. wd equals delta ("a/b" and "a/b") + * 5. wd after delta & delta prefixes wd ("a/b/c" after "a/b/" or "a/b") + * 6. wd after delta ("a/c" after "a/b") + */ + + if (cmp < 0) { + cmp = pfxcomp(delta->old_file.path, wd->path); + + if (cmp == 0) { + if (wd->mode == GIT_FILEMODE_TREE) { + /* case 2 - descend in wd */ + if (git_iterator_advance_into_directory(workdir, &wd) < 0) + goto fail; + continue; + } + + /* case 3 - wd contains non-dir where dir expected */ + act = checkout_action_with_wd_blocker(data, delta, wd); + *wditem_ptr = git_iterator_advance(workdir, &wd) ? NULL : wd; + return act; + } + + /* case 1 - handle wd item (if it matches pathspec) */ + if (checkout_action_wd_only(data, workdir, wd, pathspec) < 0 || + git_iterator_advance(workdir, &wd) < 0) + goto fail; + + *wditem_ptr = wd; + continue; + } + + if (cmp == 0) { + /* case 4 */ + act = checkout_action_with_wd(data, delta, wd); + *wditem_ptr = git_iterator_advance(workdir, &wd) ? NULL : wd; + return act; + } + + cmp = pfxcomp(wd->path, delta->old_file.path); + + if (cmp == 0) { /* case 5 */ + if (delta->status == GIT_DELTA_TYPECHANGE && + (delta->new_file.mode == GIT_FILEMODE_TREE || + delta->new_file.mode == GIT_FILEMODE_COMMIT || + delta->old_file.mode == GIT_FILEMODE_TREE || + delta->old_file.mode == GIT_FILEMODE_COMMIT)) + { + act = checkout_action_with_wd(data, delta, wd); + *wditem_ptr = git_iterator_advance(workdir, &wd) ? NULL : wd; + return act; + } + + return checkout_action_with_wd_dir(data, delta, wd); + } + + /* case 6 - wd is after delta */ + return checkout_action_no_wd(data, delta); + } + +fail: + *wditem_ptr = NULL; + return -1; +} + static int checkout_get_actions( uint32_t **actions_ptr, size_t **counts_ptr, - checkout_diff_data *data) + checkout_data *data, + git_iterator *workdir) { int error = 0; - git_iterator *actual = NULL; const git_index_entry *wditem; git_vector pathspec = GIT_VECTOR_INIT, *deltas; git_pool pathpool = GIT_POOL_INIT_STRINGPOOL; git_diff_delta *delta; size_t i, *counts = NULL; uint32_t *actions = NULL; - bool allow_conflicts = - ((data->opts->checkout_strategy & GIT_CHECKOUT_ALLOW_CONFLICTS) != 0); - if (data->opts->paths.count > 0 && - git_pathspec_init(&pathspec, &data->opts->paths, &pathpool) < 0) + if (data->opts.paths.count > 0 && + git_pathspec_init(&pathspec, &data->opts.paths, &pathpool) < 0) return -1; - if ((error = git_iterator_for_workdir_range( - &actual, data->repo, data->pfx, data->pfx)) < 0 || - (error = git_iterator_current(actual, &wditem)) < 0) + if ((error = git_iterator_current(workdir, &wditem)) < 0) goto fail; deltas = &data->diff->deltas; @@ -460,23 +649,13 @@ static int checkout_get_actions( } git_vector_foreach(deltas, i, delta) { - int cmp = -1, act; + int act = checkout_action(data, delta, workdir, &wditem, &pathspec); - /* move workdir iterator to follow along with deltas */ - if (wditem != NULL && - (error = checkout_track_wd( - &cmp, &wditem, data, actual, delta, &pathspec)) < 0) - goto fail; - - act = checkout_action_for_delta(data, delta, !cmp ? wditem : NULL); if (act < 0) { error = act; goto fail; } - if (!cmp && git_iterator_advance(actual, &wditem) < 0) - wditem = NULL; - actions[i] = act; if (act & CHECKOUT_ACTION__REMOVE) @@ -489,14 +668,17 @@ static int checkout_get_actions( counts[CHECKOUT_ACTION__CONFLICT]++; } - if (counts[CHECKOUT_ACTION__CONFLICT] > 0 && !allow_conflicts) { + counts[CHECKOUT_ACTION__REMOVE] += data->removes.length; + + if (counts[CHECKOUT_ACTION__CONFLICT] > 0 && + (data->strategy & GIT_CHECKOUT_ALLOW_CONFLICTS) == 0) + { giterr_set(GITERR_CHECKOUT, "%d conflicts prevent checkout", (int)counts[CHECKOUT_ACTION__CONFLICT]); error = -1; goto fail; } - git_iterator_free(actual); git_pathspec_free(&pathspec); git_pool_clear(&pathpool); @@ -508,7 +690,6 @@ fail: *actions_ptr = NULL; git__free(actions); - git_iterator_free(actual); git_pathspec_free(&pathspec); git_pool_clear(&pathpool); @@ -603,7 +784,7 @@ cleanup: } static int blob_content_to_link( - git_blob *blob, const char *path, bool can_symlink) + git_blob *blob, const char *path, int can_symlink) { git_buf linktarget = GIT_BUF_INIT; int error; @@ -622,16 +803,16 @@ static int blob_content_to_link( } static int checkout_submodule( - checkout_diff_data *data, + checkout_data *data, const git_diff_file *file) { /* Until submodules are supported, UPDATE_ONLY means do nothing here */ - if ((data->opts->checkout_strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) + if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) return 0; if (git_futils_mkdir( file->path, git_repository_workdir(data->repo), - data->opts->dir_mode, GIT_MKDIR_PATH) < 0) + data->opts.dir_mode, GIT_MKDIR_PATH) < 0) return -1; /* TODO: Support checkout_strategy options. Two circumstances: @@ -647,24 +828,24 @@ static int checkout_submodule( } static void report_progress( - checkout_diff_data *data, + checkout_data *data, const char *path) { - if (data->opts->progress_cb) - data->opts->progress_cb( + if (data->opts.progress_cb) + data->opts.progress_cb( path, data->completed_steps, data->total_steps, - data->opts->progress_payload); + data->opts.progress_payload); } static int checkout_blob( - checkout_diff_data *data, + checkout_data *data, const git_diff_file *file) { int error = 0; git_blob *blob; - git_buf_truncate(data->path, data->workdir_len); - if (git_buf_puts(data->path, file->path) < 0) + git_buf_truncate(&data->path, data->workdir_len); + if (git_buf_puts(&data->path, file->path) < 0) return -1; if ((error = git_blob_lookup(&blob, data->repo, &file->oid)) < 0) @@ -672,42 +853,45 @@ static int checkout_blob( if (S_ISLNK(file->mode)) error = blob_content_to_link( - blob, git_buf_cstr(data->path), data->can_symlink); + blob, git_buf_cstr(&data->path), data->can_symlink); else error = blob_content_to_file( - blob, git_buf_cstr(data->path), file->mode, data->opts); + blob, git_buf_cstr(&data->path), file->mode, &data->opts); git_blob_free(blob); + /* if we try to create the blob and an existing directory blocks it from + * being written, then there must have been a typechange conflict in a + * parent directory - suppress the error and try to continue. + */ + if ((data->strategy & GIT_CHECKOUT_ALLOW_CONFLICTS) != 0 && + (error == GIT_ENOTFOUND || error == GIT_EEXISTS)) + { + giterr_clear(); + error = 0; + } + return error; } static int checkout_remove_the_old( unsigned int *actions, - checkout_diff_data *data) + checkout_data *data) { int error = 0; git_diff_delta *delta; + const char *str; size_t i; - const char *workdir = git_buf_cstr(data->path); + const char *workdir = git_buf_cstr(&data->path); + uint32_t flg = GIT_RMDIR_EMPTY_PARENTS | + GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_REMOVE_BLOCKERS; - git_buf_truncate(data->path, data->workdir_len); + git_buf_truncate(&data->path, data->workdir_len); git_vector_foreach(&data->diff->deltas, i, delta) { if (actions[i] & CHECKOUT_ACTION__REMOVE) { - uint32_t flg = GIT_RMDIR_EMPTY_PARENTS; - bool empty_only = - ((actions[i] & CHECKOUT_ACTION__REMOVE_EMPTY) != 0); - - if (!empty_only) - flg |= GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_REMOVE_BLOCKERS; - error = git_futils_rmdir_r(delta->old_file.path, workdir, flg); - - /* ignore error if empty_only, because that just means we lacked - * info to do the right thing when the action was picked. - */ - if (error < 0 && !empty_only) + if (error < 0) return error; data->completed_steps++; @@ -715,19 +899,57 @@ static int checkout_remove_the_old( } } + git_vector_foreach(&data->removes, i, str) { + error = git_futils_rmdir_r(str, workdir, flg); + if (error < 0) + return error; + + data->completed_steps++; + report_progress(data, str); + } + return 0; } +static int checkout_deferred_remove(git_repository *repo, const char *path) +{ +#if 0 + int error = git_futils_rmdir_r( + path, git_repository_workdir(repo), GIT_RMDIR_EMPTY_PARENTS); + + if (error == GIT_ENOTFOUND) { + error = 0; + giterr_clear(); + } + + return error; +#else + GIT_UNUSED(repo); + GIT_UNUSED(path); + return 0; +#endif +} + static int checkout_create_the_new( unsigned int *actions, - checkout_diff_data *data) + checkout_data *data) { + int error = 0; git_diff_delta *delta; size_t i; git_vector_foreach(&data->diff->deltas, i, delta) { + if (actions[i] & CHECKOUT_ACTION__DEFER_REMOVE) { + /* this had a blocker directory that should only be removed iff + * all of the contents of the directory were safely removed + */ + if ((error = checkout_deferred_remove( + data->repo, delta->old_file.path)) < 0) + return error; + } + if (actions[i] & CHECKOUT_ACTION__UPDATE_BLOB) { - int error = checkout_blob(data, &delta->new_file); + error = checkout_blob(data, &delta->new_file); if (error < 0) return error; @@ -741,12 +963,22 @@ static int checkout_create_the_new( static int checkout_create_submodules( unsigned int *actions, - checkout_diff_data *data) + checkout_data *data) { + int error = 0; git_diff_delta *delta; size_t i; git_vector_foreach(&data->diff->deltas, i, delta) { + if (actions[i] & CHECKOUT_ACTION__DEFER_REMOVE) { + /* this has a blocker directory that should only be removed iff + * all of the contents of the directory were safely removed + */ + if ((error = checkout_deferred_remove( + data->repo, delta->old_file.path)) < 0) + return error; + } + if (actions[i] & CHECKOUT_ACTION__UPDATE_SUBMODULE) { int error = checkout_submodule(data, &delta->new_file); if (error < 0) @@ -760,71 +992,177 @@ static int checkout_create_submodules( return 0; } -static int retrieve_symlink_caps(git_repository *repo, bool *out) +static int checkout_lookup_head_tree(git_tree **out, git_repository *repo) { - git_config *cfg; - int error, can_symlink = 0; + int error = 0; + git_reference *ref = NULL; + git_object *head; - if (git_repository_config__weakptr(&cfg, repo) < 0) - return -1; + if (!(error = git_repository_head(&ref, repo)) && + !(error = git_reference_peel(&head, ref, GIT_OBJ_TREE))) + *out = (git_tree *)head; - error = git_config_get_bool(&can_symlink, cfg, "core.symlinks"); - - /* If "core.symlinks" is not found anywhere, default to true. */ - if (error == GIT_ENOTFOUND) { - can_symlink = true; - error = 0; - } - - *out = can_symlink; + git_reference_free(ref); return error; } -int git_checkout__from_iterators( - git_iterator *desired, - git_iterator *expected, - git_checkout_opts *opts, - const char *pathspec_pfx) +static void checkout_data_clear(checkout_data *data) +{ + if (data->opts_free_baseline) { + git_tree_free(data->opts.baseline); + data->opts.baseline = NULL; + } + + git_vector_free(&data->removes); + git_pool_clear(&data->pool); + + git__free(data->pfx); + data->pfx = NULL; + + git_buf_free(&data->path); +} + +static int checkout_data_init( + checkout_data *data, + git_repository *repo, + git_checkout_opts *proposed) { int error = 0; - checkout_diff_data data; - git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; - git_buf workdir = GIT_BUF_INIT; - uint32_t *actions = NULL; - size_t *counts = NULL; + git_config *cfg; - memset(&data, 0, sizeof(data)); + memset(data, 0, sizeof(*data)); - data.repo = git_iterator_owner(desired); - if (!data.repo) data.repo = git_iterator_owner(expected); - if (!data.repo) { + if (!repo) { giterr_set(GITERR_CHECKOUT, "Cannot checkout nothing"); return -1; } - diff_opts.flags = - GIT_DIFF_INCLUDE_UNMODIFIED | GIT_DIFF_INCLUDE_UNTRACKED | - GIT_DIFF_INCLUDE_TYPECHANGE | GIT_DIFF_SKIP_BINARY_CHECK; - if (opts->paths.count > 0) - diff_opts.pathspec = opts->paths; + if ((error = git_repository__ensure_not_bare(repo, "checkout")) < 0) + return error; - /* By analyzing the cases above, it becomes clear that checkout can work - * off the diff between the desired and expected trees, instead of using - * a work dir diff. This should make things somewhat faster... + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + return error; + + data->repo = repo; + + GITERR_CHECK_VERSION( + proposed, GIT_CHECKOUT_OPTS_VERSION, "git_checkout_opts"); + + if (!proposed) + GIT_INIT_STRUCTURE(&data->opts, GIT_CHECKOUT_OPTS_VERSION); + else + memmove(&data->opts, proposed, sizeof(git_checkout_opts)); + + /* if you are forcing, definitely allow safe updates */ + + if ((data->opts.checkout_strategy & GIT_CHECKOUT_FORCE) != 0) + data->opts.checkout_strategy |= GIT_CHECKOUT_SAFE_CREATE; + if ((data->opts.checkout_strategy & GIT_CHECKOUT_SAFE_CREATE) != 0) + data->opts.checkout_strategy |= GIT_CHECKOUT_SAFE; + + data->strategy = data->opts.checkout_strategy; + + /* opts->disable_filters is false by default */ + + if (!data->opts.dir_mode) + data->opts.dir_mode = GIT_DIR_MODE; + + if (!data->opts.file_open_flags) + data->opts.file_open_flags = O_CREAT | O_TRUNC | O_WRONLY; + + data->pfx = git_pathspec_prefix(&data->opts.paths); + + error = git_config_get_bool(&data->can_symlink, cfg, "core.symlinks"); + if (error < 0) { + if (error != GIT_ENOTFOUND) + goto cleanup; + + /* If "core.symlinks" is not found anywhere, default to true. */ + data->can_symlink = true; + giterr_clear(); + error = 0; + } + + if (!data->opts.baseline) { + data->opts_free_baseline = true; + if ((error = checkout_lookup_head_tree(&data->opts.baseline, repo)) < 0) + goto cleanup; + } + + if ((error = git_vector_init(&data->removes, 0, git__strcmp_cb)) < 0 || + (error = git_pool_init(&data->pool, 1, 0)) < 0 || + (error = git_buf_puts(&data->path, git_repository_workdir(repo))) < 0) + goto cleanup; + + data->workdir_len = git_buf_len(&data->path); + +cleanup: + if (error < 0) + checkout_data_clear(data); + + return error; +} + +int git_checkout_iterator( + git_iterator *target, + git_checkout_opts *opts) +{ + int error = 0; + git_iterator *baseline = NULL, *workdir = NULL; + checkout_data data = {0}; + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + uint32_t *actions = NULL; + size_t *counts = NULL; + + /* initialize structures and options */ + error = checkout_data_init(&data, git_iterator_owner(target), opts); + if (error < 0) + return error; + + diff_opts.flags = + GIT_DIFF_INCLUDE_UNMODIFIED | + GIT_DIFF_INCLUDE_UNTRACKED | + GIT_DIFF_RECURSE_UNTRACKED_DIRS | /* needed to match baseline */ + GIT_DIFF_INCLUDE_IGNORED | + GIT_DIFF_INCLUDE_TYPECHANGE | + GIT_DIFF_INCLUDE_TYPECHANGE_TREES | + GIT_DIFF_SKIP_BINARY_CHECK; + if (data.opts.paths.count > 0) + diff_opts.pathspec = data.opts.paths; + + /* set up iterators */ + if ((error = git_iterator_reset(target, data.pfx, data.pfx)) < 0 || + (error = git_iterator_for_workdir_range( + &workdir, data.repo, data.pfx, data.pfx)) < 0 || + (error = git_iterator_for_tree_range( + &baseline, data.opts.baseline, data.pfx, data.pfx)) < 0) + goto cleanup; + + /* Handle case insensitivity for baseline if necessary */ + if (workdir->ignore_case && !baseline->ignore_case) { + if ((error = git_iterator_spoolandsort( + &baseline, baseline, git_index_entry__cmp_icase, true)) < 0) + 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. */ if ((error = git_diff__from_iterators( - &data.diff, data.repo, expected, desired, &diff_opts)) < 0) + &data.diff, data.repo, baseline, target, &diff_opts)) < 0) goto cleanup; - if ((error = git_buf_puts(&workdir, git_repository_workdir(data.repo))) < 0) - goto cleanup; - - data.opts = opts; - data.pfx = pathspec_pfx; - data.path = &workdir; - data.workdir_len = git_buf_len(&workdir); - /* 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. @@ -837,16 +1175,13 @@ int git_checkout__from_iterators( * 3. Then update all submodules in case a new .gitmodules blob was * checked out during pass #2. */ - if ((error = checkout_get_actions(&actions, &counts, &data)) < 0) + if ((error = checkout_get_actions(&actions, &counts, &data, workdir)) < 0) goto cleanup; data.total_steps = counts[CHECKOUT_ACTION__REMOVE] + counts[CHECKOUT_ACTION__UPDATE_BLOB] + counts[CHECKOUT_ACTION__UPDATE_SUBMODULE]; - if ((error = retrieve_symlink_caps(data.repo, &data.can_symlink)) < 0) - goto cleanup; - report_progress(&data, NULL); /* establish 0 baseline */ /* TODO: add ability to update index entries while checking out */ @@ -870,107 +1205,35 @@ cleanup: giterr_clear(); git_diff_list_free(data.diff); - git_buf_free(&workdir); + git_iterator_free(workdir); + git_iterator_free(data.baseline); git__free(actions); git__free(counts); + checkout_data_clear(&data); return error; } -static int checkout_lookup_head_tree(git_tree **out, git_repository *repo) -{ - int error = 0; - git_reference *ref = NULL; - git_object *head; - - if (!(error = git_repository_head(&ref, repo)) && - !(error = git_reference_peel(&head, ref, GIT_OBJ_TREE))) - *out = (git_tree *)head; - - git_reference_free(ref); - - return error; -} - -static int checkout_normalize_opts( - git_checkout_opts *normalized, - char **pfx, - git_repository *repo, - git_checkout_opts *proposed) -{ - assert(normalized); - - GITERR_CHECK_VERSION( - proposed, GIT_CHECKOUT_OPTS_VERSION, "git_checkout_opts"); - - if (!proposed) - GIT_INIT_STRUCTURE(normalized, GIT_CHECKOUT_OPTS_VERSION); - else - memmove(normalized, proposed, sizeof(git_checkout_opts)); - - /* if you are forcing, definitely allow safe updates */ - - if ((normalized->checkout_strategy & GIT_CHECKOUT_FORCE) != 0) - normalized->checkout_strategy |= GIT_CHECKOUT_SAFE_CREATE; - if ((normalized->checkout_strategy & GIT_CHECKOUT_SAFE_CREATE) != 0) - normalized->checkout_strategy |= GIT_CHECKOUT_SAFE; - - /* opts->disable_filters is false by default */ - - if (!normalized->dir_mode) - normalized->dir_mode = GIT_DIR_MODE; - - if (!normalized->file_open_flags) - normalized->file_open_flags = O_CREAT | O_TRUNC | O_WRONLY; - - if (pfx) - *pfx = git_pathspec_prefix(&normalized->paths); - - if (!normalized->baseline) { - normalized->checkout_strategy |= GIT_CHECKOUT__FREE_BASELINE; - - return checkout_lookup_head_tree(&normalized->baseline, repo); - } - - return 0; -} - -static void checkout_cleanup_opts(git_checkout_opts *opts) -{ - if ((opts->checkout_strategy & GIT_CHECKOUT__FREE_BASELINE) != 0) - git_tree_free(opts->baseline); -} - int git_checkout_index( git_repository *repo, git_index *index, git_checkout_opts *opts) { int error; - git_checkout_opts co_opts; - git_iterator *base_i, *index_i; - char *pfx; - - assert(repo); - - GITERR_CHECK_VERSION(opts, GIT_CHECKOUT_OPTS_VERSION, "git_checkout_opts"); + git_iterator *index_i; if ((error = git_repository__ensure_not_bare(repo, "checkout index")) < 0) return error; if (!index && (error = git_repository_index__weakptr(&index, repo)) < 0) return error; + GIT_REFCOUNT_INC(index); - if (!(error = checkout_normalize_opts(&co_opts, &pfx, repo, opts)) && - !(error = git_iterator_for_tree_range( - &base_i, co_opts.baseline, pfx, pfx)) && - !(error = git_iterator_for_index_range(&index_i, index, pfx, pfx))) - error = git_checkout__from_iterators(index_i, base_i, &co_opts, pfx); + if (!(error = git_iterator_for_index(&index_i, index))) + error = git_checkout_iterator(index_i, opts); - git__free(pfx); git_iterator_free(index_i); - git_iterator_free(base_i); - checkout_cleanup_opts(&co_opts); + git_index_free(index); return error; } @@ -981,12 +1244,8 @@ int git_checkout_tree( git_checkout_opts *opts) { int error; - git_checkout_opts co_opts; - git_tree *tree; - git_iterator *tree_i, *base_i; - char *pfx; - - assert(repo); + git_tree *tree = NULL; + git_iterator *tree_i = NULL; if ((error = git_repository__ensure_not_bare(repo, "checkout tree")) < 0) return error; @@ -997,17 +1256,11 @@ int git_checkout_tree( return -1; } - if (!(error = checkout_normalize_opts(&co_opts, &pfx, repo, opts)) && - !(error = git_iterator_for_tree_range( - &base_i, co_opts.baseline, pfx, pfx)) && - !(error = git_iterator_for_tree_range(&tree_i, tree, pfx, pfx))) - error = git_checkout__from_iterators(tree_i, base_i, &co_opts, pfx); + if (!(error = git_iterator_for_tree(&tree_i, tree))) + error = git_checkout_iterator(tree_i, opts); - git__free(pfx); git_iterator_free(tree_i); - git_iterator_free(base_i); git_tree_free(tree); - checkout_cleanup_opts(&co_opts); return error; } @@ -1017,30 +1270,18 @@ int git_checkout_head( git_checkout_opts *opts) { int error; - git_checkout_opts co_opts; - git_tree *head; - git_iterator *i1, *i2; - char *pfx; - - assert(repo); + git_tree *head = NULL; + git_iterator *head_i = NULL; if ((error = git_repository__ensure_not_bare(repo, "checkout head")) < 0) return error; - if ((error = checkout_lookup_head_tree(&head, repo)) < 0) - return error; + if (!(error = checkout_lookup_head_tree(&head, repo)) && + !(error = git_iterator_for_tree(&head_i, head))) + error = git_checkout_iterator(head_i, opts); - if (!(error = checkout_normalize_opts(&co_opts, &pfx, repo, opts)) && - !(error = git_iterator_for_tree_range( - &i1, co_opts.baseline, pfx, pfx)) && - !(error = git_iterator_for_tree_range(&i2, head, pfx, pfx))) - error = git_checkout__from_iterators(i1, i2, &co_opts, pfx); - - git__free(pfx); - git_iterator_free(i1); - git_iterator_free(i2); + git_iterator_free(head_i); git_tree_free(head); - checkout_cleanup_opts(&co_opts); return error; } diff --git a/src/checkout.h b/src/checkout.h index 651b0033f..815abdfed 100644 --- a/src/checkout.h +++ b/src/checkout.h @@ -10,22 +10,15 @@ #include "git2/checkout.h" #include "iterator.h" -#define GIT_CHECKOUT__FREE_BASELINE (1u << 24) +#define GIT_CHECKOUT__NOTIFY_CONFLICT_TREE (1u << 12) /** - * Given a working directory which is expected to match the contents - * of iterator "expected", this will make the directory match the - * contents of "desired" according to the rules in the checkout "opts". - * - * Because the iterators for the desired and expected values were already - * created when this is invoked, if the checkout opts `paths` is in play, - * then presumably the pathspec_pfx was already computed, so it should be - * passed in to prevent reallocation. + * Update the working directory to match the target iterator. The + * expected baseline value can be passed in via the checkout options + * or else will default to the HEAD commit. */ -extern int git_checkout__from_iterators( - git_iterator *desired, - git_iterator *expected, - git_checkout_opts *opts, - const char *pathspec_pfx); +extern int git_checkout_iterator( + git_iterator *target, + git_checkout_opts *opts); #endif diff --git a/src/diff.c b/src/diff.c index 83e73cd03..042cdf451 100644 --- a/src/diff.c +++ b/src/diff.c @@ -164,6 +164,11 @@ static git_diff_delta *diff_delta__last_for_item( if (git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) return delta; break; + case GIT_DELTA_UNTRACKED: + if (diff->strcomp(delta->new_file.path, item->path) == 0 && + git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) + return delta; + break; case GIT_DELTA_MODIFIED: if (git_oid_cmp(&delta->old_file.oid, &item->oid) == 0 || git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) @@ -531,14 +536,14 @@ static bool entry_is_prefixed( { size_t pathlen; - if (!prefix_item || diff->pfxcomp(prefix_item->path, item->path)) + if (!item || diff->pfxcomp(item->path, prefix_item->path) != 0) return false; - pathlen = strlen(item->path); + pathlen = strlen(prefix_item->path); - return (item->path[pathlen - 1] == '/' || - prefix_item->path[pathlen] == '\0' || - prefix_item->path[pathlen] == '/'); + return (prefix_item->path[pathlen - 1] == '/' || + item->path[pathlen] == '\0' || + item->path[pathlen] == '/'); } static int diff_list_init_from_iterators( @@ -616,7 +621,7 @@ int git_diff__from_iterators( * instead of just generating a DELETE record */ if ((diff->opts.flags & GIT_DIFF_INCLUDE_TYPECHANGE_TREES) != 0 && - entry_is_prefixed(diff, oitem, nitem)) + entry_is_prefixed(diff, nitem, oitem)) { /* this entry has become a tree! convert to TYPECHANGE */ git_diff_delta *last = diff_delta__last_for_item(diff, oitem); @@ -624,6 +629,17 @@ int git_diff__from_iterators( last->status = GIT_DELTA_TYPECHANGE; last->new_file.mode = GIT_FILEMODE_TREE; } + + /* If new_iter is a workdir iterator, then this situation + * will certainly be followed by a series of untracked items. + * Unless RECURSE_UNTRACKED_DIRS is set, skip over them... + */ + if (S_ISDIR(nitem->mode) && + !(diff->opts.flags & GIT_DIFF_RECURSE_UNTRACKED_DIRS)) + { + if (git_iterator_advance(new_iter, &nitem) < 0) + goto fail; + } } if (git_iterator_advance(old_iter, &oitem) < 0) @@ -635,6 +651,7 @@ int git_diff__from_iterators( */ else if (cmp > 0) { git_delta_t delta_type = GIT_DELTA_UNTRACKED; + bool contains_oitem = entry_is_prefixed(diff, oitem, nitem); /* check if contained in ignored parent directory */ if (git_buf_len(&ignore_prefix) && @@ -646,14 +663,12 @@ int git_diff__from_iterators( * it or if the user requested the contents of untracked * directories and it is not under an ignored directory. */ - bool contains_tracked = - entry_is_prefixed(diff, nitem, oitem); bool recurse_untracked = (delta_type == GIT_DELTA_UNTRACKED && (diff->opts.flags & GIT_DIFF_RECURSE_UNTRACKED_DIRS) != 0); /* do not advance into directories that contain a .git file */ - if (!contains_tracked && recurse_untracked) { + if (!contains_oitem && recurse_untracked) { git_buf *full = NULL; if (git_iterator_current_workdir_path(new_iter, &full) < 0) goto fail; @@ -661,7 +676,7 @@ int git_diff__from_iterators( recurse_untracked = false; } - if (contains_tracked || recurse_untracked) { + if (contains_oitem || recurse_untracked) { /* if this directory is ignored, remember it as the * "ignore_prefix" for processing contained items */ @@ -707,14 +722,14 @@ int git_diff__from_iterators( goto fail; /* if we are generating TYPECHANGE records then check for that - * instead of just generating an ADD/UNTRACKED record + * instead of just generating an ADDED/UNTRACKED record */ if (delta_type != GIT_DELTA_IGNORED && (diff->opts.flags & GIT_DIFF_INCLUDE_TYPECHANGE_TREES) != 0 && - entry_is_prefixed(diff, nitem, oitem)) + contains_oitem) { - /* this entry was a tree! convert to TYPECHANGE */ - git_diff_delta *last = diff_delta__last_for_item(diff, oitem); + /* this entry was prefixed with a tree - make TYPECHANGE */ + git_diff_delta *last = diff_delta__last_for_item(diff, nitem); if (last) { last->status = GIT_DELTA_TYPECHANGE; last->old_file.mode = GIT_FILEMODE_TREE; diff --git a/src/fileops.c b/src/fileops.c index 7f023bf69..47b47d6c8 100644 --- a/src/fileops.c +++ b/src/fileops.c @@ -352,6 +352,7 @@ int git_futils_mkdir_r(const char *path, const char *base, const mode_t mode) typedef struct { const char *base; + size_t baselen; uint32_t flags; int error; } futils__rmdir_data; @@ -443,9 +444,13 @@ static int futils__rmdir_recurs_foreach(void *opaque, git_buf *path) static int futils__rmdir_empty_parent(void *opaque, git_buf *path) { - int error = p_rmdir(path->ptr); + futils__rmdir_data *data = opaque; + int error; - GIT_UNUSED(opaque); + if (git_buf_len(path) <= data->baselen) + return GIT_ITEROVER; + + error = p_rmdir(git_buf_cstr(path)); if (error) { int en = errno; @@ -457,7 +462,7 @@ static int futils__rmdir_empty_parent(void *opaque, git_buf *path) giterr_clear(); error = GIT_ITEROVER; } else { - futils__error_cannot_rmdir(path->ptr, NULL); + futils__error_cannot_rmdir(git_buf_cstr(path), NULL); } } @@ -475,9 +480,10 @@ int git_futils_rmdir_r( if (git_path_join_unrooted(&fullpath, path, base, NULL) < 0) return -1; - data.base = base ? base : ""; - data.flags = flags; - data.error = 0; + data.base = base ? base : ""; + data.baselen = base ? strlen(base) : 0; + data.flags = flags; + data.error = 0; error = futils__rmdir_recurs_foreach(&data, &fullpath); diff --git a/tests-clar/checkout/index.c b/tests-clar/checkout/index.c index d42b69e23..b1778a422 100644 --- a/tests-clar/checkout/index.c +++ b/tests-clar/checkout/index.c @@ -4,7 +4,6 @@ #include "repository.h" static git_repository *g_repo; -static git_checkout_opts g_opts; static void reset_index_to_treeish(git_object *treeish) { @@ -25,8 +24,6 @@ void test_checkout_index__initialize(void) { git_tree *tree; - GIT_INIT_STRUCTURE(&g_opts, GIT_CHECKOUT_OPTS_VERSION); - g_repo = cl_git_sandbox_init("testrepo"); cl_git_pass(git_repository_head_tree(&tree, g_repo)); @@ -65,7 +62,6 @@ void test_checkout_index__cannot_checkout_a_bare_repository(void) { test_checkout_index__cleanup(); - GIT_INIT_STRUCTURE(&g_opts, GIT_CHECKOUT_OPTS_VERSION); g_repo = cl_git_sandbox_init("testrepo.git"); cl_git_fail(git_checkout_index(g_repo, NULL, NULL)); @@ -73,13 +69,15 @@ void test_checkout_index__cannot_checkout_a_bare_repository(void) void test_checkout_index__can_create_missing_files(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + cl_assert_equal_i(false, git_path_isfile("./testrepo/README")); cl_assert_equal_i(false, git_path_isfile("./testrepo/branch_file.txt")); cl_assert_equal_i(false, git_path_isfile("./testrepo/new.txt")); - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); test_file_contents("./testrepo/README", "hey there\n"); test_file_contents("./testrepo/branch_file.txt", "hi\nbye!\n"); @@ -88,34 +86,37 @@ void test_checkout_index__can_create_missing_files(void) void test_checkout_index__can_remove_untracked_files(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + git_futils_mkdir("./testrepo/dir/subdir/subsubdir", NULL, 0755, GIT_MKDIR_PATH); cl_git_mkfile("./testrepo/dir/one", "one\n"); cl_git_mkfile("./testrepo/dir/subdir/two", "two\n"); cl_assert_equal_i(true, git_path_isdir("./testrepo/dir/subdir/subsubdir")); - g_opts.checkout_strategy = + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE | GIT_CHECKOUT_REMOVE_UNTRACKED; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); cl_assert_equal_i(false, git_path_isdir("./testrepo/dir")); } void test_checkout_index__honor_the_specified_pathspecs(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; char *entries[] = { "*.txt" }; - g_opts.paths.strings = entries; - g_opts.paths.count = 1; + opts.paths.strings = entries; + opts.paths.count = 1; cl_assert_equal_i(false, git_path_isfile("./testrepo/README")); cl_assert_equal_i(false, git_path_isfile("./testrepo/branch_file.txt")); cl_assert_equal_i(false, git_path_isfile("./testrepo/new.txt")); - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); cl_assert_equal_i(false, git_path_isfile("./testrepo/README")); test_file_contents("./testrepo/branch_file.txt", "hi\nbye!\n"); @@ -139,6 +140,7 @@ static void set_core_autocrlf_to(bool value) void test_checkout_index__honor_the_gitattributes_directives(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; const char *attributes = "branch_file.txt text eol=crlf\n" "new.txt text eol=lf\n"; @@ -146,9 +148,9 @@ void test_checkout_index__honor_the_gitattributes_directives(void) cl_git_mkfile("./testrepo/.gitattributes", attributes); set_core_autocrlf_to(false); - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); test_file_contents("./testrepo/README", "hey there\n"); test_file_contents("./testrepo/new.txt", "my new file\n"); @@ -158,14 +160,15 @@ void test_checkout_index__honor_the_gitattributes_directives(void) void test_checkout_index__honor_coreautocrlf_setting_set_to_true(void) { #ifdef GIT_WIN32 + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; const char *expected_readme_text = "hey there\r\n"; cl_git_pass(p_unlink("./testrepo/.gitattributes")); set_core_autocrlf_to(true); - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); test_file_contents("./testrepo/README", expected_readme_text); #endif @@ -178,11 +181,13 @@ static void set_repo_symlink_handling_cap_to(bool value) void test_checkout_index__honor_coresymlinks_setting_set_to_true(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + set_repo_symlink_handling_cap_to(true); - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); #ifdef GIT_WIN32 test_file_contents("./testrepo/link_to_new.txt", "new.txt"); @@ -202,55 +207,63 @@ void test_checkout_index__honor_coresymlinks_setting_set_to_true(void) void test_checkout_index__honor_coresymlinks_setting_set_to_false(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + set_repo_symlink_handling_cap_to(false); - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); test_file_contents("./testrepo/link_to_new.txt", "new.txt"); } void test_checkout_index__donot_overwrite_modified_file_by_default(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!"); /* set this up to not return an error code on conflicts, but it * still will not have permission to overwrite anything... */ - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS; + opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); test_file_contents("./testrepo/new.txt", "This isn't what's stored!"); } void test_checkout_index__can_overwrite_modified_file(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!"); - g_opts.checkout_strategy = GIT_CHECKOUT_FORCE; + opts.checkout_strategy = GIT_CHECKOUT_FORCE; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); test_file_contents("./testrepo/new.txt", "my new file\n"); } void test_checkout_index__options_disable_filters(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + cl_git_mkfile("./testrepo/.gitattributes", "*.txt text eol=crlf\n"); - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; - g_opts.disable_filters = false; + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + opts.disable_filters = false; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); test_file_contents("./testrepo/new.txt", "my new file\r\n"); p_unlink("./testrepo/new.txt"); - g_opts.disable_filters = true; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + opts.disable_filters = true; + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); test_file_contents("./testrepo/new.txt", "my new file\n"); } @@ -258,6 +271,7 @@ void test_checkout_index__options_disable_filters(void) void test_checkout_index__options_dir_modes(void) { #ifndef GIT_WIN32 + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; struct stat st; git_oid oid; git_commit *commit; @@ -267,10 +281,10 @@ void test_checkout_index__options_dir_modes(void) reset_index_to_treeish((git_object *)commit); - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; - g_opts.dir_mode = 0701; + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + opts.dir_mode = 0701; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); cl_git_pass(p_stat("./testrepo/a", &st)); cl_assert_equal_i(st.st_mode & 0777, 0701); @@ -286,12 +300,13 @@ void test_checkout_index__options_dir_modes(void) void test_checkout_index__options_override_file_modes(void) { #ifndef GIT_WIN32 + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; struct stat st; - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; - g_opts.file_mode = 0700; + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + opts.file_mode = 0700; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); cl_git_pass(p_stat("./testrepo/new.txt", &st)); cl_assert_equal_i(st.st_mode & 0777, 0700); @@ -300,13 +315,15 @@ void test_checkout_index__options_override_file_modes(void) void test_checkout_index__options_open_flags(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + cl_git_mkfile("./testrepo/new.txt", "hi\n"); - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; - g_opts.file_open_flags = O_CREAT | O_RDWR | O_APPEND; + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + opts.file_open_flags = O_CREAT | O_RDWR | O_APPEND; - g_opts.checkout_strategy = GIT_CHECKOUT_FORCE; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); test_file_contents("./testrepo/new.txt", "hi\nmy new file\n"); } @@ -316,28 +333,29 @@ struct notify_data { const char *sha; }; -static int notify_cb( - const char *file, - unsigned int status, - const git_oid *blob_oid, - unsigned int checkout_mode, - unsigned int workdir_mode, +static int test_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) { struct notify_data *expectations = (struct notify_data *)payload; - GIT_UNUSED(checkout_mode); - GIT_UNUSED(workdir_mode); - GIT_UNUSED(status); + GIT_UNUSED(workdir); - cl_assert_equal_s(expectations->file, file); - cl_assert_equal_i(0, git_oid_streq(blob_oid, expectations->sha)); + cl_assert_equal_i(GIT_CHECKOUT_NOTIFY_CONFLICT, why); + cl_assert_equal_s(expectations->file, path); + cl_assert_equal_i(0, git_oid_streq(&baseline->oid, expectations->sha)); + cl_assert_equal_i(0, git_oid_streq(&target->oid, expectations->sha)); return 0; } void test_checkout_index__can_notify_of_skipped_files(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; struct notify_data data; cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!"); @@ -351,28 +369,28 @@ void test_checkout_index__can_notify_of_skipped_files(void) data.file = "new.txt"; data.sha = "a71586c1dfe8a71c6cbf6c129f404c5642ff31bd"; - g_opts.checkout_strategy = + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE | GIT_CHECKOUT_ALLOW_CONFLICTS; - g_opts.notify_flags = GIT_CHECKOUT_NOTIFY_CONFLICTS; - g_opts.notify_cb = notify_cb; - g_opts.notify_payload = &data; + opts.notify_flags = GIT_CHECKOUT_NOTIFY_CONFLICT; + opts.notify_cb = test_checkout_notify_cb; + opts.notify_payload = &data; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); } static int dont_notify_cb( - const char *file, - unsigned int status, - const git_oid *blob_oid, - unsigned int checkout_mode, - unsigned int workdir_mode, + 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_UNUSED(file); - GIT_UNUSED(status); - GIT_UNUSED(blob_oid); - GIT_UNUSED(checkout_mode); - GIT_UNUSED(workdir_mode); + GIT_UNUSED(why); + GIT_UNUSED(path); + GIT_UNUSED(baseline); + GIT_UNUSED(target); + GIT_UNUSED(workdir); GIT_UNUSED(payload); cl_assert(false); @@ -382,18 +400,20 @@ static int dont_notify_cb( void test_checkout_index__wont_notify_of_expected_line_ending_changes(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + cl_git_pass(p_unlink("./testrepo/.gitattributes")); set_core_autocrlf_to(true); cl_git_mkfile("./testrepo/new.txt", "my new file\r\n"); - g_opts.checkout_strategy = + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE | GIT_CHECKOUT_ALLOW_CONFLICTS; - g_opts.notify_flags = GIT_CHECKOUT_NOTIFY_CONFLICTS; - g_opts.notify_cb = dont_notify_cb; - g_opts.notify_payload = NULL; + opts.notify_flags = GIT_CHECKOUT_NOTIFY_CONFLICT; + opts.notify_cb = dont_notify_cb; + opts.notify_payload = NULL; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); } static void checkout_progress_counter( @@ -405,18 +425,20 @@ static void checkout_progress_counter( void test_checkout_index__calls_progress_callback(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; int calls = 0; - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; - g_opts.progress_cb = checkout_progress_counter; - g_opts.progress_payload = &calls; + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + opts.progress_cb = checkout_progress_counter; + opts.progress_payload = &calls; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, NULL, &opts)); cl_assert(calls > 0); } void test_checkout_index__can_overcome_name_clashes(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; git_index *index; cl_git_pass(git_repository_index(&index, g_repo)); @@ -440,15 +462,15 @@ void test_checkout_index__can_overcome_name_clashes(void) cl_assert(git_path_isfile("./testrepo/path1")); cl_assert(git_path_isfile("./testrepo/path0/file0")); - g_opts.checkout_strategy = + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE | GIT_CHECKOUT_ALLOW_CONFLICTS; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_pass(git_checkout_index(g_repo, index, &opts)); cl_assert(git_path_isfile("./testrepo/path1")); cl_assert(git_path_isfile("./testrepo/path0/file0")); - g_opts.checkout_strategy = GIT_CHECKOUT_FORCE; - cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + cl_git_pass(git_checkout_index(g_repo, index, &opts)); cl_assert(git_path_isfile("./testrepo/path0")); cl_assert(git_path_isfile("./testrepo/path1/file1")); @@ -458,17 +480,18 @@ void test_checkout_index__can_overcome_name_clashes(void) void test_checkout_index__validates_struct_version(void) { + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; const git_error *err; - g_opts.version = 1024; - cl_git_fail(git_checkout_index(g_repo, NULL, &g_opts)); + opts.version = 1024; + cl_git_fail(git_checkout_index(g_repo, NULL, &opts)); err = giterr_last(); cl_assert_equal_i(err->klass, GITERR_INVALID); - g_opts.version = 0; + opts.version = 0; giterr_clear(); - cl_git_fail(git_checkout_index(g_repo, NULL, &g_opts)); + cl_git_fail(git_checkout_index(g_repo, NULL, &opts)); err = giterr_last(); cl_assert_equal_i(err->klass, GITERR_INVALID); diff --git a/tests-clar/checkout/tree.c b/tests-clar/checkout/tree.c index ed46748ae..ab502dd30 100644 --- a/tests-clar/checkout/tree.c +++ b/tests-clar/checkout/tree.c @@ -126,9 +126,10 @@ void test_checkout_tree__doesnt_write_unrequested_files_to_worktree(void) cl_git_pass(git_commit_lookup(&p_master_commit, g_repo, &master_oid)); cl_git_pass(git_commit_lookup(&p_chomped_commit, g_repo, &chomped_oid)); - /* A GIT_CHECKOUT_DEFAULT checkout is not allowed to add any file to the - * working tree from the index as it is supposed to be a dry run. */ - opts.checkout_strategy = GIT_CHECKOUT_DEFAULT; + /* GIT_CHECKOUT_NONE should not add any file to the working tree from the + * index as it is supposed to be a dry run. + */ + opts.checkout_strategy = GIT_CHECKOUT_NONE; git_checkout_tree(g_repo, (git_object*)p_chomped_commit, &opts); cl_assert_equal_i(false, git_path_isfile("testrepo/readme.txt")); } diff --git a/tests-clar/checkout/typechange.c b/tests-clar/checkout/typechange.c index 85da11570..bc7039caa 100644 --- a/tests-clar/checkout/typechange.c +++ b/tests-clar/checkout/typechange.c @@ -44,7 +44,8 @@ void test_checkout_typechange__checkout_typechanges(void) for (i = 0; g_typechange_oids[i] != NULL; ++i) { cl_git_pass(git_revparse_single(&obj, g_repo, g_typechange_oids[i])); - /* fprintf(stderr, "checking out '%s'\n", g_typechange_oids[i]); */ + + /* fprintf(stderr, "---- checking out '%s' ----\n", g_typechange_oids[i]); */ cl_git_pass(git_checkout_tree(g_repo, obj, &opts));