diff --git a/include/git2/checkout.h b/include/git2/checkout.h index 390d2f215..a9314c2cb 100644 --- a/include/git2/checkout.h +++ b/include/git2/checkout.h @@ -25,20 +25,117 @@ GIT_BEGIN_DECL * Checkout behavior flags * * These flags control what checkout does with files. Pass in a - * combination of these values OR'ed together. + * combination of these values OR'ed together. If you just pass zero + * (i.e. no flags), then you are effectively doing a "dry run" where no + * files will be modified. + * + * Checkout groups the working directory content into 3 classes of files: + * (1) files that don't need a change, and files that do need a change + * that either (2) we are allowed to modifed or (3) we are not. The flags + * you pass in will decide which files we are allowed to modify. + * + * By default, checkout is not allowed to modify any files. Anything + * needing a change would be considered a conflict. + * + * GIT_CHECKOUT_UPDATE_UNMODIFIED means that checkout is allowed to update + * any file where the working directory content matches the HEAD + * (e.g. either the files match or the file is absent in both places). + * + * GIT_CHECKOUT_UPDATE_MISSING means checkout can create a missing file + * that exists in the index and does not exist in the working directory. + * This is usually desirable for initial checkout, etc. Technically, the + * missing file differs from the HEAD, which is why this is separate. + * + * GIT_CHECKOUT_UPDATE_MODIFIED means checkout is allowed to update files + * where the working directory does not match the HEAD so long as the file + * actually exists in the HEAD. This option implies UPDATE_UNMODIFIED. + * + * GIT_CHECKOUT_UPDATE_UNTRACKED means checkout is allowed to update files + * even if there is a working directory version that does not exist in the + * HEAD (i.e. the file was independently created in the workdir). This + * implies UPDATE_UNMODIFIED | UPDATE_MISSING (but *not* UPDATE_MODIFIED). + * + * + * On top of these three basic strategies, there are some modifiers + * options that can be applied: + * + * If any files need update but are disallowed by the strategy, normally + * checkout calls the conflict callback (if given) and then aborts. + * GIT_CHECKOUT_ALLOW_CONFLICTS means it is okay to update the files that + * are allowed by the strategy even if there are conflicts. The conflict + * callbacks are still made, but non-conflicting files will be updated. + * + * Any unmerged entries in the index are automatically considered conflicts. + * If you want to proceed anyhow and just skip unmerged entries, you can use + * GIT_CHECKOUT_SKIP_UNMERGED which is less dangerous than just allowing all + * conflicts. Alternatively, use GIT_CHECKOUT_USE_OURS to proceed and + * checkout the stage 2 ("ours") version. GIT_CHECKOUT_USE_THEIRS means to + * proceed and use the stage 3 ("theirs") version. + * + * GIT_CHECKOUT_UPDATE_ONLY means that update is not allowed to create new + * files or delete old ones, only update existing content. With this + * flag, files that needs to be created or deleted are not conflicts - + * they are just skipped. This also skips typechanges to existing files + * (because the old would have to be removed). + * + * GIT_CHECKOUT_REMOVE_UNTRACKED means that files in the working directory + * that are untracked (and not ignored) will be removed altogether. These + * untracked files (that do not shadow index entries) are not considered + * conflicts and would normally be ignored. + * + * + * 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 conflict 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. */ typedef enum { - /** Checkout does not update any files in the working directory. */ - GIT_CHECKOUT_DEFAULT = (1 << 0), + GIT_CHECKOUT_DEFAULT = 0, /** default is a dry run, no actual updates */ - /** When a file exists and is modified, replace it with new version. */ - GIT_CHECKOUT_OVERWRITE_MODIFIED = (1 << 1), + /** Allow update of entries where working dir matches HEAD. */ + GIT_CHECKOUT_UPDATE_UNMODIFIED = (1u << 0), - /** When a file does not exist in the working directory, create it. */ - GIT_CHECKOUT_CREATE_MISSING = (1 << 2), + /** Allow update of entries where working dir does not have file. */ + GIT_CHECKOUT_UPDATE_MISSING = (1u << 1), + + /** Allow safe updates that cannot overwrite uncommited data */ + GIT_CHECKOUT_SAFE = + (GIT_CHECKOUT_UPDATE_UNMODIFIED | GIT_CHECKOUT_UPDATE_MISSING), + + /** Allow update of entries in working dir that are modified from HEAD. */ + GIT_CHECKOUT_UPDATE_MODIFIED = (1u << 2), + + /** Update existing untracked files that are now present in the index. */ + GIT_CHECKOUT_UPDATE_UNTRACKED = (1u << 3), + + /** Allow all updates to force working directory to look like index */ + GIT_CHECKOUT_FORCE = + (GIT_CHECKOUT_SAFE | GIT_CHECKOUT_UPDATE_MODIFIED | GIT_CHECKOUT_UPDATE_UNTRACKED), + + /** Allow checkout to make updates even if conflicts are found */ + GIT_CHECKOUT_ALLOW_CONFLICTS = (1u << 4), + + /** Remove untracked files not in index (that are not ignored) */ + GIT_CHECKOUT_REMOVE_UNTRACKED = (1u << 5), + + /** Only update existing files, don't create new ones */ + GIT_CHECKOUT_UPDATE_ONLY = (1u << 6), + + /** Allow checkout to skip unmerged files */ + GIT_CHECKOUT_SKIP_UNMERGED = (1u << 10), + /** For unmerged files, checkout stage 2 from index */ + GIT_CHECKOUT_USE_OURS = (1u << 11), + /** For unmerged files, checkout stage 3 from index */ + GIT_CHECKOUT_USE_THEIRS = (1u << 12), + + /** Recursively checkout submodule with same options */ + GIT_CHECKOUT_UPDATE_SUBMODULES = (1u << 16), + /** Recursively checkout submodules only if HEAD moved in super repo */ + GIT_CHECKOUT_UPDATE_SUBMODULES_IF_CHANGED = (1u << 17), - /** If an untracked file in found in the working dir, delete it. */ - GIT_CHECKOUT_REMOVE_UNTRACKED = (1 << 3), } git_checkout_strategy_t; /** @@ -47,38 +144,38 @@ typedef enum { * Use zeros to indicate default settings. */ typedef struct git_checkout_opts { - unsigned int checkout_strategy; /** default: GIT_CHECKOUT_DEFAULT */ + unsigned int checkout_strategy; /** default will be a dry run */ + int disable_filters; /** don't apply filters like CRLF conversion */ int dir_mode; /** default is 0755 */ int file_mode; /** default is 0644 or 0755 as dictated by blob */ int file_open_flags; /** default is O_CREAT | O_TRUNC | O_WRONLY */ - /** Optional callback to notify the consumer of files that - * haven't be checked out because a modified version of them - * exist in the working directory. - * - * When provided, this callback will be invoked when the flag - * GIT_CHECKOUT_OVERWRITE_MODIFIED isn't part of the checkout strategy. + /** Optional callback made on files where the index differs from the + * working directory but the rules do not allow update. Return a + * non-zero value to abort the checkout. All such callbacks will be + * made before any changes are made to the working directory. */ - int (* skipped_notify_cb)( - const char *skipped_file, - const git_oid *blob_oid, - int file_mode, + int (*conflict_cb)( + const char *conflicting_path, + const git_oid *index_oid, + unsigned int index_mode, + unsigned int wd_mode, void *payload); - void *notify_payload; + void *conflict_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); + void (*progress_cb)( + const char *path, + size_t completed_steps, + size_t total_steps, + void *payload); void *progress_payload; - /** When not NULL, array of fnmatch patterns specifying - * which paths should be taken into account + /** When not zeroed out, array of fnmatch patterns specifying which + * paths should be taken into account, otherwise all files. */ - git_strarray paths; + git_strarray paths; } git_checkout_opts; /** diff --git a/include/git2/errors.h b/include/git2/errors.h index 8bb47f354..45e04578d 100644 --- a/include/git2/errors.h +++ b/include/git2/errors.h @@ -60,6 +60,7 @@ typedef enum { GITERR_SUBMODULE, GITERR_THREAD, GITERR_STASH, + GITERR_CHECKOUT, } git_error_t; /** diff --git a/src/checkout.c b/src/checkout.c index e4a397cd5..2bad06501 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -22,20 +22,19 @@ #include "filter.h" #include "blob.h" #include "diff.h" +#include "pathspec.h" -struct checkout_diff_data -{ +typedef struct { + git_repository *repo; + git_diff_list *diff; + git_checkout_opts *opts; git_buf *path; size_t workdir_len; - git_checkout_opts *checkout_opts; - git_repository *owner; bool can_symlink; - bool found_submodules; - bool create_submodules; int error; size_t total_steps; size_t completed_steps; -}; +} checkout_diff_data; static int buffer_to_file( git_buf *buffer, @@ -112,7 +111,8 @@ static int blob_content_to_file( if (!file_mode) file_mode = entry_filemode; - error = buffer_to_file(&filtered, path, opts->dir_mode, opts->file_open_flags, file_mode); + error = buffer_to_file( + &filtered, path, opts->dir_mode, opts->file_open_flags, file_mode); cleanup: git_filters_free(&filters); @@ -123,7 +123,8 @@ cleanup: return error; } -static int blob_content_to_link(git_blob *blob, const char *path, bool can_symlink) +static int blob_content_to_link( + git_blob *blob, const char *path, bool can_symlink) { git_buf linktarget = GIT_BUF_INIT; int error; @@ -142,58 +143,52 @@ static int blob_content_to_link(git_blob *blob, const char *path, bool can_symli } static int checkout_submodule( - struct checkout_diff_data *data, + checkout_diff_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) + return 0; + if (git_futils_mkdir( - file->path, git_repository_workdir(data->owner), - data->checkout_opts->dir_mode, GIT_MKDIR_PATH) < 0) + file->path, git_repository_workdir(data->repo), + data->opts->dir_mode, GIT_MKDIR_PATH) < 0) return -1; - /* TODO: two cases: + /* TODO: Support checkout_strategy options. Two circumstances: * 1 - submodule already checked out, but we need to move the HEAD * to the new OID, or * 2 - submodule not checked out and we should recursively check it out * - * Checkout will not execute a pull request on the submodule, but a - * clone command should probably be able to. Do we need a submodule - * callback option? + * Checkout will not execute a pull on the submodule, but a clone + * command should probably be able to. Do we need a submodule callback? */ return 0; } static void report_progress( - struct checkout_diff_data *data, + checkout_diff_data *data, const char *path) { - if (data->checkout_opts->progress_cb) - data->checkout_opts->progress_cb( - path, - data->completed_steps, - data->total_steps, - data->checkout_opts->progress_payload); + if (data->opts->progress_cb) + data->opts->progress_cb( + path, data->completed_steps, data->total_steps, + data->opts->progress_payload); } static int checkout_blob( - struct checkout_diff_data *data, + checkout_diff_data *data, const git_diff_file *file) { - git_blob *blob; int error = 0; + git_blob *blob; git_buf_truncate(data->path, data->workdir_len); - if (git_buf_joinpath(data->path, git_buf_cstr(data->path), file->path) < 0) + if (git_buf_puts(data->path, file->path) < 0) return -1; - /* If there is a directory where this blob should go, then there is an - * existing tree that conflicts with this file, but REMOVE_UNTRACKED must - * not have been passed in. Signal to caller with GIT_ENOTFOUND. - */ - if (git_path_isdir(git_buf_cstr(data->path))) - return GIT_ENOTFOUND; - - if ((error = git_blob_lookup(&blob, data->owner, &file->oid)) < 0) + if ((error = git_blob_lookup(&blob, data->repo, &file->oid)) < 0) return error; if (S_ISLNK(file->mode)) @@ -201,14 +196,14 @@ static int checkout_blob( blob, git_buf_cstr(data->path), data->can_symlink); else error = blob_content_to_file( - blob, git_buf_cstr(data->path), file->mode, data->checkout_opts); + blob, git_buf_cstr(data->path), file->mode, data->opts); git_blob_free(blob); return error; } -static int retrieve_symlink_capabilities(git_repository *repo, bool *can_symlink) +static int retrieve_symlink_caps(git_repository *repo, bool *can_symlink) { git_config *cfg; int error; @@ -218,10 +213,7 @@ static int retrieve_symlink_capabilities(git_repository *repo, bool *can_symlink error = git_config_get_bool((int *)can_symlink, cfg, "core.symlinks"); - /* - * When no "core.symlinks" entry is found in any of the configuration - * store (local, global or system), default value is "true". - */ + /* If "core.symlinks" is not found anywhere, default to true. */ if (error == GIT_ENOTFOUND) { *can_symlink = true; error = 0; @@ -230,7 +222,8 @@ static int retrieve_symlink_capabilities(git_repository *repo, bool *can_symlink return error; } -static void normalize_options(git_checkout_opts *normalized, git_checkout_opts *proposed) +static void normalize_options( + git_checkout_opts *normalized, git_checkout_opts *proposed) { assert(normalized); @@ -239,11 +232,16 @@ static void normalize_options(git_checkout_opts *normalized, git_checkout_opts * else memmove(normalized, proposed, sizeof(git_checkout_opts)); - /* Default options */ - if (!normalized->checkout_strategy) - normalized->checkout_strategy = GIT_CHECKOUT_DEFAULT; + /* implied checkout strategies */ + if ((normalized->checkout_strategy & GIT_CHECKOUT_UPDATE_MODIFIED) != 0 || + (normalized->checkout_strategy & GIT_CHECKOUT_UPDATE_UNTRACKED) != 0) + normalized->checkout_strategy |= GIT_CHECKOUT_UPDATE_UNMODIFIED; + + if ((normalized->checkout_strategy & GIT_CHECKOUT_UPDATE_UNTRACKED) != 0) + normalized->checkout_strategy |= GIT_CHECKOUT_UPDATE_MISSING; /* opts->disable_filters is false by default */ + if (!normalized->dir_mode) normalized->dir_mode = GIT_DIR_MODE; @@ -254,50 +252,174 @@ static void normalize_options(git_checkout_opts *normalized, git_checkout_opts * enum { CHECKOUT_ACTION__NONE = 0, CHECKOUT_ACTION__REMOVE = 1, - CHECKOUT_ACTION__CREATE_BLOB = 2, - CHECKOUT_ACTION__CREATE_SUBMODULE = 4, - CHECKOUT_ACTION__NOTIFY = 8 + CHECKOUT_ACTION__UPDATE_BLOB = 2, + CHECKOUT_ACTION__UPDATE_SUBMODULE = 4, + CHECKOUT_ACTION__CONFLICT = 8, + CHECKOUT_ACTION__MAX = 8 }; -static uint32_t checkout_action_for_delta( - git_checkout_opts *opts, - const git_diff_delta *delta) +static int checkout_confirm_update_blob( + checkout_diff_data *data, + const git_diff_delta *delta, + int action) { - uint32_t action = CHECKOUT_ACTION__NONE; + int error; + unsigned int strat = data->opts->checkout_strategy; + struct stat st; + bool update_only = ((strat & GIT_CHECKOUT_UPDATE_ONLY) != 0); + + /* for typechange, remove the old item first */ + if (delta->status == GIT_DELTA_TYPECHANGE) { + if (update_only) + action = CHECKOUT_ACTION__NONE; + else + action |= CHECKOUT_ACTION__REMOVE; + + return action; + } + + git_buf_truncate(data->path, data->workdir_len); + if (git_buf_puts(data->path, delta->new_file.path) < 0) + return -1; + + if ((error = p_stat(git_buf_cstr(data->path), &st)) < 0) { + if (errno == ENOENT) { + if (update_only) + action = CHECKOUT_ACTION__NONE; + } else if (errno == ENOTDIR) { + /* File exists where a parent dir needs to go - i.e. untracked + * typechange. Ignore if UPDATE_ONLY, remove if allowed. + */ + if (update_only) + action = CHECKOUT_ACTION__NONE; + else if ((strat & GIT_CHECKOUT_UPDATE_UNTRACKED) != 0) + action |= CHECKOUT_ACTION__REMOVE; + else + action = CHECKOUT_ACTION__CONFLICT; + } + /* otherwise let error happen when we attempt blob checkout later */ + } + else if (S_ISDIR(st.st_mode)) { + /* Directory exists where a blob needs to go - i.e. untracked + * typechange. Ignore if UPDATE_ONLY, remove if allowed. + */ + if (update_only) + action = CHECKOUT_ACTION__NONE; + else if ((strat & GIT_CHECKOUT_UPDATE_UNTRACKED) != 0) + action |= CHECKOUT_ACTION__REMOVE; + else + action = CHECKOUT_ACTION__CONFLICT; + } + + return action; +} + +static int checkout_action_for_delta( + checkout_diff_data *data, + const git_diff_delta *delta, + const git_index_entry *head_entry) +{ + int action = CHECKOUT_ACTION__NONE; + unsigned int strat = data->opts->checkout_strategy; switch (delta->status) { - case GIT_DELTA_UNTRACKED: - if ((opts->checkout_strategy & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0) - action |= CHECKOUT_ACTION__REMOVE; + case GIT_DELTA_UNMODIFIED: + if (!head_entry) { + /* file independently created in wd, even though not in HEAD */ + if ((strat & GIT_CHECKOUT_UPDATE_MISSING) == 0) + action = CHECKOUT_ACTION__CONFLICT; + } + else if (!git_oid_equal(&head_entry->oid, &delta->old_file.oid)) { + /* working directory was independently updated to match index */ + if ((strat & GIT_CHECKOUT_UPDATE_MODIFIED) == 0) + action = CHECKOUT_ACTION__CONFLICT; + } break; - case GIT_DELTA_TYPECHANGE: - if ((opts->checkout_strategy & GIT_CHECKOUT_OVERWRITE_MODIFIED) != 0) - action |= CHECKOUT_ACTION__REMOVE | CHECKOUT_ACTION__CREATE_BLOB; - else if (opts->skipped_notify_cb != NULL) - action = CHECKOUT_ACTION__NOTIFY; + case GIT_DELTA_ADDED: + /* Impossible. New files should be UNTRACKED or TYPECHANGE */ + action = CHECKOUT_ACTION__CONFLICT; break; case GIT_DELTA_DELETED: - if ((opts->checkout_strategy & GIT_CHECKOUT_CREATE_MISSING) != 0) - action |= CHECKOUT_ACTION__CREATE_BLOB; + if (head_entry && /* working dir missing, but exists in HEAD */ + (strat & GIT_CHECKOUT_UPDATE_MISSING) == 0) + action = CHECKOUT_ACTION__CONFLICT; + else + action = CHECKOUT_ACTION__UPDATE_BLOB; break; case GIT_DELTA_MODIFIED: - if ((opts->checkout_strategy & GIT_CHECKOUT_OVERWRITE_MODIFIED) != 0) - action |= CHECKOUT_ACTION__CREATE_BLOB; - else if (opts->skipped_notify_cb != NULL) - action = CHECKOUT_ACTION__NOTIFY; + case GIT_DELTA_TYPECHANGE: + if (!head_entry) { + /* working dir was independently updated & does not match index */ + if ((strat & GIT_CHECKOUT_UPDATE_UNTRACKED) == 0) + action = CHECKOUT_ACTION__CONFLICT; + else + action = CHECKOUT_ACTION__UPDATE_BLOB; + } + else if (git_oid_equal(&head_entry->oid, &delta->new_file.oid)) + action = CHECKOUT_ACTION__UPDATE_BLOB; + else if ((strat & GIT_CHECKOUT_UPDATE_MODIFIED) == 0) + action = CHECKOUT_ACTION__CONFLICT; + else + action = CHECKOUT_ACTION__UPDATE_BLOB; break; + case GIT_DELTA_UNTRACKED: + if (!head_entry) { + if ((strat & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0) + action = CHECKOUT_ACTION__REMOVE; + } + else if ((strat & GIT_CHECKOUT_UPDATE_MODIFIED) != 0) { + action = CHECKOUT_ACTION__REMOVE; + } else if ((strat & GIT_CHECKOUT_UPDATE_UNMODIFIED) != 0) { + git_oid wd_oid; + + /* if HEAD matches workdir, then remove, else conflict */ + + if (git_oid_iszero(&delta->new_file.oid) && + git_diff__oid_for_file( + data->repo, delta->new_file.path, delta->new_file.mode, + delta->new_file.size, &wd_oid) < 0) + action = -1; + else if (git_oid_equal(&head_entry->oid, &wd_oid)) + action = CHECKOUT_ACTION__REMOVE; + else + action = CHECKOUT_ACTION__CONFLICT; + } else { + /* present in HEAD and workdir, but absent in index */ + action = CHECKOUT_ACTION__CONFLICT; + } + break; + + case GIT_DELTA_IGNORED: default: + /* just skip these files */ break; } - if ((action & CHECKOUT_ACTION__CREATE_BLOB) != 0 && - S_ISGITLINK(delta->old_file.mode)) - action = (action & ~CHECKOUT_ACTION__CREATE_BLOB) | - CHECKOUT_ACTION__CREATE_SUBMODULE; + if (action > 0 && (action & CHECKOUT_ACTION__UPDATE_BLOB) != 0) { + if (S_ISGITLINK(delta->old_file.mode)) + action = (action & ~CHECKOUT_ACTION__UPDATE_BLOB) | + CHECKOUT_ACTION__UPDATE_SUBMODULE; + + action = checkout_confirm_update_blob(data, delta, action); + } + + if (action == CHECKOUT_ACTION__CONFLICT && + data->opts->conflict_cb != NULL && + data->opts->conflict_cb( + delta->old_file.path, &delta->old_file.oid, + delta->old_file.mode, delta->new_file.mode, + data->opts->conflict_payload) != 0) + { + giterr_clear(); + action = GIT_EUSER; + } + + if (action > 0 && (strat & GIT_CHECKOUT_UPDATE_ONLY) != 0) + action = (action & ~CHECKOUT_ACTION__REMOVE); return action; } @@ -305,53 +427,119 @@ static uint32_t checkout_action_for_delta( static int checkout_get_actions( uint32_t **actions_ptr, size_t **counts_ptr, - git_diff_list *diff, - git_checkout_opts *opts) + checkout_diff_data *data) { + int error; + git_diff_list *diff = data->diff; git_diff_delta *delta; size_t i, *counts; uint32_t *actions; + git_tree *head = NULL; + git_iterator *hiter = NULL; + char *pfx = git_pathspec_prefix(&data->opts->paths); + const git_index_entry *he; - *counts_ptr = counts = - git__calloc(CHECKOUT_ACTION__NOTIFY, sizeof(size_t)); - GITERR_CHECK_ALLOC(counts); + /* if there is no HEAD, that's okay - we'll make an empty iterator */ + (void)git_repository_head_tree(&head, data->repo); - *actions_ptr = actions = - git__calloc(diff->deltas.length, sizeof(uint32_t)); - GITERR_CHECK_ALLOC(actions); + if ((error = git_iterator_for_tree_range( + &hiter, data->repo, head, pfx, pfx)) < 0) + goto fail; - git_vector_foreach(&diff->deltas, i, delta) { - actions[i] = checkout_action_for_delta(opts, delta); + if ((diff->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0 && + !hiter->ignore_case && + (error = git_iterator_spoolandsort( + &hiter, hiter, diff->entrycmp, true)) < 0) + goto fail; - if (actions[i] & CHECKOUT_ACTION__REMOVE) - counts[CHECKOUT_ACTION__REMOVE]++; + if ((error = git_iterator_current(hiter, &he)) < 0) + goto fail; - if (actions[i] & CHECKOUT_ACTION__CREATE_BLOB) - counts[CHECKOUT_ACTION__CREATE_BLOB]++; + git__free(pfx); - if (actions[i] & CHECKOUT_ACTION__CREATE_SUBMODULE) - counts[CHECKOUT_ACTION__CREATE_SUBMODULE]++; - - if (actions[i] & CHECKOUT_ACTION__NOTIFY) - counts[CHECKOUT_ACTION__NOTIFY]++; + *counts_ptr = counts = git__calloc(CHECKOUT_ACTION__MAX+1, sizeof(size_t)); + *actions_ptr = actions = git__calloc(diff->deltas.length, sizeof(uint32_t)); + if (!counts || !actions) { + error = -1; + goto fail; } + git_vector_foreach(&diff->deltas, i, delta) { + int cmp = -1, act; + + /* try to track HEAD entries parallel to deltas */ + while (he) { + cmp = S_ISDIR(delta->new_file.mode) ? + diff->prefixcmp(he->path, delta->new_file.path) : + diff->strcmp(he->path, delta->old_file.path); + if (cmp >= 0) + break; + if (git_iterator_advance(hiter, &he) < 0) + he = NULL; + } + + act = checkout_action_for_delta(data, delta, !cmp ? he : NULL); + + if (act < 0) { + error = act; + goto fail; + } + + if (!cmp && git_iterator_advance(hiter, &he) < 0) + he = NULL; + + actions[i] = act; + + if (act & CHECKOUT_ACTION__REMOVE) + counts[CHECKOUT_ACTION__REMOVE]++; + if (act & CHECKOUT_ACTION__UPDATE_BLOB) + counts[CHECKOUT_ACTION__UPDATE_BLOB]++; + if (act & CHECKOUT_ACTION__UPDATE_SUBMODULE) + counts[CHECKOUT_ACTION__UPDATE_SUBMODULE]++; + if (act & CHECKOUT_ACTION__CONFLICT) + counts[CHECKOUT_ACTION__CONFLICT]++; + } + + if (counts[CHECKOUT_ACTION__CONFLICT] > 0 && + (data->opts->checkout_strategy & GIT_CHECKOUT_ALLOW_CONFLICTS) == 0) + { + giterr_set(GITERR_CHECKOUT, "%d conflicts prevent checkout", + (int)counts[CHECKOUT_ACTION__CONFLICT]); + goto fail; + } + + git_iterator_free(hiter); return 0; + +fail: + *counts_ptr = NULL; + git__free(counts); + *actions_ptr = NULL; + git__free(actions); + + git_iterator_free(hiter); + git__free(pfx); + + return -1; } static int checkout_remove_the_old( git_diff_list *diff, unsigned int *actions, - struct checkout_diff_data *data) + checkout_diff_data *data) { git_diff_delta *delta; size_t i; + git_buf_truncate(data->path, data->workdir_len); + git_vector_foreach(&diff->deltas, i, delta) { if (actions[i] & CHECKOUT_ACTION__REMOVE) { int error = git_futils_rmdir_r( - delta->new_file.path, git_buf_cstr(data->path), - GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_EMPTY_PARENTS); + delta->new_file.path, + git_buf_cstr(data->path), /* here set to work dir root */ + GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_EMPTY_PARENTS | + GIT_RMDIR_REMOVE_BLOCKERS); if (error < 0) return error; @@ -366,35 +554,20 @@ static int checkout_remove_the_old( static int checkout_create_the_new( git_diff_list *diff, unsigned int *actions, - struct checkout_diff_data *data) + checkout_diff_data *data) { git_diff_delta *delta; size_t i; git_vector_foreach(&diff->deltas, i, delta) { - if (actions[i] & CHECKOUT_ACTION__CREATE_BLOB) { + if (actions[i] & CHECKOUT_ACTION__UPDATE_BLOB) { int error = checkout_blob(data, &delta->old_file); - - /* ENOTFOUND means unable to create the file because of - * an existing parent dir. Probably flags were given - * asking to create new blobs without allowing removal - * of a conflicting tree (or vice versa). - */ - if (error == GIT_ENOTFOUND) - giterr_clear(); - else if (error < 0) + if (error < 0) return error; data->completed_steps++; report_progress(data, delta->old_file.path); } - - if (actions[i] & CHECKOUT_ACTION__NOTIFY) { - if (data->checkout_opts->skipped_notify_cb( - delta->old_file.path, &delta->old_file.oid, - delta->old_file.mode, data->checkout_opts->notify_payload)) - return GIT_EUSER; - } } return 0; @@ -403,13 +576,13 @@ static int checkout_create_the_new( static int checkout_create_submodules( git_diff_list *diff, unsigned int *actions, - struct checkout_diff_data *data) + checkout_diff_data *data) { git_diff_delta *delta; size_t i; git_vector_foreach(&diff->deltas, i, delta) { - if (actions[i] & CHECKOUT_ACTION__CREATE_SUBMODULE) { + if (actions[i] & CHECKOUT_ACTION__UPDATE_SUBMODULE) { int error = checkout_submodule(data, &delta->old_file); if (error < 0) return error; @@ -427,16 +600,13 @@ int git_checkout_index( git_checkout_opts *opts) { git_diff_list *diff = NULL; - git_diff_options diff_opts = {0}; git_checkout_opts checkout_opts; - struct checkout_diff_data data; + checkout_diff_data data; git_buf workdir = GIT_BUF_INIT; - uint32_t *actions = NULL; size_t *counts = NULL; - int error; assert(repo); @@ -445,9 +615,8 @@ int git_checkout_index( return error; diff_opts.flags = - GIT_DIFF_INCLUDE_UNTRACKED | - GIT_DIFF_INCLUDE_TYPECHANGE | - GIT_DIFF_SKIP_BINARY_CHECK; + GIT_DIFF_INCLUDE_UNMODIFIED | GIT_DIFF_INCLUDE_UNTRACKED | + GIT_DIFF_INCLUDE_TYPECHANGE | GIT_DIFF_SKIP_BINARY_CHECK; if (opts && opts->paths.count > 0) diff_opts.pathspec = opts->paths; @@ -470,33 +639,35 @@ int git_checkout_index( * 3. Then checkout all submodules in case a new .gitmodules blob was * checked out during pass #2. */ - if ((error = checkout_get_actions(&actions, &counts, diff, &checkout_opts)) < 0) - goto cleanup; memset(&data, 0, sizeof(data)); data.path = &workdir; data.workdir_len = git_buf_len(&workdir); - data.checkout_opts = &checkout_opts; - data.owner = repo; - data.total_steps = counts[CHECKOUT_ACTION__REMOVE] + - counts[CHECKOUT_ACTION__CREATE_BLOB] + - counts[CHECKOUT_ACTION__CREATE_SUBMODULE]; + data.repo = repo; + data.diff = diff; + data.opts = &checkout_opts; - if ((error = retrieve_symlink_capabilities(repo, &data.can_symlink)) < 0) + if ((error = checkout_get_actions(&actions, &counts, &data)) < 0) goto cleanup; - report_progress(&data, NULL); + data.total_steps = counts[CHECKOUT_ACTION__REMOVE] + + counts[CHECKOUT_ACTION__UPDATE_BLOB] + + counts[CHECKOUT_ACTION__UPDATE_SUBMODULE]; + + if ((error = retrieve_symlink_caps(repo, &data.can_symlink)) < 0) + goto cleanup; + + report_progress(&data, NULL); /* establish 0 baseline */ if (counts[CHECKOUT_ACTION__REMOVE] > 0 && (error = checkout_remove_the_old(diff, actions, &data)) < 0) goto cleanup; - if ((counts[CHECKOUT_ACTION__CREATE_BLOB] + - counts[CHECKOUT_ACTION__NOTIFY]) > 0 && + if (counts[CHECKOUT_ACTION__UPDATE_BLOB] > 0 && (error = checkout_create_the_new(diff, actions, &data)) < 0) goto cleanup; - if (counts[CHECKOUT_ACTION__CREATE_SUBMODULE] > 0 && + if (counts[CHECKOUT_ACTION__UPDATE_SUBMODULE] > 0 && (error = checkout_create_submodules(diff, actions, &data)) < 0) goto cleanup; @@ -519,32 +690,28 @@ int git_checkout_tree( git_object *treeish, git_checkout_opts *opts) { + int error = 0; git_index *index = NULL; git_tree *tree = NULL; - int error; - assert(repo && treeish); if (git_object_peel((git_object **)&tree, treeish, GIT_OBJ_TREE) < 0) { - giterr_set(GITERR_INVALID, "Provided treeish cannot be peeled into a tree."); - return GIT_ERROR; + giterr_set( + GITERR_CHECKOUT, "Provided object cannot be peeled to a tree"); + return -1; } - if ((error = git_repository_index(&index, repo)) < 0) - goto cleanup; + /* load paths in tree that match pathspec into index */ + if (!(error = git_repository_index(&index, repo)) && + !(error = git_index_read_tree_match( + index, tree, opts ? &opts->paths : NULL)) && + !(error = git_index_write(index))) + error = git_checkout_index(repo, opts); - if ((error = git_index_read_tree(index, tree)) < 0) - goto cleanup; - - if ((error = git_index_write(index)) < 0) - goto cleanup; - - error = git_checkout_index(repo, opts); - -cleanup: git_index_free(index); git_tree_free(tree); + return error; } @@ -552,21 +719,16 @@ int git_checkout_head( git_repository *repo, git_checkout_opts *opts) { - git_reference *head; int error; + git_reference *head = NULL; git_object *tree = NULL; assert(repo); - if ((error = git_repository_head(&head, repo)) < 0) - return error; + if (!(error = git_repository_head(&head, repo)) && + !(error = git_reference_peel(&tree, head, GIT_OBJ_TREE))) + error = git_checkout_tree(repo, tree, opts); - if ((error = git_reference_peel(&tree, head, GIT_OBJ_TREE)) < 0) - goto cleanup; - - error = git_checkout_tree(repo, tree, opts); - -cleanup: git_reference_free(head); git_object_free(tree); diff --git a/src/fileops.c b/src/fileops.c index d2cbd046d..5eebc5057 100644 --- a/src/fileops.c +++ b/src/fileops.c @@ -351,6 +351,7 @@ int git_futils_mkdir_r(const char *path, const char *base, const mode_t mode) } typedef struct { + const char *base; uint32_t flags; int error; } futils__rmdir_data; @@ -366,11 +367,52 @@ static int futils__error_cannot_rmdir(const char *path, const char *filemsg) return -1; } +static int futils__rm_first_parent(git_buf *path, const char *ceiling) +{ + int error = GIT_ENOTFOUND; + struct stat st; + + while (error == GIT_ENOTFOUND) { + git_buf_rtruncate_at_char(path, '/'); + + if (!path->size || git__prefixcmp(path->ptr, ceiling) != 0) + error = 0; + else if (p_lstat(path->ptr, &st) == 0) { + if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) + error = p_unlink(path->ptr); + else if (!S_ISDIR(st.st_mode)) + error = -1; /* fail to remove non-regular file */ + } else if (errno != ENOTDIR) + error = -1; + } + + if (error) + futils__error_cannot_rmdir(path->ptr, "cannot remove parent"); + + return error; +} + static int futils__rmdir_recurs_foreach(void *opaque, git_buf *path) { + struct stat st; futils__rmdir_data *data = opaque; - if (git_path_isdir(path->ptr) == true) { + if ((data->error = p_lstat(path->ptr, &st)) < 0) { + if (errno == ENOENT) + data->error = 0; + else if (errno == ENOTDIR) { + /* asked to remove a/b/c/d/e and a/b is a normal file */ + if ((data->flags & GIT_RMDIR_REMOVE_BLOCKERS) != 0) + data->error = futils__rm_first_parent(path, data->base); + else + futils__error_cannot_rmdir( + path->ptr, "parent is not directory"); + } + else + futils__error_cannot_rmdir(path->ptr, "cannot access"); + } + + else if (S_ISDIR(st.st_mode)) { int error = git_path_direach(path, futils__rmdir_recurs_foreach, data); if (error < 0) return (error == GIT_EUSER) ? data->error : error; @@ -393,9 +435,8 @@ static int futils__rmdir_recurs_foreach(void *opaque, git_buf *path) futils__error_cannot_rmdir(path->ptr, "cannot be removed"); } - else if ((data->flags & GIT_RMDIR_SKIP_NONEMPTY) == 0) { + else if ((data->flags & GIT_RMDIR_SKIP_NONEMPTY) == 0) data->error = futils__error_cannot_rmdir(path->ptr, "still present"); - } return data->error; } @@ -434,6 +475,7 @@ 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; diff --git a/src/fileops.h b/src/fileops.h index 6952c463c..a74f8b758 100644 --- a/src/fileops.h +++ b/src/fileops.h @@ -109,6 +109,7 @@ extern int git_futils_mkpath2file(const char *path, const mode_t mode); * * GIT_RMDIR_SKIP_NONEMPTY - skip non-empty directories with no error. * * GIT_RMDIR_EMPTY_PARENTS - remove containing directories up to base * if removing this item leaves them empty + * * GIT_RMDIR_REMOVE_BLOCKERS - remove blocking file that causes ENOTDIR * * The old values translate into the new as follows: * @@ -121,6 +122,7 @@ typedef enum { GIT_RMDIR_REMOVE_FILES = (1 << 0), GIT_RMDIR_SKIP_NONEMPTY = (1 << 1), GIT_RMDIR_EMPTY_PARENTS = (1 << 2), + GIT_RMDIR_REMOVE_BLOCKERS = (1 << 3), } git_futils_rmdir_flags; /** diff --git a/src/index.c b/src/index.c index c1b4565a3..0ae1b4479 100644 --- a/src/index.c +++ b/src/index.c @@ -13,6 +13,8 @@ #include "tree.h" #include "tree-cache.h" #include "hash.h" +#include "iterator.h" +#include "pathspec.h" #include "git2/odb.h" #include "git2/oid.h" #include "git2/blob.h" @@ -1590,3 +1592,54 @@ git_repository *git_index_owner(const git_index *index) { return INDEX_OWNER(index); } + +int git_index_read_tree_match( + git_index *index, git_tree *tree, git_strarray *strspec) +{ +#if 0 + git_iterator *iter = NULL; + const git_index_entry *entry; + char *pfx = NULL; + git_vector pathspec = GIT_VECTOR_INIT; + git_pool pathpool = GIT_POOL_INIT_STRINGPOOL; +#endif + + if (!git_pathspec_is_interesting(strspec)) + return git_index_read_tree(index, tree); + + return git_index_read_tree(index, tree); + +#if 0 + /* The following loads the matches into the index, but doesn't + * erase obsoleted entries (e.g. you load a blob at "a/b" which + * should obsolete a blob at "a/b/c/d" since b is no longer a tree) + */ + + if (git_pathspec_init(&pathspec, strspec, &pathpool) < 0) + return -1; + + pfx = git_pathspec_prefix(strspec); + + if ((error = git_iterator_for_tree_range( + &iter, INDEX_OWNER(index), tree, pfx, pfx)) < 0 || + (error = git_iterator_current(iter, &entry)) < 0) + goto cleanup; + + while (entry != NULL) { + if (git_pathspec_match_path(&pathspec, entry->path, false, false) && + (error = git_index_add(index, entry)) < 0) + goto cleanup; + + if ((error = git_iterator_advance(iter, &entry)) < 0) + goto cleanup; + } + +cleanup: + git_iterator_free(iter); + git_pathspec_free(&pathspec); + git_pool_clear(&pathpool); + git__free(pfx); + + return error; +#endif +} diff --git a/src/index.h b/src/index.h index 9778a543a..f0dcd64d5 100644 --- a/src/index.h +++ b/src/index.h @@ -48,4 +48,7 @@ extern unsigned int git_index__prefix_position(git_index *index, const char *pat extern int git_index_entry__cmp(const void *a, const void *b); extern int git_index_entry__cmp_icase(const void *a, const void *b); +extern int git_index_read_tree_match( + git_index *index, git_tree *tree, git_strarray *strspec); + #endif diff --git a/src/path.c b/src/path.c index 09556bd3f..98351bec3 100644 --- a/src/path.c +++ b/src/path.c @@ -382,9 +382,10 @@ int git_path_walk_up( iter.asize = path->asize; while (scan >= stop) { - if ((error = cb(data, &iter)) < 0) - break; + error = cb(data, &iter); iter.ptr[scan] = oldc; + if (error < 0) + break; scan = git_buf_rfind_next(&iter, '/'); if (scan >= 0) { scan++; diff --git a/src/reset.c b/src/reset.c index 7df1c1a57..69a9c4f04 100644 --- a/src/reset.c +++ b/src/reset.c @@ -137,10 +137,7 @@ int git_reset( } memset(&opts, 0, sizeof(opts)); - opts.checkout_strategy = - GIT_CHECKOUT_CREATE_MISSING - | GIT_CHECKOUT_OVERWRITE_MODIFIED - | GIT_CHECKOUT_REMOVE_UNTRACKED; + opts.checkout_strategy = GIT_CHECKOUT_FORCE; if (git_checkout_index(repo, &opts) < 0) { giterr_set(GITERR_INDEX, "%s - Failed to checkout the index.", ERROR_MSG); diff --git a/src/stash.c b/src/stash.c index 1d6940e3c..7bff466d1 100644 --- a/src/stash.c +++ b/src/stash.c @@ -501,8 +501,8 @@ static int reset_index_and_workdir( memset(&opts, 0, sizeof(git_checkout_opts)); - opts.checkout_strategy = - GIT_CHECKOUT_CREATE_MISSING | GIT_CHECKOUT_OVERWRITE_MODIFIED; + opts.checkout_strategy = + GIT_CHECKOUT_UPDATE_MODIFIED | GIT_CHECKOUT_UPDATE_UNTRACKED; if (remove_untracked) opts.checkout_strategy |= GIT_CHECKOUT_REMOVE_UNTRACKED; diff --git a/tests-clar/checkout/index.c b/tests-clar/checkout/index.c index 18c59a45d..72044d01c 100644 --- a/tests-clar/checkout/index.c +++ b/tests-clar/checkout/index.c @@ -26,7 +26,7 @@ void test_checkout_index__initialize(void) git_tree *tree; memset(&g_opts, 0, sizeof(g_opts)); - g_opts.checkout_strategy = GIT_CHECKOUT_CREATE_MISSING; + g_opts.checkout_strategy = GIT_CHECKOUT_SAFE; g_repo = cl_git_sandbox_init("testrepo"); @@ -78,7 +78,6 @@ void test_checkout_index__can_create_missing_files(void) 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_CREATE_MISSING; cl_git_pass(git_checkout_index(g_repo, &g_opts)); test_file_contents("./testrepo/README", "hey there\n"); @@ -94,7 +93,7 @@ void test_checkout_index__can_remove_untracked_files(void) cl_assert_equal_i(true, git_path_isdir("./testrepo/dir/subdir/subsubdir")); - g_opts.checkout_strategy = GIT_CHECKOUT_REMOVE_UNTRACKED; + g_opts.checkout_strategy |= GIT_CHECKOUT_REMOVE_UNTRACKED; cl_git_pass(git_checkout_index(g_repo, &g_opts)); cl_assert_equal_i(false, git_path_isdir("./testrepo/dir")); @@ -203,7 +202,11 @@ void test_checkout_index__donot_overwrite_modified_file_by_default(void) { cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!"); - g_opts.checkout_strategy = 0; + /* 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_ALLOW_CONFLICTS; + cl_git_pass(git_checkout_index(g_repo, &g_opts)); test_file_contents("./testrepo/new.txt", "This isn't what's stored!"); @@ -213,7 +216,8 @@ void test_checkout_index__can_overwrite_modified_file(void) { cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!"); - g_opts.checkout_strategy = GIT_CHECKOUT_OVERWRITE_MODIFIED; + g_opts.checkout_strategy |= GIT_CHECKOUT_UPDATE_MODIFIED; + cl_git_pass(git_checkout_index(g_repo, &g_opts)); test_file_contents("./testrepo/new.txt", "my new file\n"); @@ -282,28 +286,30 @@ void test_checkout_index__options_open_flags(void) g_opts.file_open_flags = O_CREAT | O_RDWR | O_APPEND; - g_opts.checkout_strategy |= GIT_CHECKOUT_OVERWRITE_MODIFIED; + g_opts.checkout_strategy |= GIT_CHECKOUT_UPDATE_MODIFIED; cl_git_pass(git_checkout_index(g_repo, &g_opts)); test_file_contents("./testrepo/new.txt", "hi\nmy new file\n"); } -struct notify_data { +struct conflict_data { const char *file; const char *sha; }; -static int notify_cb( - const char *skipped_file, +static int conflict_cb( + const char *conflict_file, const git_oid *blob_oid, - int file_mode, + unsigned int index_mode, + unsigned int wd_mode, void *payload) { - struct notify_data *expectations = (struct notify_data *)payload; + struct conflict_data *expectations = (struct conflict_data *)payload; - GIT_UNUSED(file_mode); + GIT_UNUSED(index_mode); + GIT_UNUSED(wd_mode); - cl_assert_equal_s(expectations->file, skipped_file); + cl_assert_equal_s(expectations->file, conflict_file); cl_assert_equal_i(0, git_oid_streq(blob_oid, expectations->sha)); return 0; @@ -311,7 +317,7 @@ static int notify_cb( void test_checkout_index__can_notify_of_skipped_files(void) { - struct notify_data data; + struct conflict_data data; cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!"); @@ -324,22 +330,24 @@ void test_checkout_index__can_notify_of_skipped_files(void) data.file = "new.txt"; data.sha = "a71586c1dfe8a71c6cbf6c129f404c5642ff31bd"; - g_opts.checkout_strategy = GIT_CHECKOUT_CREATE_MISSING; - g_opts.skipped_notify_cb = notify_cb; - g_opts.notify_payload = &data; + g_opts.checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS; + g_opts.conflict_cb = conflict_cb; + g_opts.conflict_payload = &data; cl_git_pass(git_checkout_index(g_repo, &g_opts)); } -static int dont_notify_cb( - const char *skipped_file, +static int dont_conflict_cb( + const char *conflict_file, const git_oid *blob_oid, - int file_mode, + unsigned int index_mode, + unsigned int wd_mode, void *payload) { - GIT_UNUSED(skipped_file); + GIT_UNUSED(conflict_file); GIT_UNUSED(blob_oid); - GIT_UNUSED(file_mode); + GIT_UNUSED(index_mode); + GIT_UNUSED(wd_mode); GIT_UNUSED(payload); cl_assert(false); @@ -354,9 +362,9 @@ void test_checkout_index__wont_notify_of_expected_line_ending_changes(void) cl_git_mkfile("./testrepo/new.txt", "my new file\r\n"); - g_opts.checkout_strategy = GIT_CHECKOUT_CREATE_MISSING; - g_opts.skipped_notify_cb = dont_notify_cb; - g_opts.notify_payload = NULL; + g_opts.checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS; + g_opts.conflict_cb = dont_conflict_cb; + g_opts.conflict_payload = NULL; cl_git_pass(git_checkout_index(g_repo, &g_opts)); } @@ -389,8 +397,9 @@ void test_checkout_index__can_overcome_name_clashes(void) cl_git_pass(p_mkdir("./testrepo/path1", 0777)); cl_git_mkfile("./testrepo/path1/file1", "content\r\n"); - cl_git_pass(git_index_add(index, "path0", 0)); - cl_git_pass(git_index_add(index, "path1/file1", 0)); + cl_git_pass(git_index_add_from_workdir(index, "path0")); + cl_git_pass(git_index_add_from_workdir(index, "path1/file1")); + cl_git_pass(p_unlink("./testrepo/path0")); cl_git_pass(git_futils_rmdir_r( @@ -400,11 +409,20 @@ void test_checkout_index__can_overcome_name_clashes(void) cl_git_pass(p_mkdir("./testrepo/path0", 0777)); cl_git_mkfile("./testrepo/path0/file0", "content\r\n"); - g_opts.checkout_strategy = GIT_CHECKOUT_CREATE_MISSING; + cl_assert(git_path_isfile("./testrepo/path1")); + cl_assert(git_path_isfile("./testrepo/path0/file0")); + + g_opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS; cl_git_pass(git_checkout_index(g_repo, &g_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, &g_opts)); + + cl_assert(git_path_isfile("./testrepo/path0")); + cl_assert(git_path_isfile("./testrepo/path1/file1")); + git_index_free(index); } diff --git a/tests-clar/checkout/tree.c b/tests-clar/checkout/tree.c index c42aefc31..983425324 100644 --- a/tests-clar/checkout/tree.c +++ b/tests-clar/checkout/tree.c @@ -12,7 +12,7 @@ void test_checkout_tree__initialize(void) g_repo = cl_git_sandbox_init("testrepo"); memset(&g_opts, 0, sizeof(g_opts)); - g_opts.checkout_strategy = GIT_CHECKOUT_CREATE_MISSING; + g_opts.checkout_strategy = GIT_CHECKOUT_SAFE; } void test_checkout_tree__cleanup(void) @@ -74,10 +74,13 @@ static void progress(const char *path, size_t cur, size_t tot, void *payload) void test_checkout_tree__calls_progress_callback(void) { bool was_called = 0; + g_opts.progress_cb = progress; g_opts.progress_payload = &was_called; cl_git_pass(git_revparse_single(&g_object, g_repo, "master")); + cl_git_pass(git_checkout_tree(g_repo, g_object, &g_opts)); + cl_assert_equal_i(was_called, true); } diff --git a/tests-clar/checkout/typechange.c b/tests-clar/checkout/typechange.c index e86af52ee..cd34885de 100644 --- a/tests-clar/checkout/typechange.c +++ b/tests-clar/checkout/typechange.c @@ -40,10 +40,12 @@ void test_checkout_typechange__checkout_typechanges(void) git_object *obj; git_checkout_opts opts = {0}; - opts.checkout_strategy = - GIT_CHECKOUT_REMOVE_UNTRACKED | - GIT_CHECKOUT_CREATE_MISSING | - GIT_CHECKOUT_OVERWRITE_MODIFIED; + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + + /* if you don't include GIT_CHECKOUT_REMOVE_UNTRACKED then on the final + * checkout which is supposed to remove all the files, we will not + * actually remove them! + */ for (i = 0; g_typechange_oids[i] != NULL; ++i) { cl_git_pass(git_revparse_single(&obj, g_repo, g_typechange_oids[i])); @@ -51,6 +53,9 @@ void test_checkout_typechange__checkout_typechanges(void) cl_git_pass(git_checkout_tree(g_repo, obj, &opts)); + cl_git_pass( + git_repository_set_head_detached(g_repo, git_object_id(obj))); + git_object_free(obj); if (!g_typechange_empty[i]) { diff --git a/tests-clar/clone/network.c b/tests-clar/clone/network.c index 19385f77a..68fa8eb6b 100644 --- a/tests-clar/clone/network.c +++ b/tests-clar/clone/network.c @@ -113,7 +113,7 @@ void test_clone_network__can_checkout_a_cloned_repo(void) bool checkout_progress_cb_was_called = false, fetch_progress_cb_was_called = false; - opts.checkout_strategy = GIT_CHECKOUT_CREATE_MISSING; + opts.checkout_strategy = GIT_CHECKOUT_UPDATE_UNMODIFIED; opts.progress_cb = &checkout_progress; opts.progress_payload = &checkout_progress_cb_was_called;