From e9e20c8474b4f7ecdb1076c2f2d9c06f21e6be5b Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Wed, 19 Dec 2012 14:52:12 -0800 Subject: [PATCH 01/26] Update cl_git_pass to return more info This adds a failure reporting function that is called by cl_git_pass which captures the actual error return code and the error message if available in the failure report. --- tests-clar/clar_libgit2.c | 10 ++++++++++ tests-clar/clar_libgit2.h | 18 +++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/tests-clar/clar_libgit2.c b/tests-clar/clar_libgit2.c index ce3ec4af4..88ffb2bca 100644 --- a/tests-clar/clar_libgit2.c +++ b/tests-clar/clar_libgit2.c @@ -2,6 +2,16 @@ #include "posix.h" #include "path.h" +void cl_git_report_failure( + int error, const char *file, int line, const char *fncall) +{ + char msg[4096]; + const git_error *last = giterr_last(); + p_snprintf(msg, 4096, "error %d - %s", + error, last ? last->message : ""); + clar__assert(0, file, line, fncall, msg, 1); +} + void cl_git_mkfile(const char *filename, const char *content) { int fd; diff --git a/tests-clar/clar_libgit2.h b/tests-clar/clar_libgit2.h index 91a542654..321ec5f2f 100644 --- a/tests-clar/clar_libgit2.h +++ b/tests-clar/clar_libgit2.h @@ -6,17 +6,17 @@ #include "common.h" /** - * Special wrapper for `clar_must_pass` that passes - * the last library error as the test failure message. + * Replace for `clar_must_pass` that passes the last library error as the + * test failure message. * - * Use this wrapper around all `git_` library calls that - * return error codes! + * Use this wrapper around all `git_` library calls that return error codes! */ #define cl_git_pass(expr) do { \ + int _lg2_error; \ giterr_clear(); \ - if ((expr) != 0) \ - clar__assert(0, __FILE__, __LINE__, "Function call failed: " #expr, giterr_last() ? giterr_last()->message : NULL, 1); \ - } while(0) + if ((_lg2_error = (expr)) != 0) \ + cl_git_report_failure(_lg2_error, __FILE__, __LINE__, "Function call failed: " #expr); \ + } while (0) /** * Wrapper for `clar_must_fail` -- this one is @@ -25,6 +25,10 @@ */ #define cl_git_fail(expr) cl_must_fail(expr) +#define cl_git_fail_with(expr, error) cl_assert_equal_i(error,expr) + +void cl_git_report_failure(int, const char *, int, const char *); + #define cl_assert_equal_sz(sz1,sz2) cl_assert((sz1) == (sz2)) /* From 6ac724afbe0b1244f84b9acc5dc8be0646be5ce3 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Tue, 18 Dec 2012 15:12:06 -0800 Subject: [PATCH 02/26] Clear error to avoid leaving invalid error behind --- src/attr.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/attr.c b/src/attr.c index 95d63bea8..1b414417e 100644 --- a/src/attr.c +++ b/src/attr.c @@ -572,8 +572,10 @@ static int collect_attr_files( error = git_futils_find_system_file(&dir, GIT_ATTR_FILE_SYSTEM); if (!error) error = push_attr_file(repo, files, NULL, dir.ptr); - else if (error == GIT_ENOTFOUND) + else if (error == GIT_ENOTFOUND) { + giterr_clear(); error = 0; + } } cleanup: From 6fee906c982d001062968b4caee4f289f0c86b59 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Tue, 18 Dec 2012 15:13:11 -0800 Subject: [PATCH 03/26] missing error message is confusing --- src/index.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.c b/src/index.c index c04796875..9f2012b3a 100644 --- a/src/index.c +++ b/src/index.c @@ -814,7 +814,10 @@ int git_index_find(git_index *index, const char *path) if ((pos = git_vector_bsearch2( &index->entries, index->entries_search_path, path)) < 0) + { + giterr_set(GITERR_INDEX, "Index does not contain %s", path); return pos; + } /* Since our binary search only looked at path, we may be in the * middle of a list of stages. From d0951175d49ffff929b4ed1e08d45509360c3f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20G=C3=BClker?= Date: Thu, 6 Dec 2012 16:12:21 +0100 Subject: [PATCH 04/26] Add failing test to demonstrate wrong checkout behaviour --- tests-clar/checkout/tree.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests-clar/checkout/tree.c b/tests-clar/checkout/tree.c index 88dbe4ffc..90f215fc1 100644 --- a/tests-clar/checkout/tree.c +++ b/tests-clar/checkout/tree.c @@ -85,3 +85,23 @@ void test_checkout_tree__calls_progress_callback(void) cl_assert_equal_i(was_called, true); } + +void test_checkout_tree__doesnt_write_unrequested_files_to_worktree(void) +{ + git_oid master_oid; + git_oid chomped_oid; + git_commit* p_master_commit; + git_commit* p_chomped_commit; + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + + git_oid_fromstr(&master_oid, "a65fedf39aefe402d3bb6e24df4d4f5fe4547750"); + git_oid_fromstr(&chomped_oid, "e90810b8df3e80c413d903f631643c716887138d"); + 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_tree(g_repo, (git_object*)p_chomped_commit, &opts); + cl_assert_equal_i(false, git_path_isfile("testrepo/readme.txt")); +} From c5df10f4aa0debf113da3430849876a3a28a25ea Mon Sep 17 00:00:00 2001 From: Jameson Miller Date: Fri, 30 Nov 2012 10:59:03 -0500 Subject: [PATCH 05/26] Failing test on git_checkout_tree when removing directories --- tests-clar/checkout/tree.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests-clar/checkout/tree.c b/tests-clar/checkout/tree.c index 90f215fc1..79cfb6f87 100644 --- a/tests-clar/checkout/tree.c +++ b/tests-clar/checkout/tree.c @@ -48,6 +48,33 @@ void test_checkout_tree__can_checkout_a_subdirectory_from_a_commit(void) cl_assert_equal_i(true, git_path_isfile("./testrepo/ab/de/fgh/1.txt")); } +void test_checkout_tree__can_checkout_and_remove_directory(void) +{ + git_reference *head; + cl_assert_equal_i(false, git_path_isdir("./testrepo/ab/")); + + // Checkout brach "subtrees" and update HEAD, so that HEAD matches the current working tree + cl_git_pass(git_revparse_single(&g_object, g_repo, "subtrees")); + cl_git_pass(git_checkout_tree(g_repo, g_object, &g_opts)); + cl_git_pass(git_reference_lookup(&head, g_repo, "HEAD")); + cl_git_pass(git_reference_symbolic_set_target(head, "refs/heads/subtrees")); + git_reference_free(head); + + cl_assert_equal_i(true, git_path_isdir("./testrepo/ab/")); + cl_assert_equal_i(true, git_path_isfile("./testrepo/ab/de/2.txt")); + cl_assert_equal_i(true, git_path_isfile("./testrepo/ab/de/fgh/1.txt")); + + // Checkout brach "master" and update HEAD, so that HEAD matches the current working tree + cl_git_pass(git_revparse_single(&g_object, g_repo, "master")); + cl_git_pass(git_checkout_tree(g_repo, g_object, &g_opts)); + cl_git_pass(git_reference_lookup(&head, g_repo, "HEAD")); + cl_git_pass(git_reference_symbolic_set_target(head, "refs/heads/master")); + git_reference_free(head); + + // This directory should no longer exist + cl_assert_equal_i(false, git_path_isdir("./testrepo/ab/")); +} + void test_checkout_tree__can_checkout_a_subdirectory_from_a_subtree(void) { char *entries[] = { "de/" }; From bfe7d7de226e91c7ed99b68fc447aa0bcd5182ab Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Mon, 26 Nov 2012 17:24:02 -0800 Subject: [PATCH 06/26] Reorder operations in git reset This corrects the order of operations in git reset so that the checkout to reset the working directory content is done before the HEAD is moved. This allows us to use the HEAD and the index content to know what files can / should safely be reset. Unfortunately, there are still some cases where the behavior of this revision differs from core git. Notable, a file which has been added to the index but is not present in the HEAD is considered to be tracked by core git (and thus removable by a reset command) whereas since this loads the target state into the index prior to resetting, it will consider such a file to be untracked and won't touch it. That is a larger fix that I'll defer to a future commit. --- src/reset.c | 69 +++++++++++++++++++++++------------------------------ 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/src/reset.c b/src/reset.c index 04b0863b9..f5daa8f55 100644 --- a/src/reset.c +++ b/src/reset.c @@ -62,13 +62,10 @@ int git_reset( git_object *commit = NULL; git_index *index = NULL; git_tree *tree = NULL; - int error; + int error = 0; git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; assert(repo && target); - assert(reset_type == GIT_RESET_SOFT - || reset_type == GIT_RESET_MIXED - || reset_type == GIT_RESET_HARD); if (git_object_owner(target) != repo) { giterr_set(GITERR_OBJECT, @@ -76,57 +73,51 @@ int git_reset( return -1; } - if (reset_type != GIT_RESET_SOFT - && (error = git_repository__ensure_not_bare( - repo, + if (reset_type != GIT_RESET_SOFT && + (error = git_repository__ensure_not_bare(repo, reset_type == GIT_RESET_MIXED ? "reset mixed" : "reset hard")) < 0) - return error; + return error; - if ((error = git_object_peel(&commit, target, GIT_OBJ_COMMIT)) < 0) + if ((error = git_object_peel(&commit, target, GIT_OBJ_COMMIT)) < 0 || + (error = git_repository_index(&index, repo)) < 0 || + (error = git_commit_tree(&tree, (git_commit *)commit)) < 0) goto cleanup; - if ((error = git_repository_index(&index, repo)) < 0) - goto cleanup; - - if (reset_type == GIT_RESET_SOFT && + if (reset_type == GIT_RESET_SOFT && (git_repository_state(repo) == GIT_REPOSITORY_STATE_MERGE || - git_index_has_conflicts(index))) { - giterr_set(GITERR_OBJECT, "%s (soft) while in the middle of a merge.", ERROR_MSG); - error = GIT_EUNMERGED; - goto cleanup; + git_index_has_conflicts(index))) + { + giterr_set(GITERR_OBJECT, "%s (soft) in the middle of a merge.", ERROR_MSG); + error = GIT_EUNMERGED; + goto cleanup; } + /* move HEAD to the new target */ if ((error = update_head(repo, commit)) < 0) goto cleanup; - if (reset_type == GIT_RESET_SOFT) { - error = 0; - goto cleanup; + if (reset_type == GIT_RESET_HARD) { + /* overwrite working directory with HEAD */ + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + + if ((error = git_checkout_tree(repo, (git_object *)tree, &opts)) < 0) + goto cleanup; } - if ((error = git_commit_tree(&tree, (git_commit *)commit)) < 0) - goto cleanup; + if (reset_type > GIT_RESET_SOFT) { + /* reset index to the target content */ - if ((error = git_index_read_tree(index, tree)) < 0) - goto cleanup; + if ((error = git_repository_index(&index, repo)) < 0 || + (error = git_index_read_tree(index, tree)) < 0 || + (error = git_index_write(index)) < 0) + goto cleanup; - if ((error = git_index_write(index)) < 0) - goto cleanup; - - if ((error = git_repository_merge_cleanup(repo)) < 0) { - giterr_set(GITERR_REPOSITORY, "%s - Failed to clean up merge data.", ERROR_MSG); - goto cleanup; + if ((error = git_repository_merge_cleanup(repo)) < 0) { + giterr_set(GITERR_INDEX, "%s - failed to clean up merge data", ERROR_MSG); + goto cleanup; + } } - if (reset_type == GIT_RESET_MIXED) { - error = 0; - goto cleanup; - } - - opts.checkout_strategy = GIT_CHECKOUT_FORCE; - - error = git_checkout_index(repo, NULL, &opts); - cleanup: git_object_free(commit); git_index_free(index); From cf208031705388a2d1907fb9ec409ff22179f380 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Thu, 6 Dec 2012 13:36:17 -0800 Subject: [PATCH 07/26] Rework checkout internals (again) I've tried to map out the detailed behaviors of checkout and make sure that we're handling the various cases correctly, along with providing options to allow us to emulate "git checkout" and "git checkout-index" with the various flags. I've thrown away flags in the checkout API that seemed like clutter and added some new ones. Also, I've converted the conflict callback to a general notification callback so we can emulate "git checkout" output and display "dirty" files. As of this commit, the new behavior is not working 100% but some of that is probably baked into tests that are not testing the right thing. This is a decent snapshot point, I think, along the way to getting the update done. --- include/git2/checkout.h | 245 ++++--- src/checkout.c | 1127 +++++++++++++++++++----------- src/checkout.h | 31 + src/stash.c | 3 +- tests-clar/checkout/head.c | 2 +- tests-clar/checkout/index.c | 103 ++- tests-clar/checkout/tree.c | 2 +- tests-clar/checkout/typechange.c | 5 - tests-clar/reset/hard.c | 4 +- 9 files changed, 971 insertions(+), 551 deletions(-) create mode 100644 src/checkout.h diff --git a/include/git2/checkout.h b/include/git2/checkout.h index c36e2a41b..5eedd7bfd 100644 --- a/include/git2/checkout.h +++ b/include/git2/checkout.h @@ -24,105 +24,121 @@ GIT_BEGIN_DECL /** * Checkout behavior flags * - * These flags control what checkout does with files. Pass in a - * 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. + * 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. * - * 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. + * Checkout examines the differences between the target and expected trees + * plus the current working directory and groups files into five categories: * - * 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). + * 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. + * 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 + * 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. * * - * On top of these three basic strategies, there are some modifiers - * options that can be applied: + * You control the actions checkout takes with one of four base strategies: * - * 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. + * - `GIT_CHECKOUT_NONE` is the default and applies no changes. It is a dry + * run that you can use to find conflicts, etc. if you wish. * - * 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_SAFE` is like `git checkout` and only applies changes + * between the expected and target trees to files in category 2. * - * 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_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 + * emulating `git checkout-index` for some of the subtleties of this. * - * 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. + * - `GIT_CHECKOUT_FORCE` is like `git checkout -f` and will update the + * working directory to match the target content regardless of conflicts, + * overwriting dirty and conflicting files. + * + * + * There are some additional flags to modified the behavior of checkout: + * + * - GIT_CHECKOUT_ALLOW_CONFLICTS can be added to apply safe file updates + * even if there are conflicts. Normally, the entire checkout will be + * cancelled if any files are in category 4. With this flag, conflicts + * will be skipped (though the notification callback will still be invoked + * on the conflicting files if requested). + * + * - GIT_CHECKOUT_REMOVE_UNTRACKED means that files in the working directory + * that are untracked (but not ignored) should be deleted. The are not + * considered conflicts and would normally be ignored by checkout. + * + * - GIT_CHECKOUT_REMOVE_IGNORED means to remove ignored files from the + * working directory as well. Obviously, these would normally be ignored. + * + * - GIT_CHECKOUT_UPDATE_ONLY means to only update the content of files that + * already exist. Files will not be created nor deleted. This does not + * make adds and deletes into conflicts - it just skips applying those + * changes. This will also skip updates to typechanged files (since that + * would involve deleting the old and creating the new). + * + * - Unmerged entries in the index are also considered conflicts. The + * GIT_CHECKOUT_SKIP_UNMERGED flag causes us to skip files with unmerged + * index entries. You can also use GIT_CHECKOUT_USE_OURS and + * GIT_CHECKOUT_USE_THEIRS to proceeed with the checkout using either the + * stage 2 ("ours") or stage 3 ("theirs") version of files in the index. + * + * + * 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. + * + * To emulate `git checkout-index`, use `GIT_CHECKOUT_SAFE_CREATE` with a + * notification callback that cancels the operation if a dirty-but-existing + * file is found in the working directory. This core git command isn't + * quite "force" but is sensitive about some types of changes. + * + * To emulate `git checkout -f`, you use `GIT_CHECKOUT_FORCE`. * * * 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 + * conflicts, or you can use the notification callback to explicitly abort + * the action before any updates are made. Despite this, if a second + * process is modifying the filesystem while checkout is running, it can't * guarantee that the choices is makes while initially examining the * filesystem are still going to be correct as it applies them. */ typedef enum { - GIT_CHECKOUT_DEFAULT = 0, /** default is a dry run, no actual updates */ - - /** Allow update of entries where working dir matches HEAD. */ - GIT_CHECKOUT_UPDATE_UNMODIFIED = (1u << 0), - - /** Allow update of entries where working dir does not have file. */ - GIT_CHECKOUT_UPDATE_MISSING = (1u << 1), + GIT_CHECKOUT_NONE = 0, /** default is a dry run, no actual updates */ /** Allow safe updates that cannot overwrite uncommited data */ - GIT_CHECKOUT_SAFE = - (GIT_CHECKOUT_UPDATE_UNMODIFIED | GIT_CHECKOUT_UPDATE_MISSING), + GIT_CHECKOUT_SAFE = (1u << 0), - /** 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 safe updates plus creation of missing files */ + GIT_CHECKOUT_SAFE_CREATE = (1u << 1), /** 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), + GIT_CHECKOUT_FORCE = (1u << 2), - /** Allow checkout to make updates even if conflicts are found */ + + /** Allow checkout to make safe 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), + /** Remove ignored files not in index */ + GIT_CHECKOUT_REMOVE_IGNORED = (1u << 6), + /** Only update existing files, don't create new ones */ - GIT_CHECKOUT_UPDATE_ONLY = (1u << 6), + GIT_CHECKOUT_UPDATE_ONLY = (1u << 7), /** * THE FOLLOWING OPTIONS ARE NOT YET IMPLEMENTED @@ -142,35 +158,86 @@ typedef enum { } git_checkout_strategy_t; +/** + * Checkout notification flags + * + * When running a checkout, you can set a notification callback (`notify_cb`) + * to be invoked for some or all files to be checked out. Which files + * receive a callback depend on the `notify_flags` value which is a + * combination of these flags. + * + * - GIT_CHECKOUT_NOTIFY_CONFLICTS 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 + * content. Core git displays these files when checkout runs, but does + * not stop the checkout. For these, `status_flags` will have only work + * tree bits set (i.e. GIT_STATUS_WT_MODIFIED, etc). + * + * - GIT_CHECKOUT_NOTIFY_UPDATED sends notification for any file changed by + * the checkout. Callback `status_flags` will have only index bits set. + * + * - GIT_CHECKOUT_NOTIFY_UNTRACKED notifies for all untracked files that + * are not ignored. Passing GIT_CHECKOUT_REMOVE_UNTRACKED would remove + * these files. The `status_flags` will be GIT_STATUS_WT_NEW. + * + * - GIT_CHECKOUT_NOTIFY_IGNORED notifies for the ignored files. Passing + * GIT_CHECKOUT_REMOVE_IGNORED will remove these. The `status_flags` + * will be to GIT_STATUS_IGNORED. + * + * If you return a non-zero value from the notify callback, the checkout + * will be canceled. Notification callbacks are made prior to making any + * modifications, so returning non-zero will cancel the entire checkout. + * If you are do not use GIT_CHECKOUT_ALLOW_CONFLICTS and there are + * conflicts, you don't need to explicitly cancel from the callback. + * Checkout itself will abort after all files are processed. + * + * To emulate core git checkout output, use GIT_CHECKOUT_NOTIFY_CONFLICTS + * and GIT_CHECKOUT_NOTIFY_DIRTY. Conflicts will have `status_flags` with + * changes in both the index and work tree (see the `git_status_t` values). + * Dirty files will only have work tree flags set. + */ +typedef enum { + GIT_CHECKOUT_NOTIFY_CONFLICTS = (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_t; + /** * Checkout options structure * * Use zeros to indicate default settings. - * This needs to be initialized with the `GIT_CHECKOUT_OPTS_INIT` macro: + * + * This should be initialized with the `GIT_CHECKOUT_OPTS_INIT` macro to + * correctly set the `version` field. * * git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; */ typedef struct git_checkout_opts { unsigned int version; + 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 */ + int disable_filters; /** don't apply filters like CRLF conversion */ + unsigned int dir_mode; /** default is 0755 */ + unsigned 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 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 (*conflict_cb)( - const char *conflicting_path, + unsigned int notify_flags; /** see `git_checkout_notify_t` above */ + int (*notify_cb)( + const char *path, + unsigned int status_flags, /** combo of git_status_t values */ const git_oid *index_oid, - unsigned int index_mode, - unsigned int wd_mode, + unsigned int checkout_mode, + unsigned int workdir_mode, void *payload); - void *conflict_payload; + void *notify_payload; /* Optional callback to notify the consumer of checkout progress. */ void (*progress_cb)( @@ -184,14 +251,16 @@ typedef struct git_checkout_opts { * paths should be taken into account, otherwise all files. */ git_strarray paths; + + git_tree *baseline; /** expected content of workdir, defaults to HEAD */ } git_checkout_opts; #define GIT_CHECKOUT_OPTS_VERSION 1 #define GIT_CHECKOUT_OPTS_INIT {GIT_CHECKOUT_OPTS_VERSION} /** - * Updates files in the index and the working tree to match the content of the - * commit pointed at by HEAD. + * Updates files in the index and the working tree to match the content of + * the commit pointed at by HEAD. * * @param repo repository to check out (must be non-bare) * @param opts specifies checkout options (may be NULL) diff --git a/src/checkout.c b/src/checkout.c index 66eb698ab..8e8c41bd5 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -7,7 +7,8 @@ #include -#include "git2/checkout.h" +#include "checkout.h" + #include "git2/repository.h" #include "git2/refs.h" #include "git2/tree.h" @@ -15,19 +16,180 @@ #include "git2/config.h" #include "git2/diff.h" -#include "common.h" #include "refs.h" -#include "buffer.h" #include "repository.h" #include "filter.h" #include "blob.h" #include "diff.h" #include "pathspec.h" +/* Key + * === + * B1,B2,B3 - blobs with different SHAs, + * Bi - ignored blob (WD only) + * T1,T2,T3 - trees with different SHAs, + * Ti - ignored tree (WD only) + * x - nothing + */ + +/* Diff with 2 non-workdir iterators + * ================================= + * Old New + * --- --- + * 0 x x - nothing + * 1 x B1 - added blob + * 2 x T1 - added tree + * 3 B1 x - removed blob + * 4 B1 B1 - unmodified blob + * 5 B1 B2 - modified blob + * 6 B1 T1 - typechange blob -> tree + * 7 T1 x - removed tree + * 8 T1 B1 - typechange tree -> blob + * 9 T1 T1 - unmodified tree + * 10 T1 T2 - modified tree (implies modified/added/removed blob inside) + */ + +/* Diff with non-work & workdir iterators + * ====================================== + * Old New-WD + * --- ------ + * 0 x x - nothing + * 1 x B1 - added blob + * 2 x Bi - ignored file + * 3 x T1 - added tree + * 4 x Ti - ignored tree + * 5 B1 x - removed blob + * 6 B1 B1 - unmodified blob + * 7 B1 B2 - modified blob + * 8 B1 T1 - typechange blob -> tree + * 9 B1 Ti - removed blob AND ignored tree as separate items + * 10 T1 x - removed tree + * 11 T1 B1 - typechange tree -> blob + * 12 T1 Bi - removed tree AND ignored blob as separate items + * 13 T1 T1 - unmodified tree + * 14 T1 T2 - modified tree (implies modified/added/removed blob inside) + * + * If there is a corresponding blob in the old, Bi is irrelevant + * If there is a corresponding tree in the old, Ti is irrelevant + */ + +/* Checkout From 3 Iterators (2 not workdir, 1 workdir) + * ==================================================== + * + * (Expect == Old HEAD / Desire == What To Checkout / Actual == Workdir) + * + * Expect Desire Actual-WD + * ------ ------ ------ + * 0 x x x - nothing + * 1 x x B1/Bi/T1/Ti - untracked/ignored blob/tree (SAFE) + * 2+ x B1 x - add blob (SAFE) + * 3 x B1 B1 - independently added blob (FORCEABLE-2) + * 4* x B1 B2/Bi/T1/Ti - add blob with content conflict (FORCEABLE-2) + * 5+ x T1 x - add tree (SAFE) + * 6* x T1 B1/Bi - add tree with blob conflict (FORCEABLE-2) + * 7 x T1 T1/i - independently added tree (SAFE+MISSING) + * 8 B1 x x - independently deleted blob (SAFE+MISSING) + * 9- B1 x B1 - delete blob (SAFE) + * 10- B1 x B2 - delete of modified blob (FORCEABLE-1) + * 11 B1 x T1/Ti - independently deleted blob AND untrack/ign tree (SAFE+MISSING !!!) + * 12 B1 B1 x - locally deleted blob (DIRTY || SAFE+CREATE) + * 13+ B1 B2 x - update to deleted blob (SAFE+MISSING) + * 14 B1 B1 B1 - unmodified file (SAFE) + * 15 B1 B1 B2 - locally modified file (DIRTY) + * 16+ B1 B2 B1 - update unmodified blob (SAFE) + * 17 B1 B2 B2 - independently updated blob (FORCEABLE-1) + * 18+ B1 B2 B3 - update to modified blob (FORCEABLE-1) + * 19 B1 B1 T1/Ti - locally deleted blob AND untrack/ign tree (DIRTY) + * 20* B1 B2 T1/Ti - update to deleted blob AND untrack/ign tree (F-1) + * 21+ B1 T1 x - add tree with locally deleted blob (SAFE+MISSING) + * 22* B1 T1 B1 - add tree AND deleted blob (SAFE) + * 23* B1 T1 B2 - add tree with delete of modified blob (F-1) + * 24 B1 T1 T1 - add tree with deleted blob (F-1) + * 25 T1 x x - independently deleted tree (SAFE+MISSING) + * 26 T1 x B1/Bi - independently deleted tree AND untrack/ign blob (F-1) + * 27- T1 x T1 - deleted tree (MAYBE SAFE) + * 28+ T1 B1 x - deleted tree AND added blob (SAFE+MISSING) + * 29 T1 B1 B1 - independently typechanged tree -> blob (F-1) + * 30+ T1 B1 B2 - typechange tree->blob with conflicting blob (F-1) + * 31* T1 B1 T1/T2 - typechange tree->blob (MAYBE SAFE) + * 32+ T1 T1 x - restore locally deleted tree (SAFE+MISSING) + * 33 T1 T1 B1/Bi - locally typechange tree->untrack/ign blob (DIRTY) + * 34 T1 T1 T1/T2 - unmodified tree (MAYBE SAFE) + * 35+ T1 T2 x - update locally deleted tree (SAFE+MISSING) + * 36* T1 T2 B1/Bi - update to tree with typechanged tree->blob conflict (F-1) + * 37 T1 T2 T1/T2/T3 - update to existing tree (MAYBE SAFE) + * + * The number will be followed by ' ' if no change is needed or '+' if the + * case needs to write to disk or '-' if something must be deleted and '*' + * if there should be a delete followed by an write. + * + * There are four tiers of safe cases: + * - SAFE == completely safe to update + * - SAFE+MISSING == safe except the workdir is missing the expect content + * - MAYBE SAFE == safe if workdir tree matches (or is missing) expected + * content, which is unknown at this point + * - FORCEABLE == conflict unless FORCE is given + * - DIRTY == no conflict but change is not applied unless FORCE + * + * Some slightly unusual circumstances: + * 8 - parent dir is only deleted when file is, so parent will be left if + * empty even though it would be deleted if the file were present + * 11 - core git does not consider this a conflict but attempts to delete T1 + * and gives "unable to unlink file" error yet does not skip the rest + * of the operation + * 12 - without FORCE file is left deleted (i.e. not restored) so new wd is + * dirty (and warning message "D file" is printed), with FORCE, file is + * restored. + * 24 - This should be considered MAYBE SAFE since effectively it is 7 and 8 + * combined, but core git considers this a conflict unless forced. + * 26 - This combines two cases (1 & 25) (and also implied 8 for tree content) + * which are ok on their own, but core git treat this as a conflict. + * If not forced, this is a conflict. If forced, this actually doesn't + * have to write anything and leaves the new blob as an untracked file. + * 32 - This is the only case where the expected and desired 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 + * either already right or is "dirty"). However, since this case also + * implies that a ?/B1/x case will exist as well, it can be skipped. + * + * Cases 3, 17, 24, 26, and 29 are all considered conflicts even though + * none of them will require making any updates to the working directory. + */ + +/* expect desire wd + * 1 x x T -> ignored dir OR untracked dir OR parent dir + * 2 x x I -> ignored file + * 3 x x A -> untracked file + * 4 x A x -> add from index (no conflict) + * 5 x A A -> independently added file + * 6 x A B -> add with conflicting file + * 7 A x x -> independently deleted file + * 8 A x A -> delete from index (no conflict) + * 9 A x B -> delete of modified file + * 10 A A x -> locally deleted file + * 11 A A A -> unmodified file (no conflict) + * 12 A A B -> locally modified + * 13 A B x -> update of deleted file + * 14 A B A -> update of unmodified file (no conflict) + * 15 A B B -> independently updated file + * 16 A B C -> update of modified file + */ + +enum { + CHECKOUT_ACTION__NONE = 0, + CHECKOUT_ACTION__REMOVE = 1, + CHECKOUT_ACTION__UPDATE_BLOB = 2, + CHECKOUT_ACTION__UPDATE_SUBMODULE = 4, + CHECKOUT_ACTION__CONFLICT = 8, + CHECKOUT_ACTION__MAX = 8, + CHECKOUT_ACTION__REMOVE_EMPTY = 16, +}; + typedef struct { git_repository *repo; git_diff_list *diff; git_checkout_opts *opts; + const char *pfx; git_buf *path; size_t workdir_len; bool can_symlink; @@ -36,6 +198,323 @@ typedef struct { size_t completed_steps; } checkout_diff_data; +static int checkout_notify( + checkout_diff_data *data, + git_checkout_notify_t why, + const git_diff_delta *delta, + const git_index_entry *wditem) +{ + GIT_UNUSED(data); + GIT_UNUSED(why); + GIT_UNUSED(delta); + GIT_UNUSED(wditem); + return 0; +} + +static bool checkout_is_workdir_modified( + checkout_diff_data *data, + const git_diff_file *item, + const git_index_entry *wditem) +{ + git_oid oid; + + if (item->size != wditem->file_size) + return true; + + if (git_diff__oid_for_file( + data->repo, wditem->path, wditem->mode, + wditem->file_size, &oid) < 0) + return false; + + return (git_oid_cmp(&item->oid, &oid) != 0); +} + +static int checkout_action_for_delta( + checkout_diff_data *data, + const git_diff_delta *delta, + const git_index_entry *wditem) +{ + 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; + + /* 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; + } + } + + /* 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; + } + } + + /* 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; + + 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 && (strat & GIT_CHECKOUT_UPDATE_ONLY) != 0) + action = (action & ~CHECKOUT_ACTION__REMOVE); + + if (action > 0 && (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; + } + + if ((action & CHECKOUT_ACTION__CONFLICT) != 0) { + if (checkout_notify( + data, GIT_CHECKOUT_NOTIFY_CONFLICTS, delta, wditem)) + 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) +{ + int cmp = -1; + const git_index_entry *wditem = *wditem_ptr; + + 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)) + return GIT_EUSER; + + if (git_iterator_advance(actual, wditem_ptr) < 0) + break; + + wditem = *wditem_ptr; + cmp = -1; + } + + *cmp_out = cmp; + + return 0; +} + +static int checkout_get_actions( + uint32_t **actions_ptr, + size_t **counts_ptr, + checkout_diff_data *data) +{ + 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) + return -1; + + if ((error = git_iterator_for_workdir_range( + &actual, data->repo, data->pfx, data->pfx)) < 0 || + (error = git_iterator_current(actual, &wditem)) < 0) + goto fail; + + deltas = &data->diff->deltas; + + *counts_ptr = counts = git__calloc(CHECKOUT_ACTION__MAX+1, sizeof(size_t)); + *actions_ptr = actions = git__calloc( + deltas->length ? deltas->length : 1, sizeof(uint32_t)); + if (!counts || !actions) { + error = -1; + goto fail; + } + + git_vector_foreach(deltas, i, delta) { + int cmp = -1, act; + + /* 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) + 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 && !allow_conflicts) { + 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); + + return 0; + +fail: + *counts_ptr = NULL; + git__free(counts); + *actions_ptr = NULL; + git__free(actions); + + git_iterator_free(actual); + git_pathspec_free(&pathspec); + git_pool_clear(&pathpool); + + return error; +} + static int buffer_to_file( git_buf *buffer, const char *path, @@ -203,350 +682,52 @@ static int checkout_blob( return error; } -static int retrieve_symlink_caps(git_repository *repo, bool *out) -{ - git_config *cfg; - int can_symlink = 0; - int error; - - if (git_repository_config__weakptr(&cfg, repo) < 0) - return -1; - - 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; - } - - if (error >= 0) - *out = can_symlink; - - return error; -} - -static void normalize_options( - git_checkout_opts *normalized, git_checkout_opts *proposed) -{ - assert(normalized); - - if (!proposed) - GIT_INIT_STRUCTURE(normalized, GIT_CHECKOUT_OPTS_VERSION); - else - memmove(normalized, proposed, sizeof(git_checkout_opts)); - - /* 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; - - if (!normalized->file_open_flags) - normalized->file_open_flags = O_CREAT | O_TRUNC | O_WRONLY; -} - -enum { - CHECKOUT_ACTION__NONE = 0, - CHECKOUT_ACTION__REMOVE = 1, - CHECKOUT_ACTION__UPDATE_BLOB = 2, - CHECKOUT_ACTION__UPDATE_SUBMODULE = 4, - CHECKOUT_ACTION__CONFLICT = 8, - CHECKOUT_ACTION__MAX = 8 -}; - -static int checkout_confirm_update_blob( - checkout_diff_data *data, - const git_diff_delta *delta, - int action) -{ - 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_lstat_posixly(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_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_ADDED: - /* Impossible. New files should be UNTRACKED or TYPECHANGE */ - action = CHECKOUT_ACTION__CONFLICT; - break; - - case GIT_DELTA_DELETED: - 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: - 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 > 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; -} - -static int checkout_get_actions( - uint32_t **actions_ptr, - size_t **counts_ptr, +static int checkout_remove_the_old( + unsigned int *actions, checkout_diff_data *data) { - int error; - git_diff_list *diff = data->diff; + int error = 0; git_diff_delta *delta; - size_t i, *counts = NULL; - uint32_t *actions = NULL; - git_tree *head = NULL; - git_iterator *hiter = NULL; - char *pfx = git_pathspec_prefix(&data->opts->paths); - const git_index_entry *he; + size_t i; + const char *workdir = git_buf_cstr(data->path); - /* if there is no HEAD, that's okay - we'll make an empty iterator */ - if (((error = git_repository_head_tree(&head, data->repo)) < 0) && - !(error == GIT_ENOTFOUND || error == GIT_EORPHANEDHEAD)) - return -1; + git_buf_truncate(data->path, data->workdir_len); - if ((error = git_iterator_for_tree_range(&hiter, head, pfx, pfx)) < 0) - goto fail; + 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 ((diff->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0 && - (error = git_iterator_spoolandsort_push(hiter, true)) < 0) - goto fail; + if (!empty_only) + flg |= GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_REMOVE_BLOCKERS; - if ((error = git_iterator_current(hiter, &he)) < 0) - goto fail; + error = git_futils_rmdir_r(delta->old_file.path, workdir, flg); - git__free(pfx); - pfx = NULL; + /* 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) + return error; - *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->pfxcomp(he->path, delta->new_file.path) : - diff->strcomp(he->path, delta->old_file.path); - if (cmp >= 0) - break; - if (git_iterator_advance(hiter, &he) < 0) - he = NULL; + data->completed_steps++; + report_progress(data, delta->old_file.path); } - - 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); - git_tree_free(head); - return 0; - -fail: - *counts_ptr = NULL; - git__free(counts); - *actions_ptr = NULL; - git__free(actions); - - git_iterator_free(hiter); - git_tree_free(head); - git__free(pfx); - - return -1; } -static int checkout_remove_the_old( - git_diff_list *diff, +static int checkout_create_the_new( unsigned int *actions, 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), /* here set to work dir root */ - GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_EMPTY_PARENTS | - GIT_RMDIR_REMOVE_BLOCKERS); + git_vector_foreach(&data->diff->deltas, i, delta) { + if (actions[i] & CHECKOUT_ACTION__UPDATE_BLOB) { + int error = checkout_blob(data, &delta->new_file); if (error < 0) return error; @@ -558,104 +739,104 @@ static int checkout_remove_the_old( return 0; } -static int checkout_create_the_new( - git_diff_list *diff, - unsigned int *actions, - checkout_diff_data *data) -{ - git_diff_delta *delta; - size_t i; - - git_vector_foreach(&diff->deltas, i, delta) { - if (actions[i] & CHECKOUT_ACTION__UPDATE_BLOB) { - int error = checkout_blob(data, &delta->old_file); - if (error < 0) - return error; - - data->completed_steps++; - report_progress(data, delta->old_file.path); - } - } - - return 0; -} - static int checkout_create_submodules( - git_diff_list *diff, unsigned int *actions, checkout_diff_data *data) { git_diff_delta *delta; size_t i; - git_vector_foreach(&diff->deltas, i, delta) { + git_vector_foreach(&data->diff->deltas, i, delta) { if (actions[i] & CHECKOUT_ACTION__UPDATE_SUBMODULE) { - int error = checkout_submodule(data, &delta->old_file); + int error = checkout_submodule(data, &delta->new_file); if (error < 0) return error; data->completed_steps++; - report_progress(data, delta->old_file.path); + report_progress(data, delta->new_file.path); } } return 0; } -int git_checkout_index( - git_repository *repo, - git_index *index, - git_checkout_opts *opts) +static int retrieve_symlink_caps(git_repository *repo, bool *out) { - git_diff_list *diff = NULL; - git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; - git_checkout_opts checkout_opts; + git_config *cfg; + int error, can_symlink = 0; + + if (git_repository_config__weakptr(&cfg, repo) < 0) + return -1; + + 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; + + return error; +} + +int git_checkout__from_iterators( + git_iterator *desired, + git_iterator *expected, + git_checkout_opts *opts, + const char *pathspec_pfx) +{ + 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; - int error; - assert(repo); + memset(&data, 0, sizeof(data)); - GITERR_CHECK_VERSION(opts, GIT_CHECKOUT_OPTS_VERSION, "git_checkout_opts"); - - if ((error = git_repository__ensure_not_bare(repo, "checkout")) < 0) - return error; + data.repo = git_iterator_owner(desired); + if (!data.repo) data.repo = git_iterator_owner(expected); + if (!data.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 && opts->paths.count > 0) + if (opts->paths.count > 0) diff_opts.pathspec = opts->paths; - if ((error = git_diff_index_to_workdir(&diff, repo, index, &diff_opts)) < 0) - goto cleanup; - - if ((error = git_buf_puts(&workdir, git_repository_workdir(repo))) < 0) - goto cleanup; - - normalize_options(&checkout_opts, opts); - - /* Checkout is best performed with up to four passes through the diff. - * - * 0. Figure out what actions should be taken and record for later. - * 1. Next do removes, because we iterate in alphabetical order, thus - * a new untracked directory will end up sorted *after* a blob that - * should be checked out with the same name. - * 2. Then checkout all blobs. - * 3. Then checkout all submodules in case a new .gitmodules blob was - * checked out during pass #2. + /* 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_diff__from_iterators( + &data.diff, data.repo, expected, desired, &diff_opts)) < 0) + goto cleanup; - memset(&data, 0, sizeof(data)); + 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); - data.repo = repo; - data.diff = diff; - data.opts = &checkout_opts; + /* In order to detect conflicts prior to performing any operations, + * and in order to deal with some order dependencies, checkout is best + * performed with up to four passes through the diff. + * + * 0. Figure out the actions to be taken, + * 1. Remove any files / directories as needed (because alphabetical + * iteration means that an untracked directory will end up sorted + * *after* a blob that should be checked out with the same name), + * 2. Then update all blobs, + * 3. Then update all submodules in case a new .gitmodules blob was + * checked out during pass #2. + */ if ((error = checkout_get_actions(&actions, &counts, &data)) < 0) goto cleanup; @@ -663,21 +844,23 @@ int git_checkout_index( counts[CHECKOUT_ACTION__UPDATE_BLOB] + counts[CHECKOUT_ACTION__UPDATE_SUBMODULE]; - if ((error = retrieve_symlink_caps(repo, &data.can_symlink)) < 0) + 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 */ + if (counts[CHECKOUT_ACTION__REMOVE] > 0 && - (error = checkout_remove_the_old(diff, actions, &data)) < 0) + (error = checkout_remove_the_old(actions, &data)) < 0) goto cleanup; if (counts[CHECKOUT_ACTION__UPDATE_BLOB] > 0 && - (error = checkout_create_the_new(diff, actions, &data)) < 0) + (error = checkout_create_the_new(actions, &data)) < 0) goto cleanup; if (counts[CHECKOUT_ACTION__UPDATE_SUBMODULE] > 0 && - (error = checkout_create_submodules(diff, actions, &data)) < 0) + (error = checkout_create_submodules(actions, &data)) < 0) goto cleanup; assert(data.completed_steps == data.total_steps); @@ -686,10 +869,108 @@ cleanup: if (error == GIT_EUSER) giterr_clear(); + git_diff_list_free(data.diff); + git_buf_free(&workdir); git__free(actions); git__free(counts); - git_diff_list_free(diff); - git_buf_free(&workdir); + + 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"); + + 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; + + 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); + + git__free(pfx); + git_iterator_free(index_i); + git_iterator_free(base_i); + checkout_cleanup_opts(&co_opts); return error; } @@ -699,11 +980,16 @@ int git_checkout_tree( const git_object *treeish, git_checkout_opts *opts) { - int error = 0; - git_index *index = NULL; - git_tree *tree = NULL; + int error; + git_checkout_opts co_opts; + git_tree *tree; + git_iterator *tree_i, *base_i; + char *pfx; - assert(repo && treeish); + assert(repo); + + if ((error = git_repository__ensure_not_bare(repo, "checkout tree")) < 0) + return error; if (git_object_peel((git_object **)&tree, treeish, GIT_OBJ_TREE) < 0) { giterr_set( @@ -711,17 +997,17 @@ int git_checkout_tree( return -1; } - /* TODO: create a temp index, load tree there and check it out */ + 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); - /* 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, NULL, opts); - - git_index_free(index); + git__free(pfx); + git_iterator_free(tree_i); + git_iterator_free(base_i); git_tree_free(tree); + checkout_cleanup_opts(&co_opts); return error; } @@ -731,17 +1017,30 @@ int git_checkout_head( git_checkout_opts *opts) { int error; - git_reference *head = NULL; - git_object *tree = NULL; + git_checkout_opts co_opts; + git_tree *head; + git_iterator *i1, *i2; + char *pfx; assert(repo); - 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_repository__ensure_not_bare(repo, "checkout head")) < 0) + return error; - git_reference_free(head); - git_object_free(tree); + if ((error = checkout_lookup_head_tree(&head, repo)) < 0) + return error; + + 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_tree_free(head); + checkout_cleanup_opts(&co_opts); return error; } diff --git a/src/checkout.h b/src/checkout.h new file mode 100644 index 000000000..651b0033f --- /dev/null +++ b/src/checkout.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2009-2012 the libgit2 contributors + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_checkout_h__ +#define INCLUDE_checkout_h__ + +#include "git2/checkout.h" +#include "iterator.h" + +#define GIT_CHECKOUT__FREE_BASELINE (1u << 24) + +/** + * 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. + */ +extern int git_checkout__from_iterators( + git_iterator *desired, + git_iterator *expected, + git_checkout_opts *opts, + const char *pathspec_pfx); + +#endif diff --git a/src/stash.c b/src/stash.c index 705fc75ea..0aba4dc85 100644 --- a/src/stash.c +++ b/src/stash.c @@ -500,8 +500,7 @@ static int reset_index_and_workdir( { git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; - opts.checkout_strategy = - GIT_CHECKOUT_UPDATE_MODIFIED | GIT_CHECKOUT_UPDATE_UNTRACKED; + opts.checkout_strategy = GIT_CHECKOUT_FORCE; if (remove_untracked) opts.checkout_strategy |= GIT_CHECKOUT_REMOVE_UNTRACKED; diff --git a/tests-clar/checkout/head.c b/tests-clar/checkout/head.c index 103b9999e..aed203a06 100644 --- a/tests-clar/checkout/head.c +++ b/tests-clar/checkout/head.c @@ -14,7 +14,7 @@ void test_checkout_head__cleanup(void) cl_git_sandbox_cleanup(); } -void test_checkout_head__checking_out_an_orphaned_head_returns_GIT_EORPHANEDHEAD(void) +void test_checkout_head__orphaned_head_returns_GIT_EORPHANEDHEAD(void) { make_head_orphaned(g_repo, NON_EXISTING_HEAD); diff --git a/tests-clar/checkout/index.c b/tests-clar/checkout/index.c index a67765b26..d42b69e23 100644 --- a/tests-clar/checkout/index.c +++ b/tests-clar/checkout/index.c @@ -26,7 +26,6 @@ void test_checkout_index__initialize(void) git_tree *tree; GIT_INIT_STRUCTURE(&g_opts, GIT_CHECKOUT_OPTS_VERSION); - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE; g_repo = cl_git_sandbox_init("testrepo"); @@ -78,6 +77,8 @@ 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_SAFE_CREATE; + cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); test_file_contents("./testrepo/README", "hey there\n"); @@ -93,7 +94,9 @@ 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_SAFE_CREATE | GIT_CHECKOUT_REMOVE_UNTRACKED; + cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); cl_assert_equal_i(false, git_path_isdir("./testrepo/dir")); @@ -110,6 +113,8 @@ void test_checkout_index__honor_the_specified_pathspecs(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_SAFE_CREATE; + cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); cl_assert_equal_i(false, git_path_isfile("./testrepo/README")); @@ -141,6 +146,8 @@ 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; + cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); test_file_contents("./testrepo/README", "hey there\n"); @@ -156,6 +163,8 @@ void test_checkout_index__honor_coreautocrlf_setting_set_to_true(void) cl_git_pass(p_unlink("./testrepo/.gitattributes")); set_core_autocrlf_to(true); + g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); test_file_contents("./testrepo/README", expected_readme_text); @@ -171,6 +180,8 @@ void test_checkout_index__honor_coresymlinks_setting_set_to_true(void) { set_repo_symlink_handling_cap_to(true); + g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); #ifdef GIT_WIN32 @@ -193,6 +204,8 @@ void test_checkout_index__honor_coresymlinks_setting_set_to_false(void) { set_repo_symlink_handling_cap_to(false); + g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); test_file_contents("./testrepo/link_to_new.txt", "new.txt"); @@ -205,7 +218,7 @@ void test_checkout_index__donot_overwrite_modified_file_by_default(void) /* 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; + g_opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS; cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); @@ -216,7 +229,7 @@ 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_UPDATE_MODIFIED; + g_opts.checkout_strategy = GIT_CHECKOUT_FORCE; cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); @@ -227,7 +240,9 @@ void test_checkout_index__options_disable_filters(void) { cl_git_mkfile("./testrepo/.gitattributes", "*.txt text eol=crlf\n"); + g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; g_opts.disable_filters = false; + cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); test_file_contents("./testrepo/new.txt", "my new file\r\n"); @@ -252,7 +267,9 @@ 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; + cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); cl_git_pass(p_stat("./testrepo/a", &st)); @@ -271,6 +288,7 @@ void test_checkout_index__options_override_file_modes(void) #ifndef GIT_WIN32 struct stat st; + g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; g_opts.file_mode = 0700; cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); @@ -284,32 +302,35 @@ void test_checkout_index__options_open_flags(void) { 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; - g_opts.checkout_strategy |= GIT_CHECKOUT_UPDATE_MODIFIED; + g_opts.checkout_strategy = GIT_CHECKOUT_FORCE; cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); test_file_contents("./testrepo/new.txt", "hi\nmy new file\n"); } -struct conflict_data { +struct notify_data { const char *file; const char *sha; }; -static int conflict_cb( - const char *conflict_file, +static int notify_cb( + const char *file, + unsigned int status, const git_oid *blob_oid, - unsigned int index_mode, - unsigned int wd_mode, + unsigned int checkout_mode, + unsigned int workdir_mode, void *payload) { - struct conflict_data *expectations = (struct conflict_data *)payload; + struct notify_data *expectations = (struct notify_data *)payload; - GIT_UNUSED(index_mode); - GIT_UNUSED(wd_mode); + GIT_UNUSED(checkout_mode); + GIT_UNUSED(workdir_mode); + GIT_UNUSED(status); - cl_assert_equal_s(expectations->file, conflict_file); + cl_assert_equal_s(expectations->file, file); cl_assert_equal_i(0, git_oid_streq(blob_oid, expectations->sha)); return 0; @@ -317,7 +338,7 @@ static int conflict_cb( void test_checkout_index__can_notify_of_skipped_files(void) { - struct conflict_data data; + struct notify_data data; cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!"); @@ -330,24 +351,28 @@ void test_checkout_index__can_notify_of_skipped_files(void) data.file = "new.txt"; data.sha = "a71586c1dfe8a71c6cbf6c129f404c5642ff31bd"; - g_opts.checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS; - g_opts.conflict_cb = conflict_cb; - g_opts.conflict_payload = &data; + g_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; cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); } -static int dont_conflict_cb( - const char *conflict_file, +static int dont_notify_cb( + const char *file, + unsigned int status, const git_oid *blob_oid, - unsigned int index_mode, - unsigned int wd_mode, + unsigned int checkout_mode, + unsigned int workdir_mode, void *payload) { - GIT_UNUSED(conflict_file); + GIT_UNUSED(file); + GIT_UNUSED(status); GIT_UNUSED(blob_oid); - GIT_UNUSED(index_mode); - GIT_UNUSED(wd_mode); + GIT_UNUSED(checkout_mode); + GIT_UNUSED(workdir_mode); GIT_UNUSED(payload); cl_assert(false); @@ -362,28 +387,32 @@ 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_ALLOW_CONFLICTS; - g_opts.conflict_cb = dont_conflict_cb; - g_opts.conflict_payload = NULL; + g_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; cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); } -static void progress(const char *path, size_t cur, size_t tot, void *payload) +static void checkout_progress_counter( + const char *path, size_t cur, size_t tot, void *payload) { - bool *was_called = (bool*)payload; GIT_UNUSED(path); GIT_UNUSED(cur); GIT_UNUSED(tot); - *was_called = true; + (*(int *)payload)++; } void test_checkout_index__calls_progress_callback(void) { - bool was_called = 0; - g_opts.progress_cb = progress; - g_opts.progress_payload = &was_called; + int calls = 0; + + g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + g_opts.progress_cb = checkout_progress_counter; + g_opts.progress_payload = &calls; cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); - cl_assert_equal_i(was_called, true); + cl_assert(calls > 0); } void test_checkout_index__can_overcome_name_clashes(void) @@ -400,7 +429,6 @@ void test_checkout_index__can_overcome_name_clashes(void) 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( "./testrepo/path1", NULL, GIT_RMDIR_REMOVE_FILES)); @@ -412,7 +440,8 @@ 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 = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS; + g_opts.checkout_strategy = + GIT_CHECKOUT_SAFE_CREATE | GIT_CHECKOUT_ALLOW_CONFLICTS; cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); cl_assert(git_path_isfile("./testrepo/path1")); diff --git a/tests-clar/checkout/tree.c b/tests-clar/checkout/tree.c index 79cfb6f87..ed46748ae 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"); GIT_INIT_STRUCTURE(&g_opts, GIT_CHECKOUT_OPTS_VERSION); - g_opts.checkout_strategy = GIT_CHECKOUT_SAFE; + g_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; } void test_checkout_tree__cleanup(void) diff --git a/tests-clar/checkout/typechange.c b/tests-clar/checkout/typechange.c index 98c15bcb7..85da11570 100644 --- a/tests-clar/checkout/typechange.c +++ b/tests-clar/checkout/typechange.c @@ -42,11 +42,6 @@ void test_checkout_typechange__checkout_typechanges(void) 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])); /* fprintf(stderr, "checking out '%s'\n", g_typechange_oids[i]); */ diff --git a/tests-clar/reset/hard.c b/tests-clar/reset/hard.c index 9381007db..6d2123e87 100644 --- a/tests-clar/reset/hard.c +++ b/tests-clar/reset/hard.c @@ -54,9 +54,7 @@ void test_reset_hard__resetting_reverts_modified_files(void) static const char *after[4] = { "current_file\n", "modified_file\n", - /* wrong value because reset is still slightly incorrect */ - "staged_new_file\n", - /* right value: NULL, */ + NULL, "staged_changes_modified_file\n" }; const char *wd = git_repository_workdir(repo); From 7e5c8a5b41ca660def7de23fd32b942878a6ee24 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Mon, 10 Dec 2012 15:31:43 -0800 Subject: [PATCH 08/26] More checkout improvements This flips checkout back to be driven off the changes between the baseline and the target trees. This reinstates the complex code for tracking the contents of the working directory, but overall, I think the resulting logic is easier to follow. --- include/git2/checkout.h | 60 +- src/checkout.c | 1053 ++++++++++++++++++------------ src/checkout.h | 21 +- src/diff.c | 43 +- src/fileops.c | 18 +- tests-clar/checkout/index.c | 187 +++--- tests-clar/checkout/tree.c | 7 +- tests-clar/checkout/typechange.c | 3 +- 8 files changed, 838 insertions(+), 554 deletions(-) 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)); From 5cf9875a4f6ee6fa26f5617aca8433dd49c72751 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Tue, 18 Dec 2012 15:19:24 -0800 Subject: [PATCH 09/26] Add index updating to checkout Make checkout update entries in the index for all files that are updated and/or removed, unless flag GIT_CHECKOUT_DONT_UPDATE_INDEX is given. To do this, iterators were extended to allow a little more introspection into the index being iterated over, etc. --- include/git2/checkout.h | 5 +- src/checkout.c | 186 +++++++++++++++++++++++++++---------- src/iterator.c | 27 ++++++ src/iterator.h | 8 ++ tests-clar/checkout/tree.c | 23 ++--- 5 files changed, 187 insertions(+), 62 deletions(-) diff --git a/include/git2/checkout.h b/include/git2/checkout.h index 196962bb9..884ea27f6 100644 --- a/include/git2/checkout.h +++ b/include/git2/checkout.h @@ -140,8 +140,11 @@ typedef enum { /** Only update existing files, don't create new ones */ GIT_CHECKOUT_UPDATE_ONLY = (1u << 7), + /** Normally checkout updates index entries as it goes; this stops that */ + GIT_CHECKOUT_DONT_UPDATE_INDEX = (1u << 8), + /** Don't refresh index/config/etc before doing checkout */ - GIT_CHECKOUT_NO_REFRESH = (1u << 8), + GIT_CHECKOUT_NO_REFRESH = (1u << 9), /** * THE FOLLOWING OPTIONS ARE NOT YET IMPLEMENTED diff --git a/src/checkout.c b/src/checkout.c index 5aeb0624c..a62df5efd 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -194,6 +194,7 @@ typedef struct { bool opts_free_baseline; char *pfx; git_iterator *baseline; + git_index *index; git_pool pool; git_vector removes; git_buf path; @@ -261,16 +262,20 @@ static int checkout_notify( static bool checkout_is_workdir_modified( checkout_data *data, - const git_diff_file *wditem, - const git_index_entry *baseitem) + const git_diff_file *baseitem, + const git_index_entry *wditem) { git_oid oid; - if (wditem->size != baseitem->file_size) + /* depending on where base is coming from, we may or may not know + * the actual size of the data, so we can't rely on this shortcut. + */ + if (baseitem->size && wditem->file_size != baseitem->size) return true; if (git_diff__oid_for_file( - data->repo, wditem->path, wditem->mode, wditem->size, &oid) < 0) + data->repo, wditem->path, wditem->mode, + wditem->file_size, &oid) < 0) return false; return (git_oid_cmp(&baseitem->oid, &oid) != 0); @@ -279,33 +284,6 @@ static bool checkout_is_workdir_modified( #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) -{ - if (act & CHECKOUT_ACTION__CONFLICT) - return "CONFLICT"; - - 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"; - } - 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"; -} - static int checkout_action_common( checkout_data *data, int action, @@ -372,19 +350,26 @@ static int checkout_action_wd_only( const git_index_entry *wd, git_vector *pathspec) { - bool ignored, remove; + bool remove = false; git_checkout_notify_t notify = GIT_CHECKOUT_NOTIFY_NONE; + const git_index_entry *entry; if (!git_pathspec_match_path( pathspec, wd->path, false, workdir->ignore_case)) return 0; - ignored = git_iterator_current_is_ignored(workdir); - - if (ignored) { + /* check if item is tracked in the index but not in the checkout diff */ + if (data->index != NULL && + (entry = git_index_get_bypath(data->index, wd->path, 0)) != NULL) + { + notify = GIT_CHECKOUT_NOTIFY_DIRTY; + remove = ((data->strategy & GIT_CHECKOUT_FORCE) != 0); + } + else if (git_iterator_current_is_ignored(workdir)) { notify = GIT_CHECKOUT_NOTIFY_IGNORED; remove = ((data->strategy & GIT_CHECKOUT_REMOVE_IGNORED) != 0); - } else { + } + else { notify = GIT_CHECKOUT_NOTIFY_UNTRACKED; remove = ((data->strategy & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0); } @@ -697,6 +682,7 @@ fail: } static int buffer_to_file( + struct stat *st, git_buf *buffer, const char *path, mode_t dir_mode, @@ -717,6 +703,9 @@ static int buffer_to_file( giterr_set(GITERR_OS, "Could not write to '%s'", path); (void)p_close(fd); } else { + if ((error = p_fstat(fd, st)) < 0) + giterr_set(GITERR_OS, "Error while statting '%s'", path); + if ((error = p_close(fd)) < 0) giterr_set(GITERR_OS, "Error while closing '%s'", path); } @@ -730,6 +719,7 @@ static int buffer_to_file( } static int blob_content_to_file( + struct stat *st, git_blob *blob, const char *path, mode_t entry_filemode, @@ -772,7 +762,12 @@ static int blob_content_to_file( file_mode = entry_filemode; error = buffer_to_file( - &filtered, path, opts->dir_mode, opts->file_open_flags, file_mode); + st, &filtered, path, opts->dir_mode, opts->file_open_flags, file_mode); + + if (!error) { + st->st_size = blob->odb_object->raw.len; + st->st_mode = entry_filemode; + } cleanup: git_filters_free(&filters); @@ -784,7 +779,7 @@ cleanup: } static int blob_content_to_link( - git_blob *blob, const char *path, int can_symlink) + struct stat *st, git_blob *blob, const char *path, int can_symlink) { git_buf linktarget = GIT_BUF_INIT; int error; @@ -792,28 +787,57 @@ static int blob_content_to_link( if ((error = git_blob__getbuf(&linktarget, blob)) < 0) return error; - if (can_symlink) - error = p_symlink(git_buf_cstr(&linktarget), path); - else + if (can_symlink) { + if ((error = p_symlink(git_buf_cstr(&linktarget), path)) < 0) + giterr_set(GITERR_CHECKOUT, "Could not create symlink %s\n", path); + } else { error = git_futils_fake_symlink(git_buf_cstr(&linktarget), path); + } + + if (!error) { + if ((error = p_lstat(path, st)) < 0) + giterr_set(GITERR_CHECKOUT, "Could not stat symlink %s", path); + + st->st_mode = GIT_FILEMODE_LINK; + } git_buf_free(&linktarget); return error; } +static int checkout_update_index( + checkout_data *data, + const git_diff_file *file, + struct stat *st) +{ + git_index_entry entry; + + if (!data->index) + return 0; + + memset(&entry, 0, sizeof(entry)); + entry.path = (char *)file->path; /* cast to prevent warning */ + git_index_entry__init_from_stat(&entry, st); + git_oid_cpy(&entry.oid, &file->oid); + + return git_index_add(data->index, &entry); +} + static int checkout_submodule( checkout_data *data, const git_diff_file *file) { + int error = 0; + /* Until submodules are supported, UPDATE_ONLY means do nothing here */ if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) return 0; - if (git_futils_mkdir( + if ((error = git_futils_mkdir( file->path, git_repository_workdir(data->repo), - data->opts.dir_mode, GIT_MKDIR_PATH) < 0) - return -1; + data->opts.dir_mode, GIT_MKDIR_PATH)) < 0) + return error; /* TODO: Support checkout_strategy options. Two circumstances: * 1 - submodule already checked out, but we need to move the HEAD @@ -824,7 +848,26 @@ static int checkout_submodule( * command should probably be able to. Do we need a submodule callback? */ - return 0; + /* update the index unless prevented */ + if ((data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0) { + struct stat st; + + git_buf_truncate(&data->path, data->workdir_len); + if (git_buf_puts(&data->path, file->path) < 0) + return -1; + + if ((error = p_stat(git_buf_cstr(&data->path), &st)) < 0) { + giterr_set( + GITERR_CHECKOUT, "Could not stat submodule %s\n", file->path); + return error; + } + + st.st_mode = GIT_FILEMODE_COMMIT; + + error = checkout_update_index(data, file, &st); + } + + return error; } static void report_progress( @@ -843,6 +886,7 @@ static int checkout_blob( { int error = 0; git_blob *blob; + struct stat st; git_buf_truncate(&data->path, data->workdir_len); if (git_buf_puts(&data->path, file->path) < 0) @@ -853,10 +897,10 @@ static int checkout_blob( if (S_ISLNK(file->mode)) error = blob_content_to_link( - blob, git_buf_cstr(&data->path), data->can_symlink); + &st, 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); + &st, blob, git_buf_cstr(&data->path), file->mode, &data->opts); git_blob_free(blob); @@ -871,6 +915,10 @@ static int checkout_blob( error = 0; } + /* update the index unless prevented */ + if (!error && (data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0) + error = checkout_update_index(data, file, &st); + return error; } @@ -896,6 +944,13 @@ static int checkout_remove_the_old( data->completed_steps++; report_progress(data, delta->old_file.path); + + if ((actions[i] & CHECKOUT_ACTION__UPDATE_BLOB) == 0 && + (data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0 && + data->index != NULL) + { + (void)git_index_remove(data->index, delta->old_file.path, 0); + } } } @@ -906,6 +961,12 @@ static int checkout_remove_the_old( data->completed_steps++; report_progress(data, str); + + if ((data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0 && + data->index != NULL) + { + (void)git_index_remove(data->index, str, 0); + } } return 0; @@ -926,6 +987,7 @@ static int checkout_deferred_remove(git_repository *repo, const char *path) #else GIT_UNUSED(repo); GIT_UNUSED(path); + assert(false); return 0; #endif } @@ -1021,15 +1083,19 @@ static void checkout_data_clear(checkout_data *data) data->pfx = NULL; git_buf_free(&data->path); + + git_index_free(data->index); + data->index = NULL; } static int checkout_data_init( checkout_data *data, - git_repository *repo, + git_iterator *target, git_checkout_opts *proposed) { int error = 0; git_config *cfg; + git_repository *repo = git_iterator_owner(target); memset(data, 0, sizeof(*data)); @@ -1054,8 +1120,24 @@ static int checkout_data_init( else memmove(&data->opts, proposed, sizeof(git_checkout_opts)); - /* if you are forcing, definitely allow safe updates */ + /* refresh config and index content unless NO_REFRESH is given */ + if ((data->opts.checkout_strategy & GIT_CHECKOUT_NO_REFRESH) == 0) { + if ((error = git_config_refresh(cfg)) < 0) + goto cleanup; + if (git_iterator_inner_type(target) == GIT_ITERATOR_INDEX) { + /* if we are iterating over the index, don't reload */ + data->index = git_iterator_index_get_index(target); + GIT_REFCOUNT_INC(data->index); + } else { + /* otherwise, grab and reload the index */ + if ((error = git_repository_index(&data->index, data->repo)) < 0 || + (error = git_index_read(data->index)) < 0) + goto cleanup; + } + } + + /* 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) @@ -1116,7 +1198,7 @@ int git_checkout_iterator( size_t *counts = NULL; /* initialize structures and options */ - error = checkout_data_init(&data, git_iterator_owner(target), opts); + error = checkout_data_init(&data, target, opts); if (error < 0) return error; @@ -1204,6 +1286,10 @@ cleanup: if (error == GIT_EUSER) giterr_clear(); + if (!error && data.index != NULL && + (data.strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0) + error = git_index_write(data.index); + git_diff_list_free(data.diff); git_iterator_free(workdir); git_iterator_free(data.baseline); diff --git a/src/iterator.c b/src/iterator.c index 28fccce0e..b15453400 100644 --- a/src/iterator.c +++ b/src/iterator.c @@ -988,6 +988,33 @@ fail: return -1; } +git_index *git_iterator_index_get_index(git_iterator *iter) +{ + if (iter->type == GIT_ITERATOR_SPOOLANDSORT) + iter = ((spoolandsort_iterator *)iter)->wrapped; + + if (iter->type == GIT_ITERATOR_INDEX) + return ((index_iterator *)iter)->index; + + return NULL; +} + +git_iterator_type_t git_iterator_inner_type(git_iterator *iter) +{ + if (iter->type == GIT_ITERATOR_SPOOLANDSORT) + iter = ((spoolandsort_iterator *)iter)->wrapped; + + return iter->type; +} + +git_iterator *git_iterator_spoolandsort_inner_iterator(git_iterator *iter) +{ + if (iter->type == GIT_ITERATOR_SPOOLANDSORT) + return ((spoolandsort_iterator *)iter)->wrapped; + + return NULL; +} + int git_iterator_current_tree_entry( git_iterator *iter, const git_tree_entry **tree_entry) { diff --git a/src/iterator.h b/src/iterator.h index 8bcb6fb0c..ccdab4d94 100644 --- a/src/iterator.h +++ b/src/iterator.h @@ -193,4 +193,12 @@ extern int git_iterator_cmp( extern int git_iterator_current_workdir_path( git_iterator *iter, git_buf **path); + +extern git_index *git_iterator_index_get_index(git_iterator *iter); + +extern git_iterator_type_t git_iterator_inner_type(git_iterator *iter); + +extern git_iterator *git_iterator_spoolandsort_inner_iterator( + git_iterator *iter); + #endif diff --git a/tests-clar/checkout/tree.c b/tests-clar/checkout/tree.c index ab502dd30..6e7175a87 100644 --- a/tests-clar/checkout/tree.c +++ b/tests-clar/checkout/tree.c @@ -50,28 +50,29 @@ void test_checkout_tree__can_checkout_a_subdirectory_from_a_commit(void) void test_checkout_tree__can_checkout_and_remove_directory(void) { - git_reference *head; cl_assert_equal_i(false, git_path_isdir("./testrepo/ab/")); - // Checkout brach "subtrees" and update HEAD, so that HEAD matches the current working tree + /* Checkout brach "subtrees" and update HEAD, so that HEAD matches the + * current working tree + */ cl_git_pass(git_revparse_single(&g_object, g_repo, "subtrees")); cl_git_pass(git_checkout_tree(g_repo, g_object, &g_opts)); - cl_git_pass(git_reference_lookup(&head, g_repo, "HEAD")); - cl_git_pass(git_reference_symbolic_set_target(head, "refs/heads/subtrees")); - git_reference_free(head); - + + cl_git_pass(git_repository_set_head(g_repo, "refs/heads/subtrees")); + cl_assert_equal_i(true, git_path_isdir("./testrepo/ab/")); cl_assert_equal_i(true, git_path_isfile("./testrepo/ab/de/2.txt")); cl_assert_equal_i(true, git_path_isfile("./testrepo/ab/de/fgh/1.txt")); - // Checkout brach "master" and update HEAD, so that HEAD matches the current working tree + /* Checkout brach "master" and update HEAD, so that HEAD matches the + * current working tree + */ cl_git_pass(git_revparse_single(&g_object, g_repo, "master")); cl_git_pass(git_checkout_tree(g_repo, g_object, &g_opts)); - cl_git_pass(git_reference_lookup(&head, g_repo, "HEAD")); - cl_git_pass(git_reference_symbolic_set_target(head, "refs/heads/master")); - git_reference_free(head); - // This directory should no longer exist + cl_git_pass(git_repository_set_head(g_repo, "refs/heads/master")); + + /* This directory should no longer exist */ cl_assert_equal_i(false, git_path_isdir("./testrepo/ab/")); } From 8fe713ccf7bf8c6330fdda7f0c733e7f3ab29d3f Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Wed, 19 Dec 2012 15:06:14 -0800 Subject: [PATCH 10/26] Make git_oid_tostr use out buffer for NULL oid Previously a NULL oid was handled like an empty buffer and returned a status empty string. This makes git_oid_tostr() set the output buffer to the empty string instead. --- src/oid.c | 4 ++-- tests-clar/object/raw/convert.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/oid.c b/src/oid.c index 1bf74b963..bbdd8541b 100644 --- a/src/oid.c +++ b/src/oid.c @@ -95,12 +95,12 @@ char *git_oid_tostr(char *out, size_t n, const git_oid *oid) { char str[GIT_OID_HEXSZ]; - if (!out || n == 0 || !oid) + if (!out || n == 0) return ""; n--; /* allow room for terminating NUL */ - if (n > 0) { + if (n > 0 && oid != NULL) { git_oid_fmt(str, oid); if (n > GIT_OID_HEXSZ) n = GIT_OID_HEXSZ; diff --git a/tests-clar/object/raw/convert.c b/tests-clar/object/raw/convert.c index 7f310ddf0..74442c153 100644 --- a/tests-clar/object/raw/convert.c +++ b/tests-clar/object/raw/convert.c @@ -21,9 +21,9 @@ void test_object_raw_convert__succeed_on_oid_to_string_conversion(void) str = git_oid_tostr(out, 0, &in); cl_assert(str && *str == '\0' && str != out); - /* NULL oid pointer, returns static empty string */ + /* NULL oid pointer, sets existing buffer to empty string */ str = git_oid_tostr(out, sizeof(out), NULL); - cl_assert(str && *str == '\0' && str != out); + cl_assert(str && *str == '\0' && str == out); /* n == 1, returns out as an empty string */ str = git_oid_tostr(out, 1, &in); From a6a82e1a59065f6d5eaf4748708c92326048a99f Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Wed, 19 Dec 2012 15:06:40 -0800 Subject: [PATCH 11/26] Improve error propagation in stash Stash was sometimes obscuring the actual error code, replacing it with a -1 when there was more descriptive value. This updates stash to preserve the original error code more reliably along with a variety of other error handling tweaks. I believe this is an improvement, but arguably, preserving the underlying error code may result in values that are harder to interpret by the caller who does not understand the internals. Discussion is welcome! --- src/stash.c | 218 ++++++++++++++++++------------------- tests-clar/stash/drop.c | 7 ++ tests-clar/stash/foreach.c | 8 +- 3 files changed, 119 insertions(+), 114 deletions(-) diff --git a/src/stash.c b/src/stash.c index 0aba4dc85..e67f7038c 100644 --- a/src/stash.c +++ b/src/stash.c @@ -22,15 +22,6 @@ static int create_error(int error, const char *msg) return error; } -static int ensure_non_bare_repository(git_repository *repo) -{ - if (!git_repository_is_bare(repo)) - return 0; - - return create_error(GIT_EBAREREPO, - "Stash related operations require a working directory."); -} - static int retrieve_head(git_reference **out, git_repository *repo) { int error = git_repository_head(out, repo); @@ -89,23 +80,22 @@ static int retrieve_base_commit_and_message( if ((error = retrieve_head(&head, repo)) < 0) return error; - error = -1; - if (strcmp("HEAD", git_reference_name(head)) == 0) - git_buf_puts(stash_message, "(no branch): "); + error = git_buf_puts(stash_message, "(no branch): "); else - git_buf_printf( + error = git_buf_printf( stash_message, "%s: ", git_reference_name(head) + strlen(GIT_REFS_HEADS_DIR)); - - if (git_commit_lookup(b_commit, repo, git_reference_target(head)) < 0) + if (error < 0) goto cleanup; - if (append_commit_description(stash_message, *b_commit) < 0) + if ((error = git_commit_lookup( + b_commit, repo, git_reference_target(head))) < 0) goto cleanup; - error = 0; + if ((error = append_commit_description(stash_message, *b_commit)) < 0) + goto cleanup; cleanup: git_reference_free(head); @@ -114,9 +104,10 @@ cleanup: static int build_tree_from_index(git_tree **out, git_index *index) { + int error; git_oid i_tree_oid; - if (git_index_write_tree(&i_tree_oid, index) < 0) + if ((error = git_index_write_tree(&i_tree_oid, index)) < 0) return -1; return git_tree_lookup(out, git_index_owner(index), &i_tree_oid); @@ -132,15 +123,15 @@ static int commit_index( git_tree *i_tree = NULL; git_oid i_commit_oid; git_buf msg = GIT_BUF_INIT; - int error = -1; + int error; - if (build_tree_from_index(&i_tree, index) < 0) + if ((error = build_tree_from_index(&i_tree, index)) < 0) goto cleanup; - if (git_buf_printf(&msg, "index on %s\n", message) < 0) + if ((error = git_buf_printf(&msg, "index on %s\n", message)) < 0) goto cleanup; - if (git_commit_create( + if ((error = git_commit_create( &i_commit_oid, git_index_owner(index), NULL, @@ -150,8 +141,8 @@ static int commit_index( git_buf_cstr(&msg), i_tree, 1, - &parent) < 0) - goto cleanup; + &parent)) < 0) + goto cleanup; error = git_commit_lookup(i_commit, git_index_owner(index), &i_commit_oid); @@ -164,6 +155,8 @@ cleanup: struct cb_data { git_index *index; + int error; + bool include_changed; bool include_untracked; bool include_ignored; @@ -174,52 +167,50 @@ static int update_index_cb( float progress, void *payload) { - int pos; struct cb_data *data = (struct cb_data *)payload; + const char *add_path = NULL; GIT_UNUSED(progress); switch (delta->status) { case GIT_DELTA_IGNORED: - if (!data->include_ignored) - break; - - return git_index_add_from_workdir(data->index, delta->new_file.path); + if (data->include_ignored) + add_path = delta->new_file.path; + break; case GIT_DELTA_UNTRACKED: - if (!data->include_untracked) - break; - - return git_index_add_from_workdir(data->index, delta->new_file.path); + if (data->include_untracked) + add_path = delta->new_file.path; + break; case GIT_DELTA_ADDED: - /* Fall through */ case GIT_DELTA_MODIFIED: - if (!data->include_changed) - break; - - return git_index_add_from_workdir(data->index, delta->new_file.path); + if (data->include_changed) + add_path = delta->new_file.path; + break; case GIT_DELTA_DELETED: if (!data->include_changed) break; - - if ((pos = git_index_find(data->index, delta->new_file.path)) < 0) - return -1; - - if (git_index_remove(data->index, delta->new_file.path, 0) < 0) - return -1; + if (git_index_find(data->index, delta->old_file.path) == 0) + data->error = git_index_remove( + data->index, delta->old_file.path, 0); + break; default: /* Unimplemented */ giterr_set( GITERR_INVALID, - "Cannot update index. Unimplemented status kind (%d)", + "Cannot update index. Unimplemented status (%d)", delta->status); - return -1; + data->error = -1; + break; } - return 0; + if (add_path != NULL) + data->error = git_index_add_from_workdir(data->index, add_path); + + return data->error; } static int build_untracked_tree( @@ -232,14 +223,15 @@ static int build_untracked_tree( git_diff_list *diff = NULL; git_diff_options opts = GIT_DIFF_OPTIONS_INIT; struct cb_data data = {0}; - int error = -1; + int error; git_index_clear(index); data.index = index; if (flags & GIT_STASH_INCLUDE_UNTRACKED) { - opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED | GIT_DIFF_RECURSE_UNTRACKED_DIRS; + opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED | + GIT_DIFF_RECURSE_UNTRACKED_DIRS; data.include_untracked = true; } @@ -248,19 +240,22 @@ static int build_untracked_tree( data.include_ignored = true; } - if (git_commit_tree(&i_tree, i_commit) < 0) + if ((error = git_commit_tree(&i_tree, i_commit)) < 0) goto cleanup; - if (git_diff_tree_to_workdir(&diff, git_index_owner(index), i_tree, &opts) < 0) + if ((error = git_diff_tree_to_workdir( + &diff, git_index_owner(index), i_tree, &opts)) < 0) goto cleanup; - if (git_diff_foreach(diff, update_index_cb, NULL, NULL, &data) < 0) + if ((error = git_diff_foreach( + diff, update_index_cb, NULL, NULL, &data)) < 0) + { + if (error == GIT_EUSER) + error = data.error; goto cleanup; + } - if (build_tree_from_index(tree_out, index) < 0) - goto cleanup; - - error = 0; + error = build_tree_from_index(tree_out, index); cleanup: git_diff_list_free(diff); @@ -279,15 +274,15 @@ static int commit_untracked( git_tree *u_tree = NULL; git_oid u_commit_oid; git_buf msg = GIT_BUF_INIT; - int error = -1; + int error; - if (build_untracked_tree(&u_tree, index, i_commit, flags) < 0) + if ((error = build_untracked_tree(&u_tree, index, i_commit, flags)) < 0) goto cleanup; - if (git_buf_printf(&msg, "untracked files on %s\n", message) < 0) + if ((error = git_buf_printf(&msg, "untracked files on %s\n", message)) < 0) goto cleanup; - if (git_commit_create( + if ((error = git_commit_create( &u_commit_oid, git_index_owner(index), NULL, @@ -297,8 +292,8 @@ static int commit_untracked( git_buf_cstr(&msg), u_tree, 0, - NULL) < 0) - goto cleanup; + NULL)) < 0) + goto cleanup; error = git_commit_lookup(u_commit, git_index_owner(index), &u_commit_oid); @@ -318,35 +313,40 @@ static int build_workdir_tree( git_diff_list *diff = NULL, *diff2 = NULL; git_diff_options opts = GIT_DIFF_OPTIONS_INIT; struct cb_data data = {0}; - int error = -1; + int error; - if (git_commit_tree(&b_tree, b_commit) < 0) + if ((error = git_commit_tree(&b_tree, b_commit)) < 0) goto cleanup; - if (git_diff_tree_to_index(&diff, repo, b_tree, NULL, &opts) < 0) + if ((error = git_diff_tree_to_index(&diff, repo, b_tree, NULL, &opts)) < 0) goto cleanup; - if (git_diff_index_to_workdir(&diff2, repo, NULL, &opts) < 0) + if ((error = git_diff_index_to_workdir(&diff2, repo, NULL, &opts)) < 0) goto cleanup; - if (git_diff_merge(diff, diff2) < 0) + if ((error = git_diff_merge(diff, diff2)) < 0) goto cleanup; data.index = index; data.include_changed = true; - if (git_diff_foreach(diff, update_index_cb, NULL, NULL, &data) < 0) + if ((error = git_diff_foreach( + diff, update_index_cb, NULL, NULL, &data)) < 0) + { + if (error == GIT_EUSER) + error = data.error; goto cleanup; + } - if (build_tree_from_index(tree_out, index) < 0) + + if ((error = build_tree_from_index(tree_out, index)) < 0) goto cleanup; - error = 0; - cleanup: git_diff_list_free(diff); git_diff_list_free(diff2); git_tree_free(b_tree); + return error; } @@ -359,25 +359,24 @@ static int commit_worktree( git_commit *b_commit, git_commit *u_commit) { + int error = 0; git_tree *w_tree = NULL, *i_tree = NULL; - int error = -1; - const git_commit *parents[] = { NULL, NULL, NULL }; parents[0] = b_commit; parents[1] = i_commit; parents[2] = u_commit; - if (git_commit_tree(&i_tree, i_commit) < 0) - return -1; - - if (git_index_read_tree(index, i_tree) < 0) + if ((error = git_commit_tree(&i_tree, i_commit)) < 0) goto cleanup; - if (build_workdir_tree(&w_tree, index, b_commit) < 0) + if ((error = git_index_read_tree(index, i_tree)) < 0) goto cleanup; - if (git_commit_create( + if ((error = build_workdir_tree(&w_tree, index, b_commit)) < 0) + goto cleanup; + + error = git_commit_create( w_commit_oid, git_index_owner(index), NULL, @@ -386,10 +385,8 @@ static int commit_worktree( NULL, message, w_tree, - u_commit ? 3 : 2, parents) < 0) - goto cleanup; - - error = 0; + u_commit ? 3 : 2, + parents); cleanup: git_tree_free(i_tree); @@ -402,9 +399,11 @@ static int prepare_worktree_commit_message( const char *user_message) { git_buf buf = GIT_BUF_INIT; - int error = -1; + int error; + + if (git_buf_set(&buf, git_buf_cstr(msg), git_buf_len(msg)) < 0) + return -1; - git_buf_set(&buf, git_buf_cstr(msg), git_buf_len(msg)); git_buf_clear(msg); if (!user_message) @@ -424,6 +423,7 @@ static int prepare_worktree_commit_message( cleanup: git_buf_free(&buf); + return error; } @@ -449,8 +449,6 @@ static int update_reflog( if ((error = git_reflog_write(reflog)) < 0) goto cleanup; - error = 0; - cleanup: git_reference_free(stash); git_reflog_free(reflog); @@ -522,7 +520,7 @@ int git_stash_save( assert(out && repo && stasher); - if ((error = ensure_non_bare_repository(repo)) < 0) + if ((error = git_repository__ensure_not_bare(repo, "stash save")) < 0) return error; if ((error = retrieve_base_commit_and_message(&b_commit, &msg, repo)) < 0) @@ -530,47 +528,50 @@ int git_stash_save( if ((error = ensure_there_are_changes_to_stash( repo, - (flags & GIT_STASH_INCLUDE_UNTRACKED) == GIT_STASH_INCLUDE_UNTRACKED, - (flags & GIT_STASH_INCLUDE_IGNORED) == GIT_STASH_INCLUDE_IGNORED)) < 0) + (flags & GIT_STASH_INCLUDE_UNTRACKED) != 0, + (flags & GIT_STASH_INCLUDE_IGNORED) != 0)) < 0) goto cleanup; - error = -1; - - if (git_repository_index(&index, repo) < 0) + if ((error = git_repository_index(&index, repo)) < 0) goto cleanup; - if (commit_index(&i_commit, index, stasher, git_buf_cstr(&msg), b_commit) < 0) + if ((error = commit_index( + &i_commit, index, stasher, git_buf_cstr(&msg), b_commit)) < 0) goto cleanup; - if ((flags & GIT_STASH_INCLUDE_UNTRACKED || flags & GIT_STASH_INCLUDE_IGNORED) - && commit_untracked(&u_commit, index, stasher, git_buf_cstr(&msg), i_commit, flags) < 0) + if ((flags & (GIT_STASH_INCLUDE_UNTRACKED | GIT_STASH_INCLUDE_IGNORED)) && + (error = commit_untracked( + &u_commit, index, stasher, git_buf_cstr(&msg), + i_commit, flags)) < 0) goto cleanup; - if (prepare_worktree_commit_message(&msg, message) < 0) + if ((error = prepare_worktree_commit_message(&msg, message)) < 0) goto cleanup; - if (commit_worktree(out, index, stasher, git_buf_cstr(&msg), i_commit, b_commit, u_commit) < 0) + if ((error = commit_worktree( + out, index, stasher, git_buf_cstr(&msg), + i_commit, b_commit, u_commit)) < 0) goto cleanup; git_buf_rtrim(&msg); - if (update_reflog(out, repo, stasher, git_buf_cstr(&msg)) < 0) + + if ((error = update_reflog(out, repo, stasher, git_buf_cstr(&msg))) < 0) goto cleanup; - if (reset_index_and_workdir( + if ((error = reset_index_and_workdir( repo, - ((flags & GIT_STASH_KEEP_INDEX) == GIT_STASH_KEEP_INDEX) ? - i_commit : b_commit, - (flags & GIT_STASH_INCLUDE_UNTRACKED) == GIT_STASH_INCLUDE_UNTRACKED) < 0) + ((flags & GIT_STASH_KEEP_INDEX) != 0) ? i_commit : b_commit, + (flags & GIT_STASH_INCLUDE_UNTRACKED) != 0)) < 0) goto cleanup; - error = 0; - cleanup: + git_buf_free(&msg); git_commit_free(i_commit); git_commit_free(b_commit); git_commit_free(u_commit); git_index_free(index); + return error; } @@ -588,7 +589,6 @@ int git_stash_foreach( error = git_reference_lookup(&stash, repo, GIT_REFS_STASH_FILE); if (error == GIT_ENOTFOUND) return 0; - if (error < 0) goto cleanup; @@ -598,18 +598,16 @@ int git_stash_foreach( max = git_reflog_entrycount(reflog); for (i = 0; i < max; i++) { entry = git_reflog_entry_byindex(reflog, i); - + if (callback(i, git_reflog_entry_message(entry), git_reflog_entry_id_new(entry), payload)) { error = GIT_EUSER; - goto cleanup; + break; } } - error = 0; - cleanup: git_reference_free(stash); git_reflog_free(reflog); diff --git a/tests-clar/stash/drop.c b/tests-clar/stash/drop.c index c146e90ec..2af95c737 100644 --- a/tests-clar/stash/drop.c +++ b/tests-clar/stash/drop.c @@ -36,15 +36,22 @@ static void push_three_states(void) cl_git_pass(git_repository_index(&index, repo)); cl_git_pass(git_index_add_from_workdir(index, "zero.txt")); commit_staged_files(&oid, index, signature); + cl_assert(git_path_exists("stash/zero.txt")); cl_git_mkfile("stash/one.txt", "content\n"); cl_git_pass(git_stash_save(&oid, repo, signature, "First", GIT_STASH_INCLUDE_UNTRACKED)); + cl_assert(!git_path_exists("stash/one.txt")); + cl_assert(git_path_exists("stash/zero.txt")); cl_git_mkfile("stash/two.txt", "content\n"); cl_git_pass(git_stash_save(&oid, repo, signature, "Second", GIT_STASH_INCLUDE_UNTRACKED)); + cl_assert(!git_path_exists("stash/two.txt")); + cl_assert(git_path_exists("stash/zero.txt")); cl_git_mkfile("stash/three.txt", "content\n"); cl_git_pass(git_stash_save(&oid, repo, signature, "Third", GIT_STASH_INCLUDE_UNTRACKED)); + cl_assert(!git_path_exists("stash/three.txt")); + cl_assert(git_path_exists("stash/zero.txt")); git_index_free(index); } diff --git a/tests-clar/stash/foreach.c b/tests-clar/stash/foreach.c index c7d59a3a1..f1983625f 100644 --- a/tests-clar/stash/foreach.c +++ b/tests-clar/stash/foreach.c @@ -38,10 +38,10 @@ void test_stash_foreach__cleanup(void) } static int callback_cb( - size_t index, - const char* message, - const git_oid *stash_oid, - void *payload) + size_t index, + const char* message, + const git_oid *stash_oid, + void *payload) { struct callback_data *data = (struct callback_data *)payload; From 6f58332f3ab04f910103b945348b6a0a314c1793 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Thu, 20 Dec 2012 16:15:02 -0800 Subject: [PATCH 12/26] Fix use of uninitialized variable --- src/stash.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stash.c b/src/stash.c index e67f7038c..dbd626a60 100644 --- a/src/stash.c +++ b/src/stash.c @@ -401,8 +401,8 @@ static int prepare_worktree_commit_message( git_buf buf = GIT_BUF_INIT; int error; - if (git_buf_set(&buf, git_buf_cstr(msg), git_buf_len(msg)) < 0) - return -1; + if ((error = git_buf_set(&buf, git_buf_cstr(msg), git_buf_len(msg))) < 0) + return error; git_buf_clear(msg); @@ -419,7 +419,7 @@ static int prepare_worktree_commit_message( git_buf_printf(msg, ": %s\n", user_message); } - error = git_buf_oom(msg) || git_buf_oom(&buf) ? -1 : 0; + error = (git_buf_oom(msg) || git_buf_oom(&buf)) ? -1 : 0; cleanup: git_buf_free(&buf); From a9a730075eee70444db4ab4bbb928c29a812bbbf Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Thu, 20 Dec 2012 16:16:22 -0800 Subject: [PATCH 13/26] Submodule caching fix and location API This adds a new API to the submodule interface that just returns where information about the submodule was found (e.g. config file only or in the HEAD, index, or working directory). Also, the old "refresh" call was potentially keeping some stale submodule data around, so this simplfies that code and literally discards the old cache, then reallocates. --- include/git2/submodule.h | 18 ++++++++++++++++++ src/submodule.c | 27 +++++++++++++++++++++------ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/include/git2/submodule.h b/include/git2/submodule.h index 90e45cebd..444b3a967 100644 --- a/include/git2/submodule.h +++ b/include/git2/submodule.h @@ -504,6 +504,24 @@ GIT_EXTERN(int) git_submodule_status( unsigned int *status, git_submodule *submodule); +/** + * Get the locations of submodule information. + * + * This is a bit like a very lightweight version of `git_submodule_status`. + * It just returns a made of the first four submodule status values (i.e. + * the ones like GIT_SUBMODULE_STATUS_IN_HEAD, etc) that tell you where the + * submodule data comes from (i.e. the HEAD commit, gitmodules file, etc.). + * This can be useful if you want to know if the submodule is present in the + * working directory at this point in time, etc. + * + * @param status Combination of first four `GIT_SUBMODULE_STATUS` flags + * @param submodule Submodule for which to get status + * @return 0 on success, <0 on error + */ +GIT_EXTERN(int) git_submodule_location( + unsigned int *location_status, + git_submodule *submodule); + /** @} */ GIT_END_DECL #endif diff --git a/src/submodule.c b/src/submodule.c index 94b4f724e..9ed6707c7 100644 --- a/src/submodule.c +++ b/src/submodule.c @@ -66,7 +66,7 @@ __KHASH_IMPL( str, static kh_inline, const char *, void *, 1, str_hash_no_trailing_slash, str_equal_no_trailing_slash); -static int load_submodule_config(git_repository *repo, bool force); +static int load_submodule_config(git_repository *repo); static git_config_backend *open_gitmodules(git_repository *, bool, const git_oid *); static int lookup_head_remote(git_buf *url, git_repository *repo); static int submodule_get(git_submodule **, git_repository *, const char *, const char *); @@ -106,7 +106,7 @@ int git_submodule_lookup( assert(repo && name); - if ((error = load_submodule_config(repo, false)) < 0) + if ((error = load_submodule_config(repo)) < 0) return error; pos = git_strmap_lookup_index(repo->submodules, name); @@ -148,7 +148,7 @@ int git_submodule_foreach( assert(repo && callback); - if ((error = load_submodule_config(repo, false)) < 0) + if ((error = load_submodule_config(repo)) < 0) return error; git_strmap_foreach_value(repo->submodules, sm, { @@ -708,7 +708,8 @@ int git_submodule_open( int git_submodule_reload_all(git_repository *repo) { assert(repo); - return load_submodule_config(repo, true); + git_submodule_config_free(repo); + return load_submodule_config(repo); } int git_submodule_reload(git_submodule *submodule) @@ -829,6 +830,20 @@ int git_submodule_status( return error; } +int git_submodule_location( + unsigned int *location_status, + git_submodule *submodule) +{ + assert(location_status && submodule); + + *location_status = submodule->flags & + (GIT_SUBMODULE_STATUS_IN_HEAD | GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS_IN_CONFIG | GIT_SUBMODULE_STATUS_IN_WD); + + return 0; +} + + /* * INTERNAL FUNCTIONS */ @@ -1225,14 +1240,14 @@ static git_config_backend *open_gitmodules( return mods; } -static int load_submodule_config(git_repository *repo, bool force) +static int load_submodule_config(git_repository *repo) { int error; git_oid gitmodules_oid; git_buf path = GIT_BUF_INIT; git_config_backend *mods = NULL; - if (repo->submodules && !force) + if (repo->submodules) return 0; memset(&gitmodules_oid, 0, sizeof(gitmodules_oid)); From 546d65a8dae8a7af7288163a580c08c827ebda1d Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Wed, 2 Jan 2013 17:01:34 -0800 Subject: [PATCH 14/26] Fix up spoolandsort iterator usage The spoolandsort iterator changes got sort-of cherry picked out of this branch and so I dropped the commit when rebasing; however, there were a few small changes that got dropped as well (since the version merged upstream wasn't quite the same as what I dropped). --- src/checkout.c | 3 +-- src/diff.c | 3 +-- src/iterator.c | 25 +++++++++---------------- src/iterator.h | 3 --- 4 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/checkout.c b/src/checkout.c index a62df5efd..76119c6e9 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -1223,8 +1223,7 @@ int git_checkout_iterator( /* 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) + if ((error = git_iterator_spoolandsort_push(baseline, true)) < 0) goto cleanup; } diff --git a/src/diff.c b/src/diff.c index 042cdf451..82a816465 100644 --- a/src/diff.c +++ b/src/diff.c @@ -589,8 +589,7 @@ int git_diff__from_iterators( *diff_ptr = NULL; - if (!diff || - diff_list_init_from_iterators(diff, old_iter, new_iter) < 0) + if (!diff || diff_list_init_from_iterators(diff, old_iter, new_iter) < 0) goto fail; if (diff->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) { diff --git a/src/iterator.c b/src/iterator.c index b15453400..cf88efffd 100644 --- a/src/iterator.c +++ b/src/iterator.c @@ -30,8 +30,8 @@ (P)->base.start = start ? git__strdup(start) : NULL; \ (P)->base.end = end ? git__strdup(end) : NULL; \ (P)->base.ignore_case = false; \ - if ((start && !(P)->base.start) || (end && !(P)->base.end)) \ - return -1; \ + if ((start && !(P)->base.start) || (end && !(P)->base.end)) { \ + git__free(P); return -1; } \ } while (0) static int iterator__reset_range( @@ -990,31 +990,24 @@ fail: git_index *git_iterator_index_get_index(git_iterator *iter) { - if (iter->type == GIT_ITERATOR_SPOOLANDSORT) - iter = ((spoolandsort_iterator *)iter)->wrapped; - if (iter->type == GIT_ITERATOR_INDEX) return ((index_iterator *)iter)->index; + if (iter->type == GIT_ITERATOR_SPOOLANDSORT && + ((spoolandsort_callbacks *)iter->cb)->orig_type == GIT_ITERATOR_INDEX) + return ((index_iterator *)iter)->index; + return NULL; } git_iterator_type_t git_iterator_inner_type(git_iterator *iter) { if (iter->type == GIT_ITERATOR_SPOOLANDSORT) - iter = ((spoolandsort_iterator *)iter)->wrapped; + return ((spoolandsort_callbacks *)iter->cb)->orig_type; return iter->type; } -git_iterator *git_iterator_spoolandsort_inner_iterator(git_iterator *iter) -{ - if (iter->type == GIT_ITERATOR_SPOOLANDSORT) - return ((spoolandsort_iterator *)iter)->wrapped; - - return NULL; -} - int git_iterator_current_tree_entry( git_iterator *iter, const git_tree_entry **tree_entry) { @@ -1085,8 +1078,8 @@ int git_iterator_advance_into_directory( if (iter->type == GIT_ITERATOR_WORKDIR && wi->entry.path && - S_ISDIR(wi->entry.mode) && - !S_ISGITLINK(wi->entry.mode)) + (wi->entry.mode == GIT_FILEMODE_TREE || + wi->entry.mode == GIT_FILEMODE_COMMIT)) { if (workdir_iterator__expand_dir(wi) < 0) /* if error loading or if empty, skip the directory. */ diff --git a/src/iterator.h b/src/iterator.h index ccdab4d94..c0e35605c 100644 --- a/src/iterator.h +++ b/src/iterator.h @@ -198,7 +198,4 @@ extern git_index *git_iterator_index_get_index(git_iterator *iter); extern git_iterator_type_t git_iterator_inner_type(git_iterator *iter); -extern git_iterator *git_iterator_spoolandsort_inner_iterator( - git_iterator *iter); - #endif From 16a666d3d421fcbffabc861646b02e22c63d9ad9 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Wed, 2 Jan 2013 17:05:54 -0800 Subject: [PATCH 15/26] Fix workdir notifications and removals The notifications were broken from the various iterations over this code and were not returning working dir item data correctly. Also, workdir items that were alphabetically after the last item in diff were not being processed. --- src/checkout.c | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/checkout.c b/src/checkout.c index 76119c6e9..962250e4e 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -209,9 +209,9 @@ static int checkout_notify( checkout_data *data, git_checkout_notify_t why, const git_diff_delta *delta, - const git_index_entry *baseitem) + const git_index_entry *wditem) { - git_diff_file basefile; + git_diff_file wdfile; const git_diff_file *baseline = NULL, *target = NULL, *workdir = NULL; if (!data->opts.notify_cb) @@ -220,44 +220,41 @@ static int checkout_notify( if ((why & data->opts.notify_flags) == 0) return 0; - if (baseitem) { - memset(&basefile, 0, sizeof(basefile)); + if (wditem) { + memset(&wdfile, 0, sizeof(wdfile)); - 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; + git_oid_cpy(&wdfile.oid, &wditem->oid); + wdfile.path = wditem->path; + wdfile.size = wditem->file_size; + wdfile.flags = GIT_DIFF_FILE_VALID_OID; + wdfile.mode = wditem->mode; - baseline = &basefile; + workdir = &wdfile; } - if ((why & GIT_CHECKOUT__NOTIFY_CONFLICT_TREE) != 0) { - /* baseitem is a blob that conflicts with a tree in the workdir */ - } else { + if (delta) { switch (delta->status) { case GIT_DELTA_UNMODIFIED: case GIT_DELTA_MODIFIED: case GIT_DELTA_TYPECHANGE: default: - target = &delta->old_file; - workdir = &delta->new_file; + baseline = &delta->old_file; + target = &delta->new_file; break; case GIT_DELTA_ADDED: case GIT_DELTA_IGNORED: case GIT_DELTA_UNTRACKED: - workdir = &delta->new_file; + target = &delta->new_file; break; case GIT_DELTA_DELETED: - target = &delta->old_file; + baseline = &delta->old_file; break; } } return data->opts.notify_cb( - why, delta->old_file.path, - baseline, target, workdir, - data->opts.notify_payload); + why, delta ? delta->old_file.path : wditem->path, + baseline, target, workdir, data->opts.notify_payload); } static bool checkout_is_workdir_modified( @@ -653,6 +650,14 @@ static int checkout_get_actions( counts[CHECKOUT_ACTION__CONFLICT]++; } + while (wditem != NULL) { + error = checkout_action_wd_only(data, workdir, wditem, &pathspec); + if (!error) + error = git_iterator_advance(workdir, &wditem); + if (error < 0) + goto fail; + } + counts[CHECKOUT_ACTION__REMOVE] += data->removes.length; if (counts[CHECKOUT_ACTION__CONFLICT] > 0 && From e0548c0ea4fba4a66e73ba326b463fd754cc6e52 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Wed, 2 Jan 2013 17:09:07 -0800 Subject: [PATCH 16/26] Fix some submodule and typechange checkout cases There were a bunch of small bugs in the checkout code where I was assuming that a typechange was always from a tree to a blob or vice versa. This fixes up most of those cases. Also, there were circumstances where the submodule definitions were changed by the checkout and the submodule data was not getting reloaded properly before the new submodules were checked out. --- src/checkout.c | 133 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 109 insertions(+), 24 deletions(-) diff --git a/src/checkout.c b/src/checkout.c index 962250e4e..a26f007b1 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -15,6 +15,7 @@ #include "git2/blob.h" #include "git2/config.h" #include "git2/diff.h" +#include "git2/submodule.h" #include "refs.h" #include "repository.h" @@ -201,6 +202,7 @@ typedef struct { size_t workdir_len; unsigned int strategy; int can_symlink; + bool reload_submodules; size_t total_steps; size_t completed_steps; } checkout_data; @@ -264,6 +266,26 @@ static bool checkout_is_workdir_modified( { git_oid oid; + /* handle "modified" submodule */ + if (wditem->mode == GIT_FILEMODE_COMMIT) { + git_submodule *sm; + unsigned int sm_status = 0; + const git_oid *sm_oid = NULL; + + if (git_submodule_lookup(&sm, data->repo, wditem->path) < 0 || + git_submodule_status(&sm_status, sm) < 0) + return true; + + if (GIT_SUBMODULE_STATUS_IS_WD_DIRTY(sm_status)) + return true; + + sm_oid = git_submodule_wd_id(sm); + if (!sm_oid) + return false; + + return (git_oid_cmp(&baseitem->oid, sm_oid) != 0); + } + /* depending on where base is coming from, we may or may not know * the actual size of the data, so we can't rely on this shortcut. */ @@ -385,6 +407,21 @@ static int checkout_action_wd_only( return 0; } +static bool submodule_is_config_only( + checkout_data *data, + const char *path) +{ + git_submodule *sm = NULL; + unsigned int sm_loc = 0; + + if (git_submodule_lookup(&sm, data->repo, path) < 0 || + git_submodule_location(&sm_loc, sm) < 0 || + sm_loc == GIT_SUBMODULE_STATUS_IN_CONFIG) + return true; + + return false; +} + static int checkout_action_with_wd( checkout_data *data, const git_diff_delta *delta, @@ -394,9 +431,7 @@ static int checkout_action_with_wd( 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_is_workdir_modified(data, &delta->old_file, wd)) { if (checkout_notify( data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, wd)) return GIT_EUSER; @@ -419,12 +454,31 @@ static int checkout_action_with_wd( 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); + if (delta->old_file.mode == GIT_FILEMODE_TREE) { + if (wd->mode == GIT_FILEMODE_TREE) + /* either deleting items in old tree will delete the wd dir, + * or we'll get a conflict when we attempt blob update... + */ + action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + else if (wd->mode == GIT_FILEMODE_COMMIT) { + /* workdir is possibly a "phantom" submodule - treat as a + * tree if the only submodule info came from the config + */ + if (submodule_is_config_only(data, wd->path)) + action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + else + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + } else + 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); + + /* don't update if the typechange is to a tree */ + if (delta->new_file.mode == GIT_FILEMODE_TREE) + action = (action & ~CHECKOUT_ACTION__UPDATE_BLOB); break; default: /* impossible */ break; @@ -481,7 +535,9 @@ static int checkout_action_with_wd_dir( 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) + if (delta->old_file.mode == GIT_FILEMODE_COMMIT) + /* expected submodule (and maybe found one) */; + else 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) */ @@ -491,19 +547,20 @@ static int checkout_action_with_wd_dir( 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) { + /* For typechange from dir, 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. + */ action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); if (action != CHECKOUT_ACTION__NONE) action |= CHECKOUT_ACTION__DEFER_REMOVE; } + else if (delta->new_file.mode != GIT_FILEMODE_TREE) + /* For typechange to dir, dir is already created so no action */ + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); break; default: /* impossible */ break; @@ -576,15 +633,29 @@ static int checkout_action( 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; + size_t pathlen = strlen(delta->old_file.path); + if (wd->path[pathlen] != '/') + return checkout_action_no_wd(data, delta); + + if (delta->status == GIT_DELTA_TYPECHANGE) { + if (delta->old_file.mode == GIT_FILEMODE_TREE) { + act = checkout_action_with_wd(data, delta, wd); + if (git_iterator_advance_into_directory(workdir, &wd) < 0) + wd = NULL; + *wditem_ptr = wd; + return act; + } + + if (delta->new_file.mode == GIT_FILEMODE_TREE || + delta->new_file.mode == GIT_FILEMODE_COMMIT || + delta->old_file.mode == GIT_FILEMODE_COMMIT) + { + act = checkout_action_with_wd(data, delta, wd); + if (git_iterator_advance(workdir, &wd) < 0) + wd = NULL; + *wditem_ptr = wd; + return act; + } } return checkout_action_with_wd_dir(data, delta, wd); @@ -834,6 +905,7 @@ static int checkout_submodule( const git_diff_file *file) { int error = 0; + git_submodule *sm; /* Until submodules are supported, UPDATE_ONLY means do nothing here */ if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) @@ -844,6 +916,9 @@ static int checkout_submodule( data->opts.dir_mode, GIT_MKDIR_PATH)) < 0) return error; + if ((error = git_submodule_lookup(&sm, data->repo, file->path)) < 0) + return error; + /* TODO: Support checkout_strategy options. Two circumstances: * 1 - submodule already checked out, but we need to move the HEAD * to the new OID, or @@ -924,6 +999,10 @@ static int checkout_blob( if (!error && (data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0) error = checkout_update_index(data, file, &st); + /* update the submodule data if this was a new .gitmodules file */ + if (!error && strcmp(file->path, ".gitmodules") == 0) + data->reload_submodules = true; + return error; } @@ -1036,6 +1115,11 @@ static int checkout_create_submodules( git_diff_delta *delta; size_t i; + /* initial reload of submodules if .gitmodules was changed */ + if (data->reload_submodules && + (error = git_submodule_reload_all(data->repo)) < 0) + return error; + 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 @@ -1056,7 +1140,8 @@ static int checkout_create_submodules( } } - return 0; + /* final reload once submodules have been updated */ + return git_submodule_reload_all(data->repo); } static int checkout_lookup_head_tree(git_tree **out, git_repository *repo) From c50c58decd92270319bcbdb59e1038e0e2f8f241 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Wed, 2 Jan 2013 17:10:56 -0800 Subject: [PATCH 17/26] Extend tests for checkout with typechanges Test a number of other cases, including intentionally forced conflicts and deeper inspection that trees get created properly. There is a still a bug in checkout because the first test here (i.e. test_checkout_typechange__checkout_typechanges_safe) should be able to pass with GIT_CHECKOUT_SAFE as a strategy, but it will not because of some lingering submodule checkout issues. --- tests-clar/checkout/typechange.c | 175 ++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 4 deletions(-) diff --git a/tests-clar/checkout/typechange.c b/tests-clar/checkout/typechange.c index bc7039caa..b92cc23fa 100644 --- a/tests-clar/checkout/typechange.c +++ b/tests-clar/checkout/typechange.c @@ -2,6 +2,7 @@ #include "git2/checkout.h" #include "path.h" #include "posix.h" +#include "fileops.h" static git_repository *g_repo = NULL; @@ -34,24 +35,97 @@ void test_checkout_typechange__cleanup(void) cl_fixture_cleanup("submod2_target"); } -void test_checkout_typechange__checkout_typechanges(void) +static void assert_file_exists(const char *path) +{ + cl_assert_(git_path_isfile(path), path); +} + +static void assert_dir_exists(const char *path) +{ + cl_assert_(git_path_isdir(path), path); +} + +static void assert_workdir_matches_tree( + git_repository *repo, const git_oid *id, const char *root, bool recurse) +{ + git_object *obj; + git_tree *tree; + size_t i, max_i; + git_buf path = GIT_BUF_INIT; + + if (!root) + root = git_repository_workdir(repo); + cl_assert(root); + + cl_git_pass(git_object_lookup(&obj, repo, id, GIT_OBJ_ANY)); + cl_git_pass(git_object_peel((git_object **)&tree, obj, GIT_OBJ_TREE)); + git_object_free(obj); + + max_i = git_tree_entrycount(tree); + + for (i = 0; i < max_i; ++i) { + const git_tree_entry *te = git_tree_entry_byindex(tree, i); + cl_assert(te); + + cl_git_pass(git_buf_joinpath(&path, root, git_tree_entry_name(te))); + + switch (git_tree_entry_type(te)) { + case GIT_OBJ_COMMIT: + assert_dir_exists(path.ptr); + break; + case GIT_OBJ_TREE: + assert_dir_exists(path.ptr); + if (recurse) + assert_workdir_matches_tree( + repo, git_tree_entry_id(te), path.ptr, true); + break; + case GIT_OBJ_BLOB: + switch (git_tree_entry_filemode(te)) { + case GIT_FILEMODE_BLOB: + case GIT_FILEMODE_BLOB_EXECUTABLE: + assert_file_exists(path.ptr); + /* because of cross-platform, don't confirm exec bit yet */ + break; + case GIT_FILEMODE_LINK: + cl_assert_(git_path_exists(path.ptr), path.ptr); + /* because of cross-platform, don't confirm link yet */ + break; + default: + cl_assert(false); /* really?! */ + } + break; + default: + cl_assert(false); /* really?!! */ + } + } + + git_tree_free(tree); + git_buf_free(&path); +} + +void test_checkout_typechange__checkout_typechanges_safe(void) { int i; git_object *obj; git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; - opts.checkout_strategy = GIT_CHECKOUT_FORCE; - 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]); */ + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + + /* There are bugs in some submodule->tree changes that prevent + * SAFE from passing here, even though the following should work: + */ + /* !i ? GIT_CHECKOUT_FORCE : GIT_CHECKOUT_SAFE; */ cl_git_pass(git_checkout_tree(g_repo, obj, &opts)); cl_git_pass( git_repository_set_head_detached(g_repo, git_object_id(obj))); + assert_workdir_matches_tree(g_repo, git_object_id(obj), NULL, true); + git_object_free(obj); if (!g_typechange_empty[i]) { @@ -71,3 +145,96 @@ void test_checkout_typechange__checkout_typechanges(void) } } } + +typedef struct { + int conflicts; + int dirty; + int updates; + int untracked; + int ignored; +} notify_counts; + +static int notify_counter( + 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) +{ + notify_counts *cts = payload; + + GIT_UNUSED(path); + GIT_UNUSED(baseline); + GIT_UNUSED(target); + GIT_UNUSED(workdir); + + switch (why) { + case GIT_CHECKOUT_NOTIFY_CONFLICT: cts->conflicts++; break; + case GIT_CHECKOUT_NOTIFY_DIRTY: cts->dirty++; break; + case GIT_CHECKOUT_NOTIFY_UPDATED: cts->updates++; break; + case GIT_CHECKOUT_NOTIFY_UNTRACKED: cts->untracked++; break; + case GIT_CHECKOUT_NOTIFY_IGNORED: cts->ignored++; break; + default: break; + } + + return 0; +} + +static void force_create_file(const char *file) +{ + int error = git_futils_rmdir_r(file, NULL, + GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_REMOVE_BLOCKERS); + cl_assert(!error || error == GIT_ENOTFOUND); + cl_git_pass(git_futils_mkpath2file(file, 0777)); + cl_git_rewritefile(file, "yowza!"); +} + +void test_checkout_typechange__checkout_with_conflicts(void) +{ + int i; + git_object *obj; + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + notify_counts cts = {0}; + + opts.notify_flags = + GIT_CHECKOUT_NOTIFY_CONFLICT | GIT_CHECKOUT_NOTIFY_UNTRACKED; + opts.notify_cb = notify_counter; + opts.notify_payload = &cts; + + for (i = 0; g_typechange_oids[i] != NULL; ++i) { + cl_git_pass(git_revparse_single(&obj, g_repo, g_typechange_oids[i])); + + force_create_file("typechanges/a/blocker"); + force_create_file("typechanges/b"); + force_create_file("typechanges/c/sub/sub/file"); + git_futils_rmdir_r("typechanges/d", NULL, GIT_RMDIR_REMOVE_FILES); + p_mkdir("typechanges/d", 0777); /* intentionally empty dir */ + force_create_file("typechanges/untracked"); + + opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; + memset(&cts, 0, sizeof(cts)); + + cl_git_fail(git_checkout_tree(g_repo, obj, &opts)); + cl_assert(cts.conflicts > 0); + cl_assert(cts.untracked > 0); + + opts.checkout_strategy = + GIT_CHECKOUT_FORCE | GIT_CHECKOUT_REMOVE_UNTRACKED; + memset(&cts, 0, sizeof(cts)); + + cl_assert(git_path_exists("typechanges/untracked")); + + cl_git_pass(git_checkout_tree(g_repo, obj, &opts)); + cl_assert_equal_i(0, cts.conflicts); + + cl_assert(!git_path_exists("typechanges/untracked")); + + cl_git_pass( + git_repository_set_head_detached(g_repo, git_object_id(obj))); + + assert_workdir_matches_tree(g_repo, git_object_id(obj), NULL, true); + + git_object_free(obj); + } +} From b3fb9237c215e9a0e2e042afd9252d541ce40541 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Wed, 2 Jan 2013 17:12:45 -0800 Subject: [PATCH 18/26] Clone should use GIT_CHECKOUT_SAFE_CREATE For clone to work as expected, it should be using a SAFE_CREATE checkout (i.e. create files that are missing, even if the target tree matches the current HEAD). --- tests-clar/online/clone.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests-clar/online/clone.c b/tests-clar/online/clone.c index c216a1ea7..082ed52b3 100644 --- a/tests-clar/online/clone.c +++ b/tests-clar/online/clone.c @@ -91,7 +91,7 @@ void test_online_clone__can_checkout_a_cloned_repo(void) bool checkout_progress_cb_was_called = false, fetch_progress_cb_was_called = false; - g_options.checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE; + g_options.checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; g_options.checkout_opts.progress_cb = &checkout_progress; g_options.checkout_opts.progress_payload = &checkout_progress_cb_was_called; g_options.fetch_progress_cb = &fetch_progress; From 77cffa31db07187c2fa65457ace1b6cb2547dc5b Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Wed, 2 Jan 2013 17:14:00 -0800 Subject: [PATCH 19/26] Simplify checkout documentation This moves a lot of the detailed checkout documentation into a new file (docs/checkout-internals.md) and simplifies the public docs for the checkout API. --- docs/checkout-internals.md | 203 +++++++++++++++++++++++++++++++++++ include/git2/checkout.h | 209 ++++++++++++++++--------------------- src/checkout.c | 184 ++------------------------------ 3 files changed, 299 insertions(+), 297 deletions(-) create mode 100644 docs/checkout-internals.md diff --git a/docs/checkout-internals.md b/docs/checkout-internals.md new file mode 100644 index 000000000..cb646da5d --- /dev/null +++ b/docs/checkout-internals.md @@ -0,0 +1,203 @@ +Checkout Internals +================== + +Checkout has to handle a lot of different cases. It examines the +differences between the target tree, the baseline tree and the working +directory, plus the contents of the index, and groups files into five +categories: + +1. UNMODIFIED - Files that match in all places. +2. SAFE - Files where the working directory and the baseline content + match that can be safely updated to the target. +3. DIRTY/MISSING - Files where the working directory differs from the + baseline but there is no conflicting change with the target. One + example is a file that doesn't exist in the working directory - no + data would be lost as a result of writing this file. Which action + will be taken with these files depends on the options you use. +4. CONFLICTS - Files where changes in the working directory conflict + with changes to be applied by the target. If conflicts are found, + they prevent any other modifications from being made (although there + are options to override that and force the update, of course). +5. UNTRACKED/IGNORED - Files in the working directory that are untracked + or ignored (i.e. only in the working directory, not the other places). + +Right now, this classification is done via 3 iterators (for the three +trees), with a final lookup in the index. At some point, this may move to +a 4 iterator version to incorporate the index better. + +The actual checkout is done in five phases (at least right now). + +1. The diff between the baseline and the target tree is used as a base + list of possible updates to be applied. +2. Iterate through the diff and the working directory, building a list of + actions to be taken (and sending notifications about conflicts and + dirty files). +3. Remove any files / directories as needed (because alphabetical + iteration means that an untracked directory will end up sorted *after* + a blob that should be checked out with the same name). +4. Update all blobs. +5. Update all submodules (after 4 in case a new .gitmodules blob was + checked out) + +Checkout could be driven either off a target-to-workdir diff or a +baseline-to-target diff. There are pros and cons of each. + +Target-to-workdir means the diff includes every file that could be +modified, which simplifies bookkeeping, but the code to constantly refer +back to the baseline gets complicated. + +Baseline-to-target has simpler code because the diff defines the action to +take, but needs special handling for untracked and ignored files, if they +need to be removed. + +The current checkout implementation is based on a baseline-to-target diff. + + +Picking Actions +=============== + +The most interesting aspect of this is phase 2, picking the actions that +should be taken. There are a lot of corner cases, so it may be easier to +start by looking at the rules for a simple 2-iterator diff: + +Key +--- +- B1,B2,B3 - blobs with different SHAs, +- Bi - ignored blob (WD only) +- T1,T2,T3 - trees with different SHAs, +- Ti - ignored tree (WD only) +- x - nothing + +Diff with 2 non-workdir iterators +--------------------------------- + + Old New + --- --- + 0 x x - nothing + 1 x B1 - added blob + 2 x T1 - added tree + 3 B1 x - removed blob + 4 B1 B1 - unmodified blob + 5 B1 B2 - modified blob + 6 B1 T1 - typechange blob -> tree + 7 T1 x - removed tree + 8 T1 B1 - typechange tree -> blob + 9 T1 T1 - unmodified tree + 10 T1 T2 - modified tree (implies modified/added/removed blob inside) + + +Now, let's make the "New" iterator into a working directory iterator, so +we replace "added" items with either untracked or ignored, like this: + +Diff with non-work & workdir iterators +-------------------------------------- + + Old New-WD + --- ------ + 0 x x - nothing + 1 x B1 - untracked blob + 2 x Bi - ignored file + 3 x T1 - untracked tree + 4 x Ti - ignored tree + 5 B1 x - removed blob + 6 B1 B1 - unmodified blob + 7 B1 B2 - modified blob + 8 B1 T1 - typechange blob -> tree + 9 B1 Ti - removed blob AND ignored tree as separate items + 10 T1 x - removed tree + 11 T1 B1 - typechange tree -> blob + 12 T1 Bi - removed tree AND ignored blob as separate items + 13 T1 T1 - unmodified tree + 14 T1 T2 - modified tree (implies modified/added/removed blob inside) + +Note: if there is a corresponding entry in the old tree, then a working +directory item won't be ignored (i.e. no Bi or Ti for tracked items). + + +Now, expand this to three iterators: a baseline tree, a target tree, and +an actual working directory tree: + +Checkout From 3 Iterators (2 not workdir, 1 workdir) +---------------------------------------------------- + +(base == old HEAD; target == what to checkout; actual == working dir) + + base target actual/workdir + ---- ------ ------ + 0 x x x - nothing + 1 x x B1/Bi/T1/Ti - untracked/ignored blob/tree (SAFE) + 2+ x B1 x - add blob (SAFE) + 3 x B1 B1 - independently added blob (FORCEABLE-2) + 4* x B1 B2/Bi/T1/Ti - add blob with content conflict (FORCEABLE-2) + 5+ x T1 x - add tree (SAFE) + 6* x T1 B1/Bi - add tree with blob conflict (FORCEABLE-2) + 7 x T1 T1/i - independently added tree (SAFE+MISSING) + 8 B1 x x - independently deleted blob (SAFE+MISSING) + 9- B1 x B1 - delete blob (SAFE) + 10- B1 x B2 - delete of modified blob (FORCEABLE-1) + 11 B1 x T1/Ti - independently deleted blob AND untrack/ign tree (SAFE+MISSING !!!) + 12 B1 B1 x - locally deleted blob (DIRTY || SAFE+CREATE) + 13+ B1 B2 x - update to deleted blob (SAFE+MISSING) + 14 B1 B1 B1 - unmodified file (SAFE) + 15 B1 B1 B2 - locally modified file (DIRTY) + 16+ B1 B2 B1 - update unmodified blob (SAFE) + 17 B1 B2 B2 - independently updated blob (FORCEABLE-1) + 18+ B1 B2 B3 - update to modified blob (FORCEABLE-1) + 19 B1 B1 T1/Ti - locally deleted blob AND untrack/ign tree (DIRTY) + 20* B1 B2 T1/Ti - update to deleted blob AND untrack/ign tree (F-1) + 21+ B1 T1 x - add tree with locally deleted blob (SAFE+MISSING) + 22* B1 T1 B1 - add tree AND deleted blob (SAFE) + 23* B1 T1 B2 - add tree with delete of modified blob (F-1) + 24 B1 T1 T1 - add tree with deleted blob (F-1) + 25 T1 x x - independently deleted tree (SAFE+MISSING) + 26 T1 x B1/Bi - independently deleted tree AND untrack/ign blob (F-1) + 27- T1 x T1 - deleted tree (MAYBE SAFE) + 28+ T1 B1 x - deleted tree AND added blob (SAFE+MISSING) + 29 T1 B1 B1 - independently typechanged tree -> blob (F-1) + 30+ T1 B1 B2 - typechange tree->blob with conflicting blob (F-1) + 31* T1 B1 T1/T2 - typechange tree->blob (MAYBE SAFE) + 32+ T1 T1 x - restore locally deleted tree (SAFE+MISSING) + 33 T1 T1 B1/Bi - locally typechange tree->untrack/ign blob (DIRTY) + 34 T1 T1 T1/T2 - unmodified tree (MAYBE SAFE) + 35+ T1 T2 x - update locally deleted tree (SAFE+MISSING) + 36* T1 T2 B1/Bi - update to tree with typechanged tree->blob conflict (F-1) + 37 T1 T2 T1/T2/T3 - update to existing tree (MAYBE SAFE) + +The number is followed by ' ' if no change is needed or '+' if the case +needs to write to disk or '-' if something must be deleted and '*' if +there should be a delete followed by an write. + +There are four tiers of safe cases: + +- SAFE == completely safe to update +- SAFE+MISSING == safe except the workdir is missing the expect content +- MAYBE SAFE == safe if workdir tree matches (or is missing) baseline + content, which is unknown at this point +- FORCEABLE == conflict unless FORCE is given +- DIRTY == no conflict but change is not applied unless FORCE + +Some slightly unusual circumstances: + + 8 - parent dir is only deleted when file is, so parent will be left if + empty even though it would be deleted if the file were present + 11 - core git does not consider this a conflict but attempts to delete T1 + and gives "unable to unlink file" error yet does not skip the rest + of the operation + 12 - without FORCE file is left deleted (i.e. not restored) so new wd is + dirty (and warning message "D file" is printed), with FORCE, file is + restored. + 24 - This should be considered MAYBE SAFE since effectively it is 7 and 8 + combined, but core git considers this a conflict unless forced. + 26 - This combines two cases (1 & 25) (and also implied 8 for tree content) + which are ok on their own, but core git treat this as a conflict. + If not forced, this is a conflict. If forced, this actually doesn't + have to write anything and leaves the new blob as an untracked file. + 32 - This is the only case where the baseline and target values match + and yet we will still write to the working directory. In all other + cases, if baseline == target, we don't touch the workdir (it is + either already right or is "dirty"). However, since this case also + implies that a ?/B1/x case will exist as well, it can be skipped. + +Cases 3, 17, 24, 26, and 29 are all considered conflicts even though +none of them will require making any updates to the working directory. + diff --git a/include/git2/checkout.h b/include/git2/checkout.h index 884ea27f6..12fffebad 100644 --- a/include/git2/checkout.h +++ b/include/git2/checkout.h @@ -23,97 +23,82 @@ GIT_BEGIN_DECL /** * Checkout behavior flags * - * In libgit2, the function of checkout is to update the working directory - * to match a target tree. It does not move the HEAD commit - you do that - * separately. To safely perform the update, checkout relies on a baseline - * tree (generally the current HEAD) as a reference for the unmodified - * content expected in the working directory. + * In libgit2, checkout is used to update the working directory and index + * to match a target tree. Unlike git checkout, it does not move the HEAD + * commit for you - use `git_repository_set_head` or the like to do that. * - * Checkout examines the differences between the target tree, the baseline - * tree and the working directory, and groups files into five categories: + * Checkout looks at (up to) four things: the "target" tree you want to + * check out, the "baseline" tree of what was checked out previously, the + * working directory for actual files, and the index for staged changes. * - * 1. UNMODIFIED - Files that match in all places. - * 2. SAFE - Files where the working directory and the baseline content - * match that can be safely updated to the target. - * 3. DIRTY/MISSING - Files where the working directory differs from the - * baseline but there is no conflicting change with the target. One - * example is a file that doesn't exist in the working directory - no - * data would be lost as a result of writing this file. Which action - * will be taken with these files depends on the options you use. - * 4. CONFLICTS - Files where changes in the working directory conflict - * with changes to be applied by the target. If conflicts are found, - * they prevent any other modifications from being made (although there - * are options to override that and force the update, of course). - * 5. UNTRACKED/IGNORED - Files in the working directory that are untracked - * or ignored (i.e. only in the working directory, not the other places). + * You give checkout one of four strategies for update: * + * - `GIT_CHECKOUT_NONE` is a dry-run strategy that checks for conflicts, + * etc., but doesn't make any actual changes. * - * You control the actions checkout takes with one of four base strategies: + * - `GIT_CHECKOUT_FORCE` is at the opposite extreme, taking any action to + * make the working directory match the target (including potentially + * discarding modified files). * - * - `GIT_CHECKOUT_NONE` is the default and applies no changes. It is a dry - * run that you can use to find conflicts, etc. if you wish. + * In between those are `GIT_CHECKOUT_SAFE` and `GIT_CHECKOUT_SAFE_CREATE` + * both of which only make modifications that will not lose changes. * - * - `GIT_CHECKOUT_SAFE` is like `git checkout` and only applies changes - * between the baseline and target trees to files in category 2. + * | target == baseline | target != baseline | + * ---------------------|-----------------------|----------------------| + * workdir == baseline | no action | create, update, or | + * | | delete file | + * ---------------------|-----------------------|----------------------| + * workdir exists and | no action | conflict (notify | + * is != baseline | notify dirty MODIFIED | and cancel checkout) | + * ---------------------|-----------------------|----------------------| + * workdir missing, | create if SAFE_CREATE | create file | + * baseline present | notify dirty DELETED | | + * ---------------------|-----------------------|----------------------| * - * - `GIT_CHECKOUT_SAFE_CREATE` also creates files that are missing from the - * working directory (category 3), even if there is no change between the - * baseline and target trees for those files. See notes below on - * emulating `git checkout-index` for some of the subtleties of this. - * - * - `GIT_CHECKOUT_FORCE` is like `git checkout -f` and will update the - * working directory to match the target content regardless of conflicts, - * overwriting dirty and conflicting files. - * - * - * There are some additional flags to modified the behavior of checkout: - * - * - GIT_CHECKOUT_ALLOW_CONFLICTS can be added to apply safe file updates - * even if there are conflicts. Normally, the entire checkout will be - * cancelled if any files are in category 4. With this flag, conflicts - * will be skipped (though the notification callback will still be invoked - * on the conflicting files if requested). - * - * - GIT_CHECKOUT_REMOVE_UNTRACKED means that files in the working directory - * that are untracked (but not ignored) should be deleted. The are not - * considered conflicts and would normally be ignored by checkout. - * - * - GIT_CHECKOUT_REMOVE_IGNORED means to remove ignored files from the - * working directory as well. Obviously, these would normally be ignored. - * - * - GIT_CHECKOUT_UPDATE_ONLY means to only update the content of files that - * already exist. Files will not be created nor deleted. This does not - * make adds and deletes into conflicts - it just skips applying those - * changes. This will also skip updates to typechanged files (since that - * would involve deleting the old and creating the new). - * - * - Unmerged entries in the index are also considered conflicts. The - * GIT_CHECKOUT_SKIP_UNMERGED flag causes us to skip files with unmerged - * index entries. You can also use GIT_CHECKOUT_USE_OURS and - * GIT_CHECKOUT_USE_THEIRS to proceeed with the checkout using either the - * stage 2 ("ours") or stage 3 ("theirs") version of files in the index. + * The only difference between SAFE and SAFE_CREATE is that SAFE_CREATE + * will cause a file to be checked out if it is missing from the working + * directory even if it is not modified between the target and baseline. * * * To emulate `git checkout`, use `GIT_CHECKOUT_SAFE` with a checkout * notification callback (see below) that displays information about dirty - * files (i.e. files that don't need an update but that no longer match the - * baseline content). The default behavior will cancel on conflicts. + * files. The default behavior will cancel checkout 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 * file is found in the working directory. This core git command isn't * quite "force" but is sensitive about some types of changes. * - * To emulate `git checkout -f`, you use `GIT_CHECKOUT_FORCE`. + * To emulate `git checkout -f`, use `GIT_CHECKOUT_FORCE`. + * + * To emulate `git clone` use `GIT_CHECKOUT_SAFE_CREATE` in the options. * * - * Checkout is "semi-atomic" as in it will go through the work to be done - * before making any changes and if may decide to abort if there are - * conflicts, or you can use the notification callback to explicitly abort - * the action before any updates are made. Despite this, if a second - * process is modifying the filesystem while checkout is running, it can't - * guarantee that the choices is makes while initially examining the - * filesystem are still going to be correct as it applies them. + * There are some additional flags to modified the behavior of checkout: + * + * - GIT_CHECKOUT_ALLOW_CONFLICTS makes SAFE mode apply safe file updates + * even if there are conflicts (instead of cancelling the checkout). + * + * - GIT_CHECKOUT_REMOVE_UNTRACKED means remove untracked files (i.e. not + * in target, baseline, or index, and not ignored) from the working dir. + * + * - GIT_CHECKOUT_REMOVE_IGNORED means remove ignored files (that are also + * unrtacked) from the working directory as well. + * + * - GIT_CHECKOUT_UPDATE_ONLY means to only update the content of files that + * already exist. Files will not be created nor deleted. This just skips + * applying adds, deletes, and typechanges. + * + * - GIT_CHECKOUT_DONT_UPDATE_INDEX prevents checkout from writing the + * updated files' information to the index. + * + * - Normally, checkout will reload the index and git attributes from disk + * before any operations. GIT_CHECKOUT_NO_REFRESH prevents this reload. + * + * - Unmerged index entries are conflicts. GIT_CHECKOUT_SKIP_UNMERGED skips + * files with unmerged index entries instead. GIT_CHECKOUT_USE_OURS and + * GIT_CHECKOUT_USE_THEIRS to proceeed with the checkout using either the + * stage 2 ("ours") or stage 3 ("theirs") version of files in the index. */ typedef enum { GIT_CHECKOUT_NONE = 0, /** default is a dry run, no actual updates */ @@ -167,45 +152,23 @@ typedef enum { /** * Checkout notification flags * - * When running a checkout, you can set a notification callback (`notify_cb`) - * to be invoked for some or all files to be checked out. Which files - * receive a callback depend on the `notify_flags` value which is a - * combination of these flags. + * Checkout will invoke an options notification callback (`notify_cb`) for + * certain cases - you pick which ones via `notify_flags`: * - * - GIT_CHECKOUT_NOTIFY_CONFLICT means that conflicting files that would - * prevent the checkout from occurring will receive callbacks. If you - * used GIT_CHECKOUT_ALLOW_CONFLICTS, the callbacks are still done, but - * the checkout will not be blocked. The callback `status_flags` will - * have both index and work tree change bits set (see `git_status_t`). + * - GIT_CHECKOUT_NOTIFY_CONFLICT invokes checkout on conflicting paths. * - * - GIT_CHECKOUT_NOTIFY_DIRTY means to notify about "dirty" files, i.e. - * those that do not need to be updated but no longer match the baseline - * content. Core git displays these files when checkout runs, but does - * not stop the checkout. For these, `status_flags` will have only work - * tree bits set (i.e. GIT_STATUS_WT_MODIFIED, etc). + * - GIT_CHECKOUT_NOTIFY_DIRTY notifies about "dirty" files, i.e. those that + * do not need an update but no longer match the baseline. Core git + * displays these files when checkout runs, but won't stop the checkout. * - * - GIT_CHECKOUT_NOTIFY_UPDATED sends notification for any file changed by - * the checkout. Callback `status_flags` will have only index bits set. + * - GIT_CHECKOUT_NOTIFY_UPDATED sends notification for any file changed. * - * - GIT_CHECKOUT_NOTIFY_UNTRACKED notifies for all untracked files that - * are not ignored. Passing GIT_CHECKOUT_REMOVE_UNTRACKED would remove - * these files. The `status_flags` will be GIT_STATUS_WT_NEW. + * - GIT_CHECKOUT_NOTIFY_UNTRACKED notifies about untracked files. * - * - GIT_CHECKOUT_NOTIFY_IGNORED notifies for the ignored files. Passing - * GIT_CHECKOUT_REMOVE_IGNORED will remove these. The `status_flags` - * will be to GIT_STATUS_IGNORED. + * - GIT_CHECKOUT_NOTIFY_IGNORED notifies about ignored files. * - * If you return a non-zero value from the notify callback, the checkout - * will be canceled. Notification callbacks are made prior to making any - * modifications, so returning non-zero will cancel the entire checkout. - * If you are do not use GIT_CHECKOUT_ALLOW_CONFLICTS and there are - * conflicts, you don't need to explicitly cancel from the callback. - * Checkout itself will abort after all files are processed. - * - * To emulate core git checkout output, use GIT_CHECKOUT_NOTIFY_CONFLICTS - * and GIT_CHECKOUT_NOTIFY_DIRTY. Conflicts will have `status_flags` with - * changes in both the index and work tree (see the `git_status_t` values). - * Dirty files will only have work tree flags set. + * Returning a non-zero value from this callback will cancel the checkout. + * Notification callbacks are made prior to modifying any files on disk. */ typedef enum { GIT_CHECKOUT_NOTIFY_NONE = 0, @@ -216,13 +179,27 @@ typedef enum { GIT_CHECKOUT_NOTIFY_IGNORED = (1u << 4), } git_checkout_notify_t; +/** Checkout notification callback function */ +typedef int (*git_checkout_notify_cb)( + git_checkout_notify_t why, + const char *path, + const git_diff_file *baseline, + const git_diff_file *target, + const git_diff_file *workdir, + void *payload); + +/** Checkout progress notification function */ +typedef void (*git_checkout_progress_cb)( + const char *path, + size_t completed_steps, + size_t total_steps, + void *payload); + /** * Checkout options structure * - * Use zeros to indicate default settings. - * - * This should be initialized with the `GIT_CHECKOUT_OPTS_INIT` macro to - * correctly set the `version` field. + * Zero out for defaults. Initialize with `GIT_CHECKOUT_OPTS_INIT` macro to + * correctly set the `version` field. E.g. * * git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; */ @@ -237,21 +214,11 @@ typedef struct git_checkout_opts { int file_open_flags; /** default is O_CREAT | O_TRUNC | O_WRONLY */ unsigned int notify_flags; /** see `git_checkout_notify_t` above */ - int (*notify_cb)( - git_checkout_notify_t why, - const char *path, - const git_diff_file *baseline, - const git_diff_file *target, - const git_diff_file *workdir, - void *payload); + git_checkout_notify_cb notify_cb; void *notify_payload; /* Optional callback to notify the consumer of checkout progress. */ - void (*progress_cb)( - const char *path, - size_t completed_steps, - size_t total_steps, - void *payload); + git_checkout_progress_cb progress_cb; void *progress_payload; /** When not zeroed out, array of fnmatch patterns specifying which diff --git a/src/checkout.c b/src/checkout.c index a26f007b1..2e132947d 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -24,157 +24,7 @@ #include "diff.h" #include "pathspec.h" -/* Key - * === - * B1,B2,B3 - blobs with different SHAs, - * Bi - ignored blob (WD only) - * T1,T2,T3 - trees with different SHAs, - * Ti - ignored tree (WD only) - * x - nothing - */ - -/* Diff with 2 non-workdir iterators - * ================================= - * Old New - * --- --- - * 0 x x - nothing - * 1 x B1 - added blob - * 2 x T1 - added tree - * 3 B1 x - removed blob - * 4 B1 B1 - unmodified blob - * 5 B1 B2 - modified blob - * 6 B1 T1 - typechange blob -> tree - * 7 T1 x - removed tree - * 8 T1 B1 - typechange tree -> blob - * 9 T1 T1 - unmodified tree - * 10 T1 T2 - modified tree (implies modified/added/removed blob inside) - */ - -/* Diff with non-work & workdir iterators - * ====================================== - * Old New-WD - * --- ------ - * 0 x x - nothing - * 1 x B1 - added blob - * 2 x Bi - ignored file - * 3 x T1 - added tree - * 4 x Ti - ignored tree - * 5 B1 x - removed blob - * 6 B1 B1 - unmodified blob - * 7 B1 B2 - modified blob - * 8 B1 T1 - typechange blob -> tree - * 9 B1 Ti - removed blob AND ignored tree as separate items - * 10 T1 x - removed tree - * 11 T1 B1 - typechange tree -> blob - * 12 T1 Bi - removed tree AND ignored blob as separate items - * 13 T1 T1 - unmodified tree - * 14 T1 T2 - modified tree (implies modified/added/removed blob inside) - * - * If there is a corresponding blob in the old, Bi is irrelevant - * If there is a corresponding tree in the old, Ti is irrelevant - */ - -/* Checkout From 3 Iterators (2 not workdir, 1 workdir) - * ==================================================== - * - * (Expect == Old HEAD / Desire == What To Checkout / Actual == Workdir) - * - * Expect Desire Actual-WD - * ------ ------ ------ - * 0 x x x - nothing - * 1 x x B1/Bi/T1/Ti - untracked/ignored blob/tree (SAFE) - * 2+ x B1 x - add blob (SAFE) - * 3 x B1 B1 - independently added blob (FORCEABLE-2) - * 4* x B1 B2/Bi/T1/Ti - add blob with content conflict (FORCEABLE-2) - * 5+ x T1 x - add tree (SAFE) - * 6* x T1 B1/Bi - add tree with blob conflict (FORCEABLE-2) - * 7 x T1 T1/i - independently added tree (SAFE+MISSING) - * 8 B1 x x - independently deleted blob (SAFE+MISSING) - * 9- B1 x B1 - delete blob (SAFE) - * 10- B1 x B2 - delete of modified blob (FORCEABLE-1) - * 11 B1 x T1/Ti - independently deleted blob AND untrack/ign tree (SAFE+MISSING !!!) - * 12 B1 B1 x - locally deleted blob (DIRTY || SAFE+CREATE) - * 13+ B1 B2 x - update to deleted blob (SAFE+MISSING) - * 14 B1 B1 B1 - unmodified file (SAFE) - * 15 B1 B1 B2 - locally modified file (DIRTY) - * 16+ B1 B2 B1 - update unmodified blob (SAFE) - * 17 B1 B2 B2 - independently updated blob (FORCEABLE-1) - * 18+ B1 B2 B3 - update to modified blob (FORCEABLE-1) - * 19 B1 B1 T1/Ti - locally deleted blob AND untrack/ign tree (DIRTY) - * 20* B1 B2 T1/Ti - update to deleted blob AND untrack/ign tree (F-1) - * 21+ B1 T1 x - add tree with locally deleted blob (SAFE+MISSING) - * 22* B1 T1 B1 - add tree AND deleted blob (SAFE) - * 23* B1 T1 B2 - add tree with delete of modified blob (F-1) - * 24 B1 T1 T1 - add tree with deleted blob (F-1) - * 25 T1 x x - independently deleted tree (SAFE+MISSING) - * 26 T1 x B1/Bi - independently deleted tree AND untrack/ign blob (F-1) - * 27- T1 x T1 - deleted tree (MAYBE SAFE) - * 28+ T1 B1 x - deleted tree AND added blob (SAFE+MISSING) - * 29 T1 B1 B1 - independently typechanged tree -> blob (F-1) - * 30+ T1 B1 B2 - typechange tree->blob with conflicting blob (F-1) - * 31* T1 B1 T1/T2 - typechange tree->blob (MAYBE SAFE) - * 32+ T1 T1 x - restore locally deleted tree (SAFE+MISSING) - * 33 T1 T1 B1/Bi - locally typechange tree->untrack/ign blob (DIRTY) - * 34 T1 T1 T1/T2 - unmodified tree (MAYBE SAFE) - * 35+ T1 T2 x - update locally deleted tree (SAFE+MISSING) - * 36* T1 T2 B1/Bi - update to tree with typechanged tree->blob conflict (F-1) - * 37 T1 T2 T1/T2/T3 - update to existing tree (MAYBE SAFE) - * - * The number will be followed by ' ' if no change is needed or '+' if the - * case needs to write to disk or '-' if something must be deleted and '*' - * if there should be a delete followed by an write. - * - * There are four tiers of safe cases: - * - SAFE == completely safe to update - * - SAFE+MISSING == safe except the workdir is missing the expect content - * - MAYBE SAFE == safe if workdir tree matches (or is missing) baseline - * content, which is unknown at this point - * - FORCEABLE == conflict unless FORCE is given - * - DIRTY == no conflict but change is not applied unless FORCE - * - * Some slightly unusual circumstances: - * 8 - parent dir is only deleted when file is, so parent will be left if - * empty even though it would be deleted if the file were present - * 11 - core git does not consider this a conflict but attempts to delete T1 - * and gives "unable to unlink file" error yet does not skip the rest - * of the operation - * 12 - without FORCE file is left deleted (i.e. not restored) so new wd is - * dirty (and warning message "D file" is printed), with FORCE, file is - * restored. - * 24 - This should be considered MAYBE SAFE since effectively it is 7 and 8 - * combined, but core git considers this a conflict unless forced. - * 26 - This combines two cases (1 & 25) (and also implied 8 for tree content) - * which are ok on their own, but core git treat this as a conflict. - * If not forced, this is a conflict. If forced, this actually doesn't - * have to write anything and leaves the new blob as an untracked file. - * 32 - This is the only case where the baseline and target values match - * and yet we will still write to the working directory. In all other - * cases, if baseline == target, we don't touch the workdir (it is - * either already right or is "dirty"). However, since this case also - * implies that a ?/B1/x case will exist as well, it can be skipped. - * - * Cases 3, 17, 24, 26, and 29 are all considered conflicts even though - * none of them will require making any updates to the working directory. - */ - -/* expect desire wd - * 1 x x T -> ignored dir OR untracked dir OR parent dir - * 2 x x I -> ignored file - * 3 x x A -> untracked file - * 4 x A x -> add from index (no conflict) - * 5 x A A -> independently added file - * 6 x A B -> add with conflicting file - * 7 A x x -> independently deleted file - * 8 A x A -> delete from index (no conflict) - * 9 A x B -> delete of modified file - * 10 A A x -> locally deleted file - * 11 A A A -> unmodified file (no conflict) - * 12 A A B -> locally modified - * 13 A B x -> update of deleted file - * 14 A B A -> update of unmodified file (no conflict) - * 15 A B B -> independently updated file - * 16 A B C -> update of modified file - */ +/* See docs/checkout-internals.md for more information */ enum { CHECKOUT_ACTION__NONE = 0, @@ -1317,34 +1167,15 @@ int git_checkout_iterator( goto cleanup; } - /* Checkout can be driven either off a target-to-workdir diff or a - * baseline-to-target diff. There are pros and cons of each. - * - * Target-to-workdir means the diff includes every file that could be - * modified, which simplifies bookkeeping, but the code to constantly - * refer back to the baseline gets complicated. - * - * Baseline-to-target has simpler code because the diff defines the - * action to take, but needs special handling for untracked and ignored - * files, if they need to be removed. - * - * I've implemented both versions and opted for the second. + /* Generate baseline-to-target diff which will include an entry for + * every possible update that might need to be made. */ if ((error = git_diff__from_iterators( &data.diff, data.repo, baseline, target, &diff_opts)) < 0) goto cleanup; - /* In order to detect conflicts prior to performing any operations, - * and in order to deal with some order dependencies, checkout is best - * performed with up to four passes through the diff. - * - * 0. Figure out the actions to be taken, - * 1. Remove any files / directories as needed (because alphabetical - * iteration means that an untracked directory will end up sorted - * *after* a blob that should be checked out with the same name), - * 2. Then update all blobs, - * 3. Then update all submodules in case a new .gitmodules blob was - * checked out during pass #2. + /* Loop through diff (and working directory iterator) building a list of + * actions to be taken, plus look for conflicts and send notifications. */ if ((error = checkout_get_actions(&actions, &counts, &data, workdir)) < 0) goto cleanup; @@ -1355,8 +1186,9 @@ int git_checkout_iterator( report_progress(&data, NULL); /* establish 0 baseline */ - /* TODO: add ability to update index entries while checking out */ - + /* To deal with some order dependencies, perform remaining checkout + * in three passes: removes, then update blobs, then update submodules. + */ if (counts[CHECKOUT_ACTION__REMOVE] > 0 && (error = checkout_remove_the_old(actions, &data)) < 0) goto cleanup; From 2850252af7e9baf070a495f60781d2424e6a0b32 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Thu, 3 Jan 2013 09:11:52 -0800 Subject: [PATCH 20/26] Oh yeah, bugs from my rebase --- src/clone.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clone.c b/src/clone.c index 39c0ba26c..ff586a68a 100644 --- a/src/clone.c +++ b/src/clone.c @@ -362,7 +362,7 @@ static bool should_checkout( if (!opts) return false; - if (opts->checkout_strategy == GIT_CHECKOUT_DEFAULT) + if (opts->checkout_strategy == GIT_CHECKOUT_NONE) return false; return !git_repository_head_orphan(repo); From 0d70f650518163af2b4d46028b1ce9cef71fbc99 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Thu, 3 Jan 2013 10:51:18 -0800 Subject: [PATCH 21/26] Fixing checkout UPDATE_ONLY and adding tests This adds a bunch of new checkout tests and in the process I found a bug in the GIT_CHECKOUT_UPDATE_ONLY flag which I fixed. --- src/checkout.c | 28 ++++++ tests-clar/checkout/tree.c | 171 +++++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) diff --git a/src/checkout.c b/src/checkout.c index 2e132947d..342852792 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -810,6 +810,27 @@ static void report_progress( data->opts.progress_payload); } +static int checkout_safe_for_update_only(const char *path, mode_t expected_mode) +{ + struct stat st; + + if (p_lstat(path, &st) < 0) { + /* if doesn't exist, then no error and no update */ + if (errno == ENOENT || errno == ENOTDIR) + return 0; + + /* otherwise, stat error and no update */ + giterr_set(GITERR_OS, "Failed to stat file '%s'", path); + return -1; + } + + /* only safe for update if this is the same type of file */ + if ((st.st_mode & ~0777) == (expected_mode & ~0777)) + return 1; + + return 0; +} + static int checkout_blob( checkout_data *data, const git_diff_file *file) @@ -822,6 +843,13 @@ static int checkout_blob( if (git_buf_puts(&data->path, file->path) < 0) return -1; + if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) { + int rval = checkout_safe_for_update_only( + git_buf_cstr(&data->path), file->mode); + if (rval <= 0) + return rval; + } + if ((error = git_blob_lookup(&blob, data->repo, &file->oid)) < 0) return error; diff --git a/tests-clar/checkout/tree.c b/tests-clar/checkout/tree.c index 6e7175a87..ff5c43aef 100644 --- a/tests-clar/checkout/tree.c +++ b/tests-clar/checkout/tree.c @@ -2,6 +2,8 @@ #include "git2/checkout.h" #include "repository.h" +#include "buffer.h" +#include "fileops.h" static git_repository *g_repo; static git_checkout_opts g_opts; @@ -134,3 +136,172 @@ void test_checkout_tree__doesnt_write_unrequested_files_to_worktree(void) git_checkout_tree(g_repo, (git_object*)p_chomped_commit, &opts); cl_assert_equal_i(false, git_path_isfile("testrepo/readme.txt")); } + +static void assert_on_branch(git_repository *repo, const char *branch) +{ + git_reference *head; + git_buf bname = GIT_BUF_INIT; + + cl_git_pass(git_reference_lookup(&head, repo, GIT_HEAD_FILE)); + cl_assert_(git_reference_type(head) == GIT_REF_SYMBOLIC, branch); + + cl_git_pass(git_buf_joinpath(&bname, "refs/heads", branch)); + cl_assert_equal_s(bname.ptr, git_reference_symbolic_target(head)); + + git_reference_free(head); + git_buf_free(&bname); +} + +void test_checkout_tree__can_switch_branches(void) +{ + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + git_oid oid; + git_object *obj = NULL; + + assert_on_branch(g_repo, "master"); + + /* do first checkout with FORCE because we don't know if testrepo + * base data is clean for a checkout or not + */ + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + + cl_git_pass(git_reference_name_to_id(&oid, g_repo, "refs/heads/dir")); + cl_git_pass(git_object_lookup(&obj, g_repo, &oid, GIT_OBJ_ANY)); + + cl_git_pass(git_checkout_tree(g_repo, obj, &opts)); + cl_git_pass(git_repository_set_head(g_repo, "refs/heads/dir")); + + cl_assert(git_path_isfile("testrepo/README")); + cl_assert(git_path_isfile("testrepo/branch_file.txt")); + cl_assert(git_path_isfile("testrepo/new.txt")); + cl_assert(git_path_isfile("testrepo/a/b.txt")); + + cl_assert(!git_path_isdir("testrepo/ab")); + + assert_on_branch(g_repo, "dir"); + + git_object_free(obj); + + /* do second checkout safe because we should be clean after first */ + opts.checkout_strategy = GIT_CHECKOUT_SAFE; + + cl_git_pass(git_reference_name_to_id(&oid, g_repo, "refs/heads/subtrees")); + cl_git_pass(git_object_lookup(&obj, g_repo, &oid, GIT_OBJ_ANY)); + + cl_git_pass(git_checkout_tree(g_repo, obj, &opts)); + cl_git_pass(git_repository_set_head(g_repo, "refs/heads/subtrees")); + + cl_assert(git_path_isfile("testrepo/README")); + cl_assert(git_path_isfile("testrepo/branch_file.txt")); + cl_assert(git_path_isfile("testrepo/new.txt")); + cl_assert(git_path_isfile("testrepo/ab/4.txt")); + cl_assert(git_path_isfile("testrepo/ab/c/3.txt")); + cl_assert(git_path_isfile("testrepo/ab/de/2.txt")); + cl_assert(git_path_isfile("testrepo/ab/de/fgh/1.txt")); + + cl_assert(!git_path_isdir("testrepo/a")); + + assert_on_branch(g_repo, "subtrees"); + + git_object_free(obj); +} + +void test_checkout_tree__can_remove_untracked(void) +{ + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + + opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_REMOVE_UNTRACKED; + + cl_git_mkfile("testrepo/untracked_file", "as you wish"); + cl_assert(git_path_isfile("testrepo/untracked_file")); + + cl_git_pass(git_checkout_head(g_repo, &opts)); + + cl_assert(!git_path_isfile("testrepo/untracked_file")); +} + +void test_checkout_tree__can_remove_ignored(void) +{ + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + int ignored = 0; + + opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_REMOVE_IGNORED; + + cl_git_mkfile("testrepo/ignored_file", "as you wish"); + + cl_git_pass(git_ignore_add_rule(g_repo, "ignored_file\n")); + + cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "ignored_file")); + cl_assert_equal_i(1, ignored); + + cl_assert(git_path_isfile("testrepo/ignored_file")); + + cl_git_pass(git_checkout_head(g_repo, &opts)); + + cl_assert(!git_path_isfile("testrepo/ignored_file")); +} + +/* this is essentially the code from git__unescape modified slightly */ +static void strip_cr_from_buf(git_buf *buf) +{ + char *scan, *pos = buf->ptr; + + for (scan = pos; *scan; pos++, scan++) { + if (*scan == '\r') + scan++; /* skip '\r' */ + if (pos != scan) + *pos = *scan; + } + + *pos = '\0'; + buf->size = (pos - buf->ptr); +} + +void test_checkout_tree__can_update_only(void) +{ + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + git_oid oid; + git_object *obj = NULL; + git_buf buf = GIT_BUF_INIT; + + /* first let's get things into a known state - by checkout out the HEAD */ + + assert_on_branch(g_repo, "master"); + + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + cl_git_pass(git_checkout_head(g_repo, &opts)); + + cl_assert(!git_path_isdir("testrepo/a")); + + cl_git_pass(git_futils_readbuffer(&buf, "testrepo/branch_file.txt")); + strip_cr_from_buf(&buf); + cl_assert_equal_s("hi\nbye!\n", buf.ptr); + git_buf_free(&buf); + + /* now checkout branch but with update only */ + + opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_UPDATE_ONLY; + + cl_git_pass(git_reference_name_to_id(&oid, g_repo, "refs/heads/dir")); + cl_git_pass(git_object_lookup(&obj, g_repo, &oid, GIT_OBJ_ANY)); + + cl_git_pass(git_checkout_tree(g_repo, obj, &opts)); + cl_git_pass(git_repository_set_head(g_repo, "refs/heads/dir")); + + assert_on_branch(g_repo, "dir"); + + /* this normally would have been created (which was tested separately in + * the test_checkout_tree__can_switch_branches test), but with + * UPDATE_ONLY it will not have been created. + */ + cl_assert(!git_path_isdir("testrepo/a")); + + /* but this file still should have been updated */ + cl_git_pass(git_futils_readbuffer(&buf, "testrepo/branch_file.txt")); + strip_cr_from_buf(&buf); + cl_assert_equal_s("hi\n", buf.ptr); + + git_buf_free(&buf); + + git_object_free(obj); +} From dde7602ae633a557acdb74c8027335bb2584aa77 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Thu, 3 Jan 2013 13:22:34 -0800 Subject: [PATCH 22/26] Fix memory leak with checkout tree iterator --- src/checkout.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/checkout.c b/src/checkout.c index 342852792..a10507aaf 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -44,7 +44,6 @@ typedef struct { git_checkout_opts opts; bool opts_free_baseline; char *pfx; - git_iterator *baseline; git_index *index; git_pool pool; git_vector removes; @@ -1241,7 +1240,7 @@ cleanup: git_diff_list_free(data.diff); git_iterator_free(workdir); - git_iterator_free(data.baseline); + git_iterator_free(baseline); git__free(actions); git__free(counts); checkout_data_clear(&data); From d8889d2b64a0a28f33a189216e1d63669b9be206 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Thu, 3 Jan 2013 14:08:53 -0800 Subject: [PATCH 23/26] Fix checkout bug rmv untracked trees from index When checking out with the GIT_CHECKOUT_REMOVE_UNTRACKED option and there was an entire tree in the working directory and in the index that is not in the baseline nor target commit, the tree was correctly(?) removed from the working directory but was not successfully removed from the index. This fixes that and adds a test of the functionality. --- src/checkout.c | 47 ++++++++++++++++++++++++++------------ tests-clar/checkout/head.c | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/src/checkout.c b/src/checkout.c index a10507aaf..261dee112 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -418,6 +418,8 @@ static int checkout_action_with_wd_dir( return checkout_action_common(data, action, delta, wd); } +#define EXPAND_DIRS_FOR_STRATEGY (GIT_CHECKOUT_FORCE | GIT_CHECKOUT_REMOVE_UNTRACKED | GIT_CHECKOUT_REMOVE_IGNORED) + static int checkout_action( checkout_data *data, git_diff_delta *delta, @@ -429,6 +431,7 @@ static int checkout_action( int cmp = -1, act; int (*strcomp)(const char *, const char *) = data->diff->strcomp; int (*pfxcomp)(const char *str, const char *pfx) = data->diff->pfxcomp; + bool expand_dirs = (data->strategy & EXPAND_DIRS_FOR_STRATEGY) != 0; /* move workdir iterator to follow along with deltas */ @@ -449,14 +452,14 @@ static int checkout_action( 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; - } + if (wd->mode == GIT_FILEMODE_TREE && (cmp == 0 || expand_dirs)) { + /* case 2 or untracked wd item that might need removal */ + if (git_iterator_advance_into_directory(workdir, &wd) < 0) + goto fail; + continue; + } + if (cmp == 0) { /* 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; @@ -519,6 +522,26 @@ fail: return -1; } +static int checkout_remaining_wd_items( + checkout_data *data, + git_iterator *workdir, + const git_index_entry *wd, + git_vector *spec) +{ + int error = 0; + bool expand_dirs = (data->strategy & EXPAND_DIRS_FOR_STRATEGY) != 0; + + while (wd && !error) { + if (wd->mode == GIT_FILEMODE_TREE && expand_dirs) + error = git_iterator_advance_into_directory(workdir, &wd); + + else if (!(error = checkout_action_wd_only(data, workdir, wd, spec))) + error = git_iterator_advance(workdir, &wd); + } + + return error; +} + static int checkout_get_actions( uint32_t **actions_ptr, size_t **counts_ptr, @@ -570,13 +593,9 @@ static int checkout_get_actions( counts[CHECKOUT_ACTION__CONFLICT]++; } - while (wditem != NULL) { - error = checkout_action_wd_only(data, workdir, wditem, &pathspec); - if (!error) - error = git_iterator_advance(workdir, &wditem); - if (error < 0) - goto fail; - } + error = checkout_remaining_wd_items(data, workdir, wditem, &pathspec); + if (error < 0) + goto fail; counts[CHECKOUT_ACTION__REMOVE] += data->removes.length; diff --git a/tests-clar/checkout/head.c b/tests-clar/checkout/head.c index aed203a06..8b3099303 100644 --- a/tests-clar/checkout/head.c +++ b/tests-clar/checkout/head.c @@ -1,6 +1,8 @@ #include "clar_libgit2.h" #include "refs.h" #include "repo/repo_helpers.h" +#include "path.h" +#include "fileops.h" static git_repository *g_repo; @@ -20,3 +22,42 @@ void test_checkout_head__orphaned_head_returns_GIT_EORPHANEDHEAD(void) cl_assert_equal_i(GIT_EORPHANEDHEAD, git_checkout_head(g_repo, NULL)); } + +void test_checkout_head__with_index_only_tree(void) +{ + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + git_index *index; + + /* let's start by getting things into a known state */ + + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + cl_git_pass(git_checkout_head(g_repo, &opts)); + + /* now let's stage some new stuff including a new directory */ + + cl_git_pass(git_repository_index(&index, g_repo)); + + p_mkdir("testrepo/newdir", 0777); + cl_git_mkfile("testrepo/newdir/newfile.txt", "new file\n"); + + cl_git_pass(git_index_add_from_workdir(index, "newdir/newfile.txt")); + cl_git_pass(git_index_write(index)); + + cl_assert(git_path_isfile("testrepo/newdir/newfile.txt")); + cl_assert(git_index_get_bypath(index, "newdir/newfile.txt", 0) != NULL); + + git_index_free(index); + + /* okay, so now we have staged this new file; let's see if we can remove */ + + opts.checkout_strategy = GIT_CHECKOUT_FORCE | GIT_CHECKOUT_REMOVE_UNTRACKED; + cl_git_pass(git_checkout_head(g_repo, &opts)); + + cl_git_pass(git_repository_index(&index, g_repo)); + cl_git_pass(git_index_read(index)); /* reload if needed */ + + cl_assert(!git_path_isfile("testrepo/newdir/newfile.txt")); + cl_assert(git_index_get_bypath(index, "newdir/newfile.txt", 0) == NULL); + + git_index_free(index); +} From 1b88faf7aea53a72a4906f48ec30f16f1c157503 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Thu, 3 Jan 2013 14:21:25 -0800 Subject: [PATCH 24/26] Fix oid tostr issue with NULL oid I made a small change to the behavior of this code and apparently got it wrong. Sigh. --- src/oid.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/oid.c b/src/oid.c index bbdd8541b..474129b58 100644 --- a/src/oid.c +++ b/src/oid.c @@ -100,7 +100,10 @@ char *git_oid_tostr(char *out, size_t n, const git_oid *oid) n--; /* allow room for terminating NUL */ - if (n > 0 && oid != NULL) { + if (oid == NULL) + n = 0; + + if (n > 0) { git_oid_fmt(str, oid); if (n > GIT_OID_HEXSZ) n = GIT_OID_HEXSZ; From 7fc00435829d24021a477c6d6413f3d7b3e37e27 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Thu, 3 Jan 2013 15:48:52 -0800 Subject: [PATCH 25/26] Add index API to remove all files in a directory This adds the git_index_remove_directory API plus tests. --- include/git2/index.h | 11 ++++++ src/index.c | 38 ++++++++++++++++++ tests-clar/index/tests.c | 83 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) diff --git a/include/git2/index.h b/include/git2/index.h index fa9a19785..1d21403ad 100644 --- a/include/git2/index.h +++ b/include/git2/index.h @@ -312,6 +312,17 @@ GIT_EXTERN(const git_index_entry *) git_index_get_bypath( */ GIT_EXTERN(int) git_index_remove(git_index *index, const char *path, int stage); +/** + * Remove all entries from the index under a given directory + * + * @param index an existing index object + * @param dir container directory path + * @param stage stage to search + * @return 0 or an error code + */ +GIT_EXTERN(int) git_index_remove_directory( + git_index *index, const char *dir, int stage); + /** * Add or update an index entry from an in-memory struct * diff --git a/src/index.c b/src/index.c index 9f2012b3a..ce902c5ef 100644 --- a/src/index.c +++ b/src/index.c @@ -794,6 +794,44 @@ int git_index_remove(git_index *index, const char *path, int stage) return error; } +int git_index_remove_directory(git_index *index, const char *dir, int stage) +{ + git_buf pfx = GIT_BUF_INIT; + int error = 0; + size_t pos; + git_index_entry *entry; + + if (git_buf_sets(&pfx, dir) < 0 || git_path_to_dir(&pfx) < 0) + return -1; + + git_vector_sort(&index->entries); + + pos = git_index__prefix_position(index, pfx.ptr); + + while (1) { + entry = git_vector_get(&index->entries, pos); + if (!entry || git__prefixcmp(entry->path, pfx.ptr) != 0) + break; + + if (index_entry_stage(entry) != stage) { + ++pos; + continue; + } + + git_tree_cache_invalidate_path(index->tree, entry->path); + + if ((error = git_vector_remove(&index->entries, pos)) < 0) + break; + index_entry_free(entry); + + /* removed entry at 'pos' so we don't need to increment it */ + } + + git_buf_free(&pfx); + + return error; +} + static int index_find(git_index *index, const char *path, int stage) { struct entry_srch_key srch_key; diff --git a/tests-clar/index/tests.c b/tests-clar/index/tests.c index 989734c1b..5c3d4cf41 100644 --- a/tests-clar/index/tests.c +++ b/tests-clar/index/tests.c @@ -290,3 +290,86 @@ void test_index_tests__write_invalid_filename(void) cl_fixture_cleanup("read_tree"); } + +void test_index_tests__remove_entry(void) +{ + git_repository *repo; + git_index *index; + + p_mkdir("index_test", 0770); + + cl_git_pass(git_repository_init(&repo, "index_test", 0)); + cl_git_pass(git_repository_index(&index, repo)); + cl_assert(git_index_entrycount(index) == 0); + + cl_git_mkfile("index_test/hello", NULL); + cl_git_pass(git_index_add_from_workdir(index, "hello")); + cl_git_pass(git_index_write(index)); + + cl_git_pass(git_index_read(index)); /* reload */ + cl_assert(git_index_entrycount(index) == 1); + cl_assert(git_index_get_bypath(index, "hello", 0) != NULL); + + cl_git_pass(git_index_remove(index, "hello", 0)); + cl_git_pass(git_index_write(index)); + + cl_git_pass(git_index_read(index)); /* reload */ + cl_assert(git_index_entrycount(index) == 0); + cl_assert(git_index_get_bypath(index, "hello", 0) == NULL); + + git_index_free(index); + git_repository_free(repo); + cl_fixture_cleanup("index_test"); +} + +void test_index_tests__remove_directory(void) +{ + git_repository *repo; + git_index *index; + + p_mkdir("index_test", 0770); + + cl_git_pass(git_repository_init(&repo, "index_test", 0)); + cl_git_pass(git_repository_index(&index, repo)); + cl_assert_equal_i(0, (int)git_index_entrycount(index)); + + p_mkdir("index_test/a", 0770); + cl_git_mkfile("index_test/a/1.txt", NULL); + cl_git_mkfile("index_test/a/2.txt", NULL); + cl_git_mkfile("index_test/a/3.txt", NULL); + cl_git_mkfile("index_test/b.txt", NULL); + + cl_git_pass(git_index_add_from_workdir(index, "a/1.txt")); + cl_git_pass(git_index_add_from_workdir(index, "a/2.txt")); + cl_git_pass(git_index_add_from_workdir(index, "a/3.txt")); + cl_git_pass(git_index_add_from_workdir(index, "b.txt")); + cl_git_pass(git_index_write(index)); + + cl_git_pass(git_index_read(index)); /* reload */ + cl_assert_equal_i(4, (int)git_index_entrycount(index)); + cl_assert(git_index_get_bypath(index, "a/1.txt", 0) != NULL); + cl_assert(git_index_get_bypath(index, "a/2.txt", 0) != NULL); + cl_assert(git_index_get_bypath(index, "b.txt", 0) != NULL); + + cl_git_pass(git_index_remove(index, "a/1.txt", 0)); + cl_git_pass(git_index_write(index)); + + cl_git_pass(git_index_read(index)); /* reload */ + cl_assert_equal_i(3, (int)git_index_entrycount(index)); + cl_assert(git_index_get_bypath(index, "a/1.txt", 0) == NULL); + cl_assert(git_index_get_bypath(index, "a/2.txt", 0) != NULL); + cl_assert(git_index_get_bypath(index, "b.txt", 0) != NULL); + + cl_git_pass(git_index_remove_directory(index, "a", 0)); + cl_git_pass(git_index_write(index)); + + cl_git_pass(git_index_read(index)); /* reload */ + cl_assert_equal_i(1, (int)git_index_entrycount(index)); + cl_assert(git_index_get_bypath(index, "a/1.txt", 0) == NULL); + cl_assert(git_index_get_bypath(index, "a/2.txt", 0) == NULL); + cl_assert(git_index_get_bypath(index, "b.txt", 0) != NULL); + + git_index_free(index); + git_repository_free(repo); + cl_fixture_cleanup("index_test"); +} From 817d625161f212b86c22733f7dde2f2155a65ac5 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Thu, 3 Jan 2013 16:56:27 -0800 Subject: [PATCH 26/26] Fix checkout of index-only dirs and prefixed paths There are a couple of checkout bugs fixed here. One is with untracked working directory entries that are prefixes of tree entries but not in a meaningful way (i.e. "read" is a prefix of "readme.txt" but doesn't interfere in any way). The second bug is actually a redo of 07edfa0fc640f85f95507c3101e77accd7d2bf0d where directory entries in the index that are not in the diff were not being removed correctly. That fix remedied one case but broke another. --- src/checkout.c | 69 ++++++++++++++++++++++--------------- tests-clar/checkout/index.c | 25 ++++++++++++++ 2 files changed, 66 insertions(+), 28 deletions(-) diff --git a/src/checkout.c b/src/checkout.c index 261dee112..cf0a8b8e7 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -220,19 +220,34 @@ static int checkout_action_wd_only( { bool remove = false; git_checkout_notify_t notify = GIT_CHECKOUT_NOTIFY_NONE; - const git_index_entry *entry; if (!git_pathspec_match_path( pathspec, wd->path, false, workdir->ignore_case)) return 0; /* check if item is tracked in the index but not in the checkout diff */ - if (data->index != NULL && - (entry = git_index_get_bypath(data->index, wd->path, 0)) != NULL) - { - notify = GIT_CHECKOUT_NOTIFY_DIRTY; - remove = ((data->strategy & GIT_CHECKOUT_FORCE) != 0); + if (data->index != NULL) { + if (wd->mode != GIT_FILEMODE_TREE) { + if (git_index_get_bypath(data->index, wd->path, 0) != NULL) { + notify = GIT_CHECKOUT_NOTIFY_DIRTY; + remove = ((data->strategy & GIT_CHECKOUT_FORCE) != 0); + } + } else { + /* for tree entries, we have to see if there are any index + * entries that are contained inside that tree + */ + size_t pos = git_index__prefix_position(data->index, wd->path); + const git_index_entry *e = git_index_get_byindex(data->index, pos); + + if (e != NULL && data->diff->pfxcomp(e->path, wd->path) == 0) { + notify = GIT_CHECKOUT_NOTIFY_DIRTY; + remove = ((data->strategy & GIT_CHECKOUT_FORCE) != 0); + } + } } + + if (notify != GIT_CHECKOUT_NOTIFY_NONE) + /* found in index */; else if (git_iterator_current_is_ignored(workdir)) { notify = GIT_CHECKOUT_NOTIFY_IGNORED; remove = ((data->strategy & GIT_CHECKOUT_REMOVE_IGNORED) != 0); @@ -418,8 +433,6 @@ static int checkout_action_with_wd_dir( return checkout_action_common(data, action, delta, wd); } -#define EXPAND_DIRS_FOR_STRATEGY (GIT_CHECKOUT_FORCE | GIT_CHECKOUT_REMOVE_UNTRACKED | GIT_CHECKOUT_REMOVE_IGNORED) - static int checkout_action( checkout_data *data, git_diff_delta *delta, @@ -431,7 +444,6 @@ static int checkout_action( int cmp = -1, act; int (*strcomp)(const char *, const char *) = data->diff->strcomp; int (*pfxcomp)(const char *str, const char *pfx) = data->diff->pfxcomp; - bool expand_dirs = (data->strategy & EXPAND_DIRS_FOR_STRATEGY) != 0; /* move workdir iterator to follow along with deltas */ @@ -452,18 +464,21 @@ static int checkout_action( if (cmp < 0) { cmp = pfxcomp(delta->old_file.path, wd->path); - if (wd->mode == GIT_FILEMODE_TREE && (cmp == 0 || expand_dirs)) { - /* case 2 or untracked wd item that might need removal */ - if (git_iterator_advance_into_directory(workdir, &wd) < 0) - goto fail; - continue; - } - if (cmp == 0) { - /* 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; + if (wd->mode == GIT_FILEMODE_TREE) { + /* case 2 - entry prefixed by workdir tree */ + if (git_iterator_advance_into_directory(workdir, &wd) < 0) + goto fail; + continue; + } + + /* case 3 maybe - wd contains non-dir where dir expected */ + if (delta->old_file.path[strlen(wd->path)] == '/') { + 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) */ @@ -485,8 +500,7 @@ static int checkout_action( cmp = pfxcomp(wd->path, delta->old_file.path); if (cmp == 0) { /* case 5 */ - size_t pathlen = strlen(delta->old_file.path); - if (wd->path[pathlen] != '/') + if (wd->path[strlen(delta->old_file.path)] != '/') return checkout_action_no_wd(data, delta); if (delta->status == GIT_DELTA_TYPECHANGE) { @@ -529,13 +543,9 @@ static int checkout_remaining_wd_items( git_vector *spec) { int error = 0; - bool expand_dirs = (data->strategy & EXPAND_DIRS_FOR_STRATEGY) != 0; while (wd && !error) { - if (wd->mode == GIT_FILEMODE_TREE && expand_dirs) - error = git_iterator_advance_into_directory(workdir, &wd); - - else if (!(error = checkout_action_wd_only(data, workdir, wd, spec))) + if (!(error = checkout_action_wd_only(data, workdir, wd, spec))) error = git_iterator_advance(workdir, &wd); } @@ -945,7 +955,10 @@ static int checkout_remove_the_old( if ((data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0 && data->index != NULL) { - (void)git_index_remove(data->index, str, 0); + if (str[strlen(str) - 1] == '/') + (void)git_index_remove_directory(data->index, str, 0); + else + (void)git_index_remove(data->index, str, 0); } } diff --git a/tests-clar/checkout/index.c b/tests-clar/checkout/index.c index b1778a422..fe1f6874f 100644 --- a/tests-clar/checkout/index.c +++ b/tests-clar/checkout/index.c @@ -496,3 +496,28 @@ void test_checkout_index__validates_struct_version(void) err = giterr_last(); cl_assert_equal_i(err->klass, GITERR_INVALID); } + +void test_checkout_index__can_update_prefixed_files(void) +{ + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + + cl_git_mkfile("./testrepo/READ", "content\n"); + cl_git_mkfile("./testrepo/README.after", "content\n"); + cl_git_pass(p_mkdir("./testrepo/branch_file", 0777)); + cl_git_pass(p_mkdir("./testrepo/branch_file/contained_dir", 0777)); + cl_git_mkfile("./testrepo/branch_file/contained_file", "content\n"); + cl_git_pass(p_mkdir("./testrepo/branch_file.txt.after", 0777)); + + opts.checkout_strategy = GIT_CHECKOUT_FORCE | GIT_CHECKOUT_REMOVE_UNTRACKED; + + 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"); + test_file_contents("./testrepo/new.txt", "my new file\n"); + + cl_assert(!git_path_exists("testrepo/READ")); + cl_assert(!git_path_exists("testrepo/README.after")); + cl_assert(!git_path_exists("testrepo/branch_file")); + cl_assert(!git_path_exists("testrepo/branch_file.txt.after")); +}