summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/git2/checkout.h261
-rw-r--r--src/checkout.c1111
-rw-r--r--src/checkout.h31
-rw-r--r--src/stash.c3
-rw-r--r--tests-clar/checkout/head.c2
-rw-r--r--tests-clar/checkout/index.c103
-rw-r--r--tests-clar/checkout/tree.c2
-rw-r--r--tests-clar/checkout/typechange.c5
-rw-r--r--tests-clar/reset/hard.c4
9 files changed, 971 insertions, 551 deletions
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.
- *
- * Checkout groups the working directory content into 3 classes of files:
- * (1) files that don't need a change, and files that do need a change
- * that either (2) we are allowed to modifed or (3) we are not. The flags
- * you pass in will decide which files we are allowed to modify.
- *
- * By default, checkout is not allowed to modify any files. Anything
- * needing a change would be considered a conflict.
- *
- * GIT_CHECKOUT_UPDATE_UNMODIFIED means that checkout is allowed to update
- * any file where the working directory content matches the HEAD
- * (e.g. either the files match or the file is absent in both places).
- *
- * GIT_CHECKOUT_UPDATE_MISSING means checkout can create a missing file
- * that exists in the index and does not exist in the working directory.
- * This is usually desirable for initial checkout, etc. Technically, the
- * missing file differs from the HEAD, which is why this is separate.
- *
- * GIT_CHECKOUT_UPDATE_MODIFIED means checkout is allowed to update files
- * where the working directory does not match the HEAD so long as the file
- * actually exists in the HEAD. This option implies UPDATE_UNMODIFIED.
- *
- * GIT_CHECKOUT_UPDATE_UNTRACKED means checkout is allowed to update files
- * even if there is a working directory version that does not exist in the
- * HEAD (i.e. the file was independently created in the workdir). This
- * implies UPDATE_UNMODIFIED | UPDATE_MISSING (but *not* UPDATE_MODIFIED).
- *
- *
- * On top of these three basic strategies, there are some modifiers
- * options that can be applied:
- *
- * If any files need update but are disallowed by the strategy, normally
- * checkout calls the conflict callback (if given) and then aborts.
- * GIT_CHECKOUT_ALLOW_CONFLICTS means it is okay to update the files that
- * are allowed by the strategy even if there are conflicts. The conflict
- * callbacks are still made, but non-conflicting files will be updated.
- *
- * Any unmerged entries in the index are automatically considered conflicts.
- * If you want to proceed anyhow and just skip unmerged entries, you can use
- * GIT_CHECKOUT_SKIP_UNMERGED which is less dangerous than just allowing all
- * conflicts. Alternatively, use GIT_CHECKOUT_USE_OURS to proceed and
- * checkout the stage 2 ("ours") version. GIT_CHECKOUT_USE_THEIRS means to
- * proceed and use the stage 3 ("theirs") version.
- *
- * GIT_CHECKOUT_UPDATE_ONLY means that update is not allowed to create new
- * files or delete old ones, only update existing content. With this
- * flag, files that needs to be created or deleted are not conflicts -
- * they are just skipped. This also skips typechanges to existing files
- * (because the old would have to be removed).
- *
- * GIT_CHECKOUT_REMOVE_UNTRACKED means that files in the working directory
- * that are untracked (and not ignored) will be removed altogether. These
- * untracked files (that do not shadow index entries) are not considered
- * conflicts and would normally be ignored.
+ * 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 examines the differences between the target and expected trees
+ * plus the current 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.
+ * 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.
+ *
+ *
+ * You control the actions checkout takes with one of four base strategies:
+ *
+ * - `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.
+ *
+ * - `GIT_CHECKOUT_SAFE` is like `git checkout` and only applies changes
+ * between the expected 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
+ * 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.
+ *
+ *
+ * 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
@@ -143,34 +159,85 @@ 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 <assert.h>
-#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,355 +682,36 @@ 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,
- checkout_diff_data *data)
-{
- int error;
- git_diff_list *diff = data->diff;
- 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;
-
- /* 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;
-
- if ((error = git_iterator_for_tree_range(&hiter, head, pfx, pfx)) < 0)
- goto fail;
-
- if ((diff->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0 &&
- (error = git_iterator_spoolandsort_push(hiter, true)) < 0)
- goto fail;
-
- if ((error = git_iterator_current(hiter, &he)) < 0)
- goto fail;
-
- git__free(pfx);
- pfx = NULL;
-
- *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;
- }
-
- 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,
unsigned int *actions,
checkout_diff_data *data)
{
+ int error = 0;
git_diff_delta *delta;
size_t i;
+ const char *workdir = git_buf_cstr(data->path);
git_buf_truncate(data->path, data->workdir_len);
- git_vector_foreach(&diff->deltas, i, delta) {
+ git_vector_foreach(&data->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);
- if (error < 0)
+ 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)
return error;
data->completed_steps++;
- report_progress(data, delta->new_file.path);
+ report_progress(data, delta->old_file.path);
}
}
@@ -559,21 +719,20 @@ static int checkout_remove_the_old(
}
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) {
+ git_vector_foreach(&data->diff->deltas, i, delta) {
if (actions[i] & CHECKOUT_ACTION__UPDATE_BLOB) {
- int error = checkout_blob(data, &delta->old_file);
+ int error = checkout_blob(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);
}
}
@@ -581,81 +740,103 @@ static int checkout_create_the_new(
}
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);
-
- GITERR_CHECK_VERSION(opts, GIT_CHECKOUT_OPTS_VERSION, "git_checkout_opts");
+ memset(&data, 0, sizeof(data));
- 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)
+ /* 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;
- if ((error = git_buf_puts(&workdir, git_repository_workdir(repo))) < 0)
+ if ((error = git_buf_puts(&workdir, git_repository_workdir(data.repo))) < 0)
goto cleanup;
- normalize_options(&checkout_opts, opts);
+ data.opts = opts;
+ data.pfx = pathspec_pfx;
+ data.path = &workdir;
+ data.workdir_len = git_buf_len(&workdir);
- /* Checkout is best performed with up to four passes through the diff.
+ /* 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 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
+ * 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.
*/
-
- memset(&data, 0, sizeof(data));
- data.path = &workdir;
- data.workdir_len = git_buf_len(&workdir);
- data.repo = repo;
- data.diff = diff;
- data.opts = &checkout_opts;
-
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 */
-
- /* 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);
+ 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);
- 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;
+
+ if ((error = checkout_lookup_head_tree(&head, repo)) < 0)
+ return error;
- git_reference_free(head);
- git_object_free(tree);
+ 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);