Commit cfc7b835 by Vicent Martí

Merge pull request #1118 from arrbee/fix-reset-hard

Fix checkout corner cases, index handling, and reset hard behavior
parents 60406162 817d6251
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.
...@@ -9,8 +9,7 @@ ...@@ -9,8 +9,7 @@
#include "common.h" #include "common.h"
#include "types.h" #include "types.h"
#include "indexer.h" #include "diff.h"
#include "strarray.h"
/** /**
* @file git2/checkout.h * @file git2/checkout.h
...@@ -24,105 +23,113 @@ GIT_BEGIN_DECL ...@@ -24,105 +23,113 @@ GIT_BEGIN_DECL
/** /**
* Checkout behavior flags * Checkout behavior flags
* *
* These flags control what checkout does with files. Pass in a * In libgit2, checkout is used to update the working directory and index
* combination of these values OR'ed together. If you just pass zero * to match a target tree. Unlike git checkout, it does not move the HEAD
* (i.e. no flags), then you are effectively doing a "dry run" where no * commit for you - use `git_repository_set_head` or the like to do that.
* files will be modified. *
* * Checkout looks at (up to) four things: the "target" tree you want to
* Checkout groups the working directory content into 3 classes of files: * check out, the "baseline" tree of what was checked out previously, the
* (1) files that don't need a change, and files that do need a change * working directory for actual files, and the index for staged changes.
* 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. * You give checkout one of four strategies for update:
* *
* By default, checkout is not allowed to modify any files. Anything * - `GIT_CHECKOUT_NONE` is a dry-run strategy that checks for conflicts,
* needing a change would be considered a conflict. * etc., but doesn't make any actual changes.
* *
* GIT_CHECKOUT_UPDATE_UNMODIFIED means that checkout is allowed to update * - `GIT_CHECKOUT_FORCE` is at the opposite extreme, taking any action to
* any file where the working directory content matches the HEAD * make the working directory match the target (including potentially
* (e.g. either the files match or the file is absent in both places). * discarding modified files).
* *
* GIT_CHECKOUT_UPDATE_MISSING means checkout can create a missing file * In between those are `GIT_CHECKOUT_SAFE` and `GIT_CHECKOUT_SAFE_CREATE`
* that exists in the index and does not exist in the working directory. * both of which only make modifications that will not lose changes.
* This is usually desirable for initial checkout, etc. Technically, the *
* missing file differs from the HEAD, which is why this is separate. * | target == baseline | target != baseline |
* * ---------------------|-----------------------|----------------------|
* GIT_CHECKOUT_UPDATE_MODIFIED means checkout is allowed to update files * workdir == baseline | no action | create, update, or |
* where the working directory does not match the HEAD so long as the file * | | delete file |
* actually exists in the HEAD. This option implies UPDATE_UNMODIFIED. * ---------------------|-----------------------|----------------------|
* * workdir exists and | no action | conflict (notify |
* GIT_CHECKOUT_UPDATE_UNTRACKED means checkout is allowed to update files * is != baseline | notify dirty MODIFIED | and cancel checkout) |
* 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 * workdir missing, | create if SAFE_CREATE | create file |
* implies UPDATE_UNMODIFIED | UPDATE_MISSING (but *not* UPDATE_MODIFIED). * baseline present | notify dirty DELETED | |
* * ---------------------|-----------------------|----------------------|
* *
* On top of these three basic strategies, there are some modifiers * The only difference between SAFE and SAFE_CREATE is that SAFE_CREATE
* options that can be applied: * 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.
* 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 * To emulate `git checkout`, use `GIT_CHECKOUT_SAFE` with a checkout
* are allowed by the strategy even if there are conflicts. The conflict * notification callback (see below) that displays information about dirty
* callbacks are still made, but non-conflicting files will be updated. * files. The default behavior will cancel checkout on conflicts.
* *
* Any unmerged entries in the index are automatically considered conflicts. * To emulate `git checkout-index`, use `GIT_CHECKOUT_SAFE_CREATE` with a
* If you want to proceed anyhow and just skip unmerged entries, you can use * notification callback that cancels the operation if a dirty-but-existing
* GIT_CHECKOUT_SKIP_UNMERGED which is less dangerous than just allowing all * file is found in the working directory. This core git command isn't
* conflicts. Alternatively, use GIT_CHECKOUT_USE_OURS to proceed and * quite "force" but is sensitive about some types of changes.
* checkout the stage 2 ("ours") version. GIT_CHECKOUT_USE_THEIRS means to *
* proceed and use the stage 3 ("theirs") version. * To emulate `git checkout -f`, use `GIT_CHECKOUT_FORCE`.
* *
* GIT_CHECKOUT_UPDATE_ONLY means that update is not allowed to create new * To emulate `git clone` use `GIT_CHECKOUT_SAFE_CREATE` in the options.
* 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 * There are some additional flags to modified the behavior of checkout:
* (because the old would have to be removed). *
* * - GIT_CHECKOUT_ALLOW_CONFLICTS makes SAFE mode apply safe file updates
* GIT_CHECKOUT_REMOVE_UNTRACKED means that files in the working directory * even if there are conflicts (instead of cancelling the checkout).
* that are untracked (and not ignored) will be removed altogether. These *
* untracked files (that do not shadow index entries) are not considered * - GIT_CHECKOUT_REMOVE_UNTRACKED means remove untracked files (i.e. not
* conflicts and would normally be ignored. * in target, baseline, or index, and not ignored) from the working dir.
* *
* * - GIT_CHECKOUT_REMOVE_IGNORED means remove ignored files (that are also
* Checkout is "semi-atomic" as in it will go through the work to be done * unrtacked) from the working directory as well.
* 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 * - GIT_CHECKOUT_UPDATE_ONLY means to only update the content of files that
* action before any updates are made. Despite this, if a second process * already exist. Files will not be created nor deleted. This just skips
* is modifying the filesystem while checkout is running, it can't * applying adds, deletes, and typechanges.
* guarantee that the choices is makes while initially examining the *
* filesystem are still going to be correct as it applies them. * - 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 { typedef enum {
GIT_CHECKOUT_DEFAULT = 0, /** default is a dry run, no actual updates */ GIT_CHECKOUT_NONE = 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),
/** Allow safe updates that cannot overwrite uncommited data */ /** Allow safe updates that cannot overwrite uncommited data */
GIT_CHECKOUT_SAFE = GIT_CHECKOUT_SAFE = (1u << 0),
(GIT_CHECKOUT_UPDATE_UNMODIFIED | GIT_CHECKOUT_UPDATE_MISSING),
/** Allow update of entries in working dir that are modified from HEAD. */
GIT_CHECKOUT_UPDATE_MODIFIED = (1u << 2),
/** Update existing untracked files that are now present in the index. */ /** Allow safe updates plus creation of missing files */
GIT_CHECKOUT_UPDATE_UNTRACKED = (1u << 3), GIT_CHECKOUT_SAFE_CREATE = (1u << 1),
/** Allow all updates to force working directory to look like index */ /** Allow all updates to force working directory to look like index */
GIT_CHECKOUT_FORCE = GIT_CHECKOUT_FORCE = (1u << 2),
(GIT_CHECKOUT_SAFE | GIT_CHECKOUT_UPDATE_MODIFIED | GIT_CHECKOUT_UPDATE_UNTRACKED),
/** 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), GIT_CHECKOUT_ALLOW_CONFLICTS = (1u << 4),
/** Remove untracked files not in index (that are not ignored) */ /** Remove untracked files not in index (that are not ignored) */
GIT_CHECKOUT_REMOVE_UNTRACKED = (1u << 5), 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 */ /** Only update existing files, don't create new ones */
GIT_CHECKOUT_UPDATE_ONLY = (1u << 6), 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 << 9),
/** /**
* THE FOLLOWING OPTIONS ARE NOT YET IMPLEMENTED * THE FOLLOWING OPTIONS ARE NOT YET IMPLEMENTED
...@@ -143,55 +150,91 @@ typedef enum { ...@@ -143,55 +150,91 @@ typedef enum {
} git_checkout_strategy_t; } git_checkout_strategy_t;
/** /**
* Checkout notification flags
*
* Checkout will invoke an options notification callback (`notify_cb`) for
* certain cases - you pick which ones via `notify_flags`:
*
* - GIT_CHECKOUT_NOTIFY_CONFLICT invokes checkout on conflicting paths.
*
* - GIT_CHECKOUT_NOTIFY_DIRTY notifies about "dirty" files, i.e. those that
* do not need an update but no longer match the baseline. Core git
* displays these files when checkout runs, but won't stop the checkout.
*
* - GIT_CHECKOUT_NOTIFY_UPDATED sends notification for any file changed.
*
* - GIT_CHECKOUT_NOTIFY_UNTRACKED notifies about untracked files.
*
* - GIT_CHECKOUT_NOTIFY_IGNORED notifies about ignored files.
*
* Returning a non-zero value from this callback will cancel the checkout.
* Notification callbacks are made prior to modifying any files on disk.
*/
typedef enum {
GIT_CHECKOUT_NOTIFY_NONE = 0,
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_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 * Checkout options structure
* *
* Use zeros to indicate default settings. * Zero out for defaults. Initialize with `GIT_CHECKOUT_OPTS_INIT` macro to
* This needs to be initialized with the `GIT_CHECKOUT_OPTS_INIT` macro: * correctly set the `version` field. E.g.
* *
* git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; * git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
*/ */
typedef struct git_checkout_opts { typedef struct git_checkout_opts {
unsigned int version; unsigned int version;
unsigned int checkout_strategy; /** default will be a dry run */ unsigned int checkout_strategy; /** default will be a dry run */
int disable_filters; /** don't apply filters like CRLF conversion */ int disable_filters; /** don't apply filters like CRLF conversion */
int dir_mode; /** default is 0755 */ unsigned int dir_mode; /** default is 0755 */
int file_mode; /** default is 0644 or 0755 as dictated by blob */ 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 */ int file_open_flags; /** default is O_CREAT | O_TRUNC | O_WRONLY */
/** Optional callback made on files where the index differs from the unsigned int notify_flags; /** see `git_checkout_notify_t` above */
* working directory but the rules do not allow update. Return a git_checkout_notify_cb notify_cb;
* non-zero value to abort the checkout. All such callbacks will be void *notify_payload;
* made before any changes are made to the working directory.
*/
int (*conflict_cb)(
const char *conflicting_path,
const git_oid *index_oid,
unsigned int index_mode,
unsigned int wd_mode,
void *payload);
void *conflict_payload;
/* Optional callback to notify the consumer of checkout progress. */ /* Optional callback to notify the consumer of checkout progress. */
void (*progress_cb)( git_checkout_progress_cb progress_cb;
const char *path,
size_t completed_steps,
size_t total_steps,
void *payload);
void *progress_payload; void *progress_payload;
/** When not zeroed out, array of fnmatch patterns specifying which /** When not zeroed out, array of fnmatch patterns specifying which
* paths should be taken into account, otherwise all files. * paths should be taken into account, otherwise all files.
*/ */
git_strarray paths; git_strarray paths;
git_tree *baseline; /** expected content of workdir, defaults to HEAD */
} git_checkout_opts; } git_checkout_opts;
#define GIT_CHECKOUT_OPTS_VERSION 1 #define GIT_CHECKOUT_OPTS_VERSION 1
#define GIT_CHECKOUT_OPTS_INIT {GIT_CHECKOUT_OPTS_VERSION} #define GIT_CHECKOUT_OPTS_INIT {GIT_CHECKOUT_OPTS_VERSION}
/** /**
* Updates files in the index and the working tree to match the content of the * Updates files in the index and the working tree to match the content of
* commit pointed at by HEAD. * the commit pointed at by HEAD.
* *
* @param repo repository to check out (must be non-bare) * @param repo repository to check out (must be non-bare)
* @param opts specifies checkout options (may be NULL) * @param opts specifies checkout options (may be NULL)
......
...@@ -313,6 +313,17 @@ GIT_EXTERN(const git_index_entry *) git_index_get_bypath( ...@@ -313,6 +313,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); 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 * Add or update an index entry from an in-memory struct
* *
* If a previous index entry exists that has the same path and stage * If a previous index entry exists that has the same path and stage
......
...@@ -504,6 +504,24 @@ GIT_EXTERN(int) git_submodule_status( ...@@ -504,6 +504,24 @@ GIT_EXTERN(int) git_submodule_status(
unsigned int *status, unsigned int *status,
git_submodule *submodule); 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 GIT_END_DECL
#endif #endif
...@@ -572,8 +572,10 @@ static int collect_attr_files( ...@@ -572,8 +572,10 @@ static int collect_attr_files(
error = git_futils_find_system_file(&dir, GIT_ATTR_FILE_SYSTEM); error = git_futils_find_system_file(&dir, GIT_ATTR_FILE_SYSTEM);
if (!error) if (!error)
error = push_attr_file(repo, files, NULL, dir.ptr); error = push_attr_file(repo, files, NULL, dir.ptr);
else if (error == GIT_ENOTFOUND) else if (error == GIT_ENOTFOUND) {
giterr_clear();
error = 0; error = 0;
}
} }
cleanup: cleanup:
......
...@@ -7,36 +7,636 @@ ...@@ -7,36 +7,636 @@
#include <assert.h> #include <assert.h>
#include "git2/checkout.h" #include "checkout.h"
#include "git2/repository.h" #include "git2/repository.h"
#include "git2/refs.h" #include "git2/refs.h"
#include "git2/tree.h" #include "git2/tree.h"
#include "git2/blob.h" #include "git2/blob.h"
#include "git2/config.h" #include "git2/config.h"
#include "git2/diff.h" #include "git2/diff.h"
#include "git2/submodule.h"
#include "common.h"
#include "refs.h" #include "refs.h"
#include "buffer.h"
#include "repository.h" #include "repository.h"
#include "filter.h" #include "filter.h"
#include "blob.h" #include "blob.h"
#include "diff.h" #include "diff.h"
#include "pathspec.h" #include "pathspec.h"
/* See docs/checkout-internals.md for more information */
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__DEFER_REMOVE = 16,
CHECKOUT_ACTION__REMOVE_AND_UPDATE =
(CHECKOUT_ACTION__UPDATE_BLOB | CHECKOUT_ACTION__REMOVE),
};
typedef struct { typedef struct {
git_repository *repo; git_repository *repo;
git_diff_list *diff; git_diff_list *diff;
git_checkout_opts *opts; git_checkout_opts opts;
git_buf *path; bool opts_free_baseline;
char *pfx;
git_index *index;
git_pool pool;
git_vector removes;
git_buf path;
size_t workdir_len; size_t workdir_len;
bool can_symlink; unsigned int strategy;
int error; int can_symlink;
bool reload_submodules;
size_t total_steps; size_t total_steps;
size_t completed_steps; size_t completed_steps;
} checkout_diff_data; } checkout_data;
static int checkout_notify(
checkout_data *data,
git_checkout_notify_t why,
const git_diff_delta *delta,
const git_index_entry *wditem)
{
git_diff_file wdfile;
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 (wditem) {
memset(&wdfile, 0, sizeof(wdfile));
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;
workdir = &wdfile;
}
if (delta) {
switch (delta->status) {
case GIT_DELTA_UNMODIFIED:
case GIT_DELTA_MODIFIED:
case GIT_DELTA_TYPECHANGE:
default:
baseline = &delta->old_file;
target = &delta->new_file;
break;
case GIT_DELTA_ADDED:
case GIT_DELTA_IGNORED:
case GIT_DELTA_UNTRACKED:
target = &delta->new_file;
break;
case GIT_DELTA_DELETED:
baseline = &delta->old_file;
break;
}
}
return data->opts.notify_cb(
why, delta ? delta->old_file.path : wditem->path,
baseline, target, workdir, data->opts.notify_payload);
}
static bool checkout_is_workdir_modified(
checkout_data *data,
const git_diff_file *baseitem,
const git_index_entry *wditem)
{
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.
*/
if (baseitem->size && wditem->file_size != baseitem->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(&baseitem->oid, &oid) != 0);
}
#define CHECKOUT_ACTION_IF(FLAG,YES,NO) \
((data->strategy & GIT_CHECKOUT_##FLAG) ? CHECKOUT_ACTION__##YES : CHECKOUT_ACTION__##NO)
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 (action <= 0)
return action;
if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0)
action = (action & ~CHECKOUT_ACTION__REMOVE);
if ((action & CHECKOUT_ACTION__UPDATE_BLOB) != 0) {
if (S_ISGITLINK(delta->new_file.mode))
action = (action & ~CHECKOUT_ACTION__UPDATE_BLOB) |
CHECKOUT_ACTION__UPDATE_SUBMODULE;
notify = GIT_CHECKOUT_NOTIFY_UPDATED;
}
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_action_no_wd(
checkout_data *data,
const git_diff_delta *delta)
{
int action = CHECKOUT_ACTION__NONE;
switch (delta->status) {
case GIT_DELTA_UNMODIFIED: /* case 12 */
if (checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, NULL))
return GIT_EUSER;
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;
}
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 remove = false;
git_checkout_notify_t notify = GIT_CHECKOUT_NOTIFY_NONE;
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) {
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);
}
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 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,
const git_index_entry *wd)
{
int action = CHECKOUT_ACTION__NONE;
switch (delta->status) {
case GIT_DELTA_UNMODIFIED: /* case 14/15 or 33 */
if (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->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;
}
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->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) */
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 */
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;
}
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 - 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) */
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 (wd->path[strlen(delta->old_file.path)] != '/')
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);
}
/* case 6 - wd is after delta */
return checkout_action_no_wd(data, delta);
}
fail:
*wditem_ptr = NULL;
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;
while (wd && !error) {
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,
checkout_data *data,
git_iterator *workdir)
{
int error = 0;
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;
if (data->opts.paths.count > 0 &&
git_pathspec_init(&pathspec, &data->opts.paths, &pathpool) < 0)
return -1;
if ((error = git_iterator_current(workdir, &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 act = checkout_action(data, delta, workdir, &wditem, &pathspec);
if (act < 0) {
error = act;
goto fail;
}
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]++;
}
error = checkout_remaining_wd_items(data, workdir, wditem, &pathspec);
if (error < 0)
goto fail;
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_pathspec_free(&pathspec);
git_pool_clear(&pathpool);
return 0;
fail:
*counts_ptr = NULL;
git__free(counts);
*actions_ptr = NULL;
git__free(actions);
git_pathspec_free(&pathspec);
git_pool_clear(&pathpool);
return error;
}
static int buffer_to_file( static int buffer_to_file(
struct stat *st,
git_buf *buffer, git_buf *buffer,
const char *path, const char *path,
mode_t dir_mode, mode_t dir_mode,
...@@ -57,6 +657,9 @@ static int buffer_to_file( ...@@ -57,6 +657,9 @@ static int buffer_to_file(
giterr_set(GITERR_OS, "Could not write to '%s'", path); giterr_set(GITERR_OS, "Could not write to '%s'", path);
(void)p_close(fd); (void)p_close(fd);
} else { } else {
if ((error = p_fstat(fd, st)) < 0)
giterr_set(GITERR_OS, "Error while statting '%s'", path);
if ((error = p_close(fd)) < 0) if ((error = p_close(fd)) < 0)
giterr_set(GITERR_OS, "Error while closing '%s'", path); giterr_set(GITERR_OS, "Error while closing '%s'", path);
} }
...@@ -70,6 +673,7 @@ static int buffer_to_file( ...@@ -70,6 +673,7 @@ static int buffer_to_file(
} }
static int blob_content_to_file( static int blob_content_to_file(
struct stat *st,
git_blob *blob, git_blob *blob,
const char *path, const char *path,
mode_t entry_filemode, mode_t entry_filemode,
...@@ -112,7 +716,12 @@ static int blob_content_to_file( ...@@ -112,7 +716,12 @@ static int blob_content_to_file(
file_mode = entry_filemode; file_mode = entry_filemode;
error = buffer_to_file( 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: cleanup:
git_filters_free(&filters); git_filters_free(&filters);
...@@ -124,7 +733,7 @@ cleanup: ...@@ -124,7 +733,7 @@ cleanup:
} }
static int blob_content_to_link( static int blob_content_to_link(
git_blob *blob, const char *path, bool can_symlink) struct stat *st, git_blob *blob, const char *path, int can_symlink)
{ {
git_buf linktarget = GIT_BUF_INIT; git_buf linktarget = GIT_BUF_INIT;
int error; int error;
...@@ -132,448 +741,275 @@ static int blob_content_to_link( ...@@ -132,448 +741,275 @@ static int blob_content_to_link(
if ((error = git_blob__getbuf(&linktarget, blob)) < 0) if ((error = git_blob__getbuf(&linktarget, blob)) < 0)
return error; return error;
if (can_symlink) if (can_symlink) {
error = p_symlink(git_buf_cstr(&linktarget), path); if ((error = p_symlink(git_buf_cstr(&linktarget), path)) < 0)
else giterr_set(GITERR_CHECKOUT, "Could not create symlink %s\n", path);
} else {
error = git_futils_fake_symlink(git_buf_cstr(&linktarget), path); error = git_futils_fake_symlink(git_buf_cstr(&linktarget), path);
git_buf_free(&linktarget);
return error;
}
static int checkout_submodule(
checkout_diff_data *data,
const git_diff_file *file)
{
/* Until submodules are supported, UPDATE_ONLY means do nothing here */
if ((data->opts->checkout_strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0)
return 0;
if (git_futils_mkdir(
file->path, git_repository_workdir(data->repo),
data->opts->dir_mode, GIT_MKDIR_PATH) < 0)
return -1;
/* TODO: Support checkout_strategy options. Two circumstances:
* 1 - submodule already checked out, but we need to move the HEAD
* to the new OID, or
* 2 - submodule not checked out and we should recursively check it out
*
* Checkout will not execute a pull on the submodule, but a clone
* command should probably be able to. Do we need a submodule callback?
*/
return 0;
}
static void report_progress(
checkout_diff_data *data,
const char *path)
{
if (data->opts->progress_cb)
data->opts->progress_cb(
path, data->completed_steps, data->total_steps,
data->opts->progress_payload);
}
static int checkout_blob(
checkout_diff_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)
return -1;
if ((error = git_blob_lookup(&blob, data->repo, &file->oid)) < 0)
return error;
if (S_ISLNK(file->mode))
error = blob_content_to_link(
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);
git_blob_free(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; if (!error) {
} if ((error = p_lstat(path, st)) < 0)
giterr_set(GITERR_CHECKOUT, "Could not stat symlink %s", path);
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) { st->st_mode = GIT_FILEMODE_LINK;
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) git_buf_free(&linktarget);
action = (action & ~CHECKOUT_ACTION__REMOVE);
return action; return error;
} }
static int checkout_get_actions( static int checkout_update_index(
uint32_t **actions_ptr, checkout_data *data,
size_t **counts_ptr, const git_diff_file *file,
checkout_diff_data *data) struct stat *st)
{ {
int error; git_index_entry entry;
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 (!data->index)
if (((error = git_repository_head_tree(&head, data->repo)) < 0) && return 0;
!(error == GIT_ENOTFOUND || error == GIT_EORPHANEDHEAD))
return -1;
if ((error = git_iterator_for_tree_range(&hiter, head, pfx, pfx)) < 0) memset(&entry, 0, sizeof(entry));
goto fail; entry.path = (char *)file->path; /* cast to prevent warning */
git_index_entry__init_from_stat(&entry, st);
git_oid_cpy(&entry.oid, &file->oid);
if ((diff->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0 && return git_index_add(data->index, &entry);
(error = git_iterator_spoolandsort_push(hiter, true)) < 0) }
goto fail;
if ((error = git_iterator_current(hiter, &he)) < 0) static int checkout_submodule(
goto fail; checkout_data *data,
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)
return 0;
git__free(pfx); if ((error = git_futils_mkdir(
pfx = NULL; file->path, git_repository_workdir(data->repo),
data->opts.dir_mode, GIT_MKDIR_PATH)) < 0)
return error;
*counts_ptr = counts = git__calloc(CHECKOUT_ACTION__MAX+1, sizeof(size_t)); if ((error = git_submodule_lookup(&sm, data->repo, file->path)) < 0)
*actions_ptr = actions = git__calloc(diff->deltas.length, sizeof(uint32_t)); return error;
if (!counts || !actions) {
error = -1;
goto fail;
}
git_vector_foreach(&diff->deltas, i, delta) { /* TODO: Support checkout_strategy options. Two circumstances:
int cmp = -1, act; * 1 - submodule already checked out, but we need to move the HEAD
* to the new OID, or
* 2 - submodule not checked out and we should recursively check it out
*
* Checkout will not execute a pull on the submodule, but a clone
* command should probably be able to. Do we need a submodule callback?
*/
/* try to track HEAD entries parallel to deltas */ /* update the index unless prevented */
while (he) { if ((data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0) {
cmp = S_ISDIR(delta->new_file.mode) ? struct stat st;
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); git_buf_truncate(&data->path, data->workdir_len);
if (git_buf_puts(&data->path, file->path) < 0)
return -1;
if (act < 0) { if ((error = p_stat(git_buf_cstr(&data->path), &st)) < 0) {
error = act; giterr_set(
goto fail; GITERR_CHECKOUT, "Could not stat submodule %s\n", file->path);
return error;
} }
if (!cmp && git_iterator_advance(hiter, &he) < 0) st.st_mode = GIT_FILEMODE_COMMIT;
he = NULL;
actions[i] = act;
if (act & CHECKOUT_ACTION__REMOVE) error = checkout_update_index(data, file, &st);
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 && return error;
(data->opts->checkout_strategy & GIT_CHECKOUT_ALLOW_CONFLICTS) == 0) }
{
giterr_set(GITERR_CHECKOUT, "%d conflicts prevent checkout", static void report_progress(
(int)counts[CHECKOUT_ACTION__CONFLICT]); checkout_data *data,
goto fail; const char *path)
{
if (data->opts.progress_cb)
data->opts.progress_cb(
path, data->completed_steps, data->total_steps,
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;
} }
git_iterator_free(hiter); /* only safe for update if this is the same type of file */
git_tree_free(head); if ((st.st_mode & ~0777) == (expected_mode & ~0777))
return 1;
return 0; return 0;
}
fail: static int checkout_blob(
*counts_ptr = NULL; checkout_data *data,
git__free(counts); const git_diff_file *file)
*actions_ptr = NULL; {
git__free(actions); int error = 0;
git_blob *blob;
struct stat st;
git_iterator_free(hiter); git_buf_truncate(&data->path, data->workdir_len);
git_tree_free(head); if (git_buf_puts(&data->path, file->path) < 0)
git__free(pfx); return -1;
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;
if (S_ISLNK(file->mode))
error = blob_content_to_link(
&st, blob, git_buf_cstr(&data->path), data->can_symlink);
else
error = blob_content_to_file(
&st, 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;
}
/* update the index unless prevented */
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;
} }
static int checkout_remove_the_old( static int checkout_remove_the_old(
git_diff_list *diff,
unsigned int *actions, unsigned int *actions,
checkout_diff_data *data) checkout_data *data)
{ {
int error = 0;
git_diff_delta *delta; git_diff_delta *delta;
const char *str;
size_t i; size_t i;
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(&diff->deltas, i, delta) { git_vector_foreach(&data->diff->deltas, i, delta) {
if (actions[i] & CHECKOUT_ACTION__REMOVE) { if (actions[i] & CHECKOUT_ACTION__REMOVE) {
int error = git_futils_rmdir_r( error = git_futils_rmdir_r(delta->old_file.path, workdir, flg);
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) if (error < 0)
return error; return error;
data->completed_steps++; data->completed_steps++;
report_progress(data, delta->new_file.path); 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);
}
}
}
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);
if ((data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0 &&
data->index != NULL)
{
if (str[strlen(str) - 1] == '/')
(void)git_index_remove_directory(data->index, str, 0);
else
(void)git_index_remove(data->index, str, 0);
} }
} }
return 0; 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);
assert(false);
return 0;
#endif
}
static int checkout_create_the_new( static int checkout_create_the_new(
git_diff_list *diff,
unsigned int *actions, unsigned int *actions,
checkout_diff_data *data) checkout_data *data)
{ {
int error = 0;
git_diff_delta *delta; git_diff_delta *delta;
size_t i; size_t i;
git_vector_foreach(&diff->deltas, i, delta) { 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) { if (actions[i] & CHECKOUT_ACTION__UPDATE_BLOB) {
int error = checkout_blob(data, &delta->old_file); error = checkout_blob(data, &delta->new_file);
if (error < 0) if (error < 0)
return error; return error;
data->completed_steps++; data->completed_steps++;
report_progress(data, delta->old_file.path); report_progress(data, delta->new_file.path);
} }
} }
...@@ -581,103 +1017,247 @@ static int checkout_create_the_new( ...@@ -581,103 +1017,247 @@ static int checkout_create_the_new(
} }
static int checkout_create_submodules( static int checkout_create_submodules(
git_diff_list *diff,
unsigned int *actions, unsigned int *actions,
checkout_diff_data *data) checkout_data *data)
{ {
int error = 0;
git_diff_delta *delta; git_diff_delta *delta;
size_t i; size_t i;
git_vector_foreach(&diff->deltas, i, delta) { /* 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
* 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) { 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) if (error < 0)
return error; return error;
data->completed_steps++; data->completed_steps++;
report_progress(data, delta->old_file.path); report_progress(data, delta->new_file.path);
} }
} }
return 0; /* final reload once submodules have been updated */
return git_submodule_reload_all(data->repo);
} }
int git_checkout_index( static int checkout_lookup_head_tree(git_tree **out, git_repository *repo)
git_repository *repo,
git_index *index,
git_checkout_opts *opts)
{ {
git_diff_list *diff = NULL; int error = 0;
git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; git_reference *ref = NULL;
git_checkout_opts checkout_opts; git_object *head;
checkout_diff_data data;
git_buf workdir = GIT_BUF_INIT; if (!(error = git_repository_head(&ref, repo)) &&
uint32_t *actions = NULL; !(error = git_reference_peel(&head, ref, GIT_OBJ_TREE)))
size_t *counts = NULL; *out = (git_tree *)head;
int error;
git_reference_free(ref);
assert(repo); return error;
}
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);
GITERR_CHECK_VERSION(opts, GIT_CHECKOUT_OPTS_VERSION, "git_checkout_opts"); git_index_free(data->index);
data->index = NULL;
}
static int checkout_data_init(
checkout_data *data,
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));
if (!repo) {
giterr_set(GITERR_CHECKOUT, "Cannot checkout nothing");
return -1;
}
if ((error = git_repository__ensure_not_bare(repo, "checkout")) < 0) if ((error = git_repository__ensure_not_bare(repo, "checkout")) < 0)
return error; return error;
diff_opts.flags = if ((error = git_repository_config__weakptr(&cfg, repo)) < 0)
GIT_DIFF_INCLUDE_UNMODIFIED | GIT_DIFF_INCLUDE_UNTRACKED | return error;
GIT_DIFF_INCLUDE_TYPECHANGE | GIT_DIFF_SKIP_BINARY_CHECK;
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));
/* 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)
data->opts.checkout_strategy |= GIT_CHECKOUT_SAFE;
data->strategy = data->opts.checkout_strategy;
if (opts && opts->paths.count > 0) /* opts->disable_filters is false by default */
diff_opts.pathspec = opts->paths;
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_diff_index_to_workdir(&diff, repo, index, &diff_opts)) < 0) 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; goto cleanup;
if ((error = git_buf_puts(&workdir, git_repository_workdir(repo))) < 0) 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, 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; goto cleanup;
normalize_options(&checkout_opts, opts); /* Handle case insensitivity for baseline if necessary */
if (workdir->ignore_case && !baseline->ignore_case) {
if ((error = git_iterator_spoolandsort_push(baseline, true)) < 0)
goto cleanup;
}
/* Checkout is best performed with up to four passes through the diff. /* Generate baseline-to-target diff which will include an entry for
* * every possible update that might need to be made.
* 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.
*/ */
if ((error = git_diff__from_iterators(
&data.diff, data.repo, baseline, target, &diff_opts)) < 0)
goto cleanup;
memset(&data, 0, sizeof(data)); /* Loop through diff (and working directory iterator) building a list of
data.path = &workdir; * actions to be taken, plus look for conflicts and send notifications.
data.workdir_len = git_buf_len(&workdir); */
data.repo = repo; if ((error = checkout_get_actions(&actions, &counts, &data, workdir)) < 0)
data.diff = diff;
data.opts = &checkout_opts;
if ((error = checkout_get_actions(&actions, &counts, &data)) < 0)
goto cleanup; goto cleanup;
data.total_steps = counts[CHECKOUT_ACTION__REMOVE] + data.total_steps = counts[CHECKOUT_ACTION__REMOVE] +
counts[CHECKOUT_ACTION__UPDATE_BLOB] + counts[CHECKOUT_ACTION__UPDATE_BLOB] +
counts[CHECKOUT_ACTION__UPDATE_SUBMODULE]; counts[CHECKOUT_ACTION__UPDATE_SUBMODULE];
if ((error = retrieve_symlink_caps(repo, &data.can_symlink)) < 0)
goto cleanup;
report_progress(&data, NULL); /* establish 0 baseline */ report_progress(&data, NULL); /* establish 0 baseline */
/* 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 && 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; goto cleanup;
if (counts[CHECKOUT_ACTION__UPDATE_BLOB] > 0 && 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; goto cleanup;
if (counts[CHECKOUT_ACTION__UPDATE_SUBMODULE] > 0 && if (counts[CHECKOUT_ACTION__UPDATE_SUBMODULE] > 0 &&
(error = checkout_create_submodules(diff, actions, &data)) < 0) (error = checkout_create_submodules(actions, &data)) < 0)
goto cleanup; goto cleanup;
assert(data.completed_steps == data.total_steps); assert(data.completed_steps == data.total_steps);
...@@ -686,10 +1266,40 @@ cleanup: ...@@ -686,10 +1266,40 @@ cleanup:
if (error == GIT_EUSER) if (error == GIT_EUSER)
giterr_clear(); 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(baseline);
git__free(actions); git__free(actions);
git__free(counts); git__free(counts);
git_diff_list_free(diff); checkout_data_clear(&data);
git_buf_free(&workdir);
return error;
}
int git_checkout_index(
git_repository *repo,
git_index *index,
git_checkout_opts *opts)
{
int error;
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 = git_iterator_for_index(&index_i, index)))
error = git_checkout_iterator(index_i, opts);
git_iterator_free(index_i);
git_index_free(index);
return error; return error;
} }
...@@ -699,11 +1309,12 @@ int git_checkout_tree( ...@@ -699,11 +1309,12 @@ int git_checkout_tree(
const git_object *treeish, const git_object *treeish,
git_checkout_opts *opts) git_checkout_opts *opts)
{ {
int error = 0; int error;
git_index *index = NULL;
git_tree *tree = NULL; git_tree *tree = NULL;
git_iterator *tree_i = NULL;
assert(repo && treeish); 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) { if (git_object_peel((git_object **)&tree, treeish, GIT_OBJ_TREE) < 0) {
giterr_set( giterr_set(
...@@ -711,16 +1322,10 @@ int git_checkout_tree( ...@@ -711,16 +1322,10 @@ int git_checkout_tree(
return -1; return -1;
} }
/* TODO: create a temp index, load tree there and check it out */ if (!(error = git_iterator_for_tree(&tree_i, tree)))
error = git_checkout_iterator(tree_i, opts);
/* 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_iterator_free(tree_i);
git_tree_free(tree); git_tree_free(tree);
return error; return error;
...@@ -731,17 +1336,18 @@ int git_checkout_head( ...@@ -731,17 +1336,18 @@ int git_checkout_head(
git_checkout_opts *opts) git_checkout_opts *opts)
{ {
int error; int error;
git_reference *head = NULL; git_tree *head = NULL;
git_object *tree = NULL; git_iterator *head_i = NULL;
assert(repo); if ((error = git_repository__ensure_not_bare(repo, "checkout head")) < 0)
return error;
if (!(error = git_repository_head(&head, repo)) && if (!(error = checkout_lookup_head_tree(&head, repo)) &&
!(error = git_reference_peel(&tree, head, GIT_OBJ_TREE))) !(error = git_iterator_for_tree(&head_i, head)))
error = git_checkout_tree(repo, tree, opts); error = git_checkout_iterator(head_i, opts);
git_reference_free(head); git_iterator_free(head_i);
git_object_free(tree); git_tree_free(head);
return error; return error;
} }
/*
* 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__NOTIFY_CONFLICT_TREE (1u << 12)
/**
* 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_iterator(
git_iterator *target,
git_checkout_opts *opts);
#endif
...@@ -362,7 +362,7 @@ static bool should_checkout( ...@@ -362,7 +362,7 @@ static bool should_checkout(
if (!opts) if (!opts)
return false; return false;
if (opts->checkout_strategy == GIT_CHECKOUT_DEFAULT) if (opts->checkout_strategy == GIT_CHECKOUT_NONE)
return false; return false;
return !git_repository_head_orphan(repo); return !git_repository_head_orphan(repo);
......
...@@ -164,6 +164,11 @@ static git_diff_delta *diff_delta__last_for_item( ...@@ -164,6 +164,11 @@ static git_diff_delta *diff_delta__last_for_item(
if (git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) if (git_oid_cmp(&delta->new_file.oid, &item->oid) == 0)
return delta; return delta;
break; 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: case GIT_DELTA_MODIFIED:
if (git_oid_cmp(&delta->old_file.oid, &item->oid) == 0 || if (git_oid_cmp(&delta->old_file.oid, &item->oid) == 0 ||
git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) git_oid_cmp(&delta->new_file.oid, &item->oid) == 0)
...@@ -531,14 +536,14 @@ static bool entry_is_prefixed( ...@@ -531,14 +536,14 @@ static bool entry_is_prefixed(
{ {
size_t pathlen; 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; return false;
pathlen = strlen(item->path); pathlen = strlen(prefix_item->path);
return (item->path[pathlen - 1] == '/' || return (prefix_item->path[pathlen - 1] == '/' ||
prefix_item->path[pathlen] == '\0' || item->path[pathlen] == '\0' ||
prefix_item->path[pathlen] == '/'); item->path[pathlen] == '/');
} }
static int diff_list_init_from_iterators( static int diff_list_init_from_iterators(
...@@ -584,8 +589,7 @@ int git_diff__from_iterators( ...@@ -584,8 +589,7 @@ int git_diff__from_iterators(
*diff_ptr = NULL; *diff_ptr = NULL;
if (!diff || if (!diff || diff_list_init_from_iterators(diff, old_iter, new_iter) < 0)
diff_list_init_from_iterators(diff, old_iter, new_iter) < 0)
goto fail; goto fail;
if (diff->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) { if (diff->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) {
...@@ -616,7 +620,7 @@ int git_diff__from_iterators( ...@@ -616,7 +620,7 @@ int git_diff__from_iterators(
* instead of just generating a DELETE record * instead of just generating a DELETE record
*/ */
if ((diff->opts.flags & GIT_DIFF_INCLUDE_TYPECHANGE_TREES) != 0 && 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 */ /* this entry has become a tree! convert to TYPECHANGE */
git_diff_delta *last = diff_delta__last_for_item(diff, oitem); git_diff_delta *last = diff_delta__last_for_item(diff, oitem);
...@@ -624,6 +628,17 @@ int git_diff__from_iterators( ...@@ -624,6 +628,17 @@ int git_diff__from_iterators(
last->status = GIT_DELTA_TYPECHANGE; last->status = GIT_DELTA_TYPECHANGE;
last->new_file.mode = GIT_FILEMODE_TREE; 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) if (git_iterator_advance(old_iter, &oitem) < 0)
...@@ -635,6 +650,7 @@ int git_diff__from_iterators( ...@@ -635,6 +650,7 @@ int git_diff__from_iterators(
*/ */
else if (cmp > 0) { else if (cmp > 0) {
git_delta_t delta_type = GIT_DELTA_UNTRACKED; git_delta_t delta_type = GIT_DELTA_UNTRACKED;
bool contains_oitem = entry_is_prefixed(diff, oitem, nitem);
/* check if contained in ignored parent directory */ /* check if contained in ignored parent directory */
if (git_buf_len(&ignore_prefix) && if (git_buf_len(&ignore_prefix) &&
...@@ -646,14 +662,12 @@ int git_diff__from_iterators( ...@@ -646,14 +662,12 @@ int git_diff__from_iterators(
* it or if the user requested the contents of untracked * it or if the user requested the contents of untracked
* directories and it is not under an ignored directory. * directories and it is not under an ignored directory.
*/ */
bool contains_tracked =
entry_is_prefixed(diff, nitem, oitem);
bool recurse_untracked = bool recurse_untracked =
(delta_type == GIT_DELTA_UNTRACKED && (delta_type == GIT_DELTA_UNTRACKED &&
(diff->opts.flags & GIT_DIFF_RECURSE_UNTRACKED_DIRS) != 0); (diff->opts.flags & GIT_DIFF_RECURSE_UNTRACKED_DIRS) != 0);
/* do not advance into directories that contain a .git file */ /* do not advance into directories that contain a .git file */
if (!contains_tracked && recurse_untracked) { if (!contains_oitem && recurse_untracked) {
git_buf *full = NULL; git_buf *full = NULL;
if (git_iterator_current_workdir_path(new_iter, &full) < 0) if (git_iterator_current_workdir_path(new_iter, &full) < 0)
goto fail; goto fail;
...@@ -661,7 +675,7 @@ int git_diff__from_iterators( ...@@ -661,7 +675,7 @@ int git_diff__from_iterators(
recurse_untracked = false; recurse_untracked = false;
} }
if (contains_tracked || recurse_untracked) { if (contains_oitem || recurse_untracked) {
/* if this directory is ignored, remember it as the /* if this directory is ignored, remember it as the
* "ignore_prefix" for processing contained items * "ignore_prefix" for processing contained items
*/ */
...@@ -707,14 +721,14 @@ int git_diff__from_iterators( ...@@ -707,14 +721,14 @@ int git_diff__from_iterators(
goto fail; goto fail;
/* if we are generating TYPECHANGE records then check for that /* 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 && if (delta_type != GIT_DELTA_IGNORED &&
(diff->opts.flags & GIT_DIFF_INCLUDE_TYPECHANGE_TREES) != 0 && (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 */ /* this entry was prefixed with a tree - make TYPECHANGE */
git_diff_delta *last = diff_delta__last_for_item(diff, oitem); git_diff_delta *last = diff_delta__last_for_item(diff, nitem);
if (last) { if (last) {
last->status = GIT_DELTA_TYPECHANGE; last->status = GIT_DELTA_TYPECHANGE;
last->old_file.mode = GIT_FILEMODE_TREE; last->old_file.mode = GIT_FILEMODE_TREE;
......
...@@ -352,6 +352,7 @@ int git_futils_mkdir_r(const char *path, const char *base, const mode_t mode) ...@@ -352,6 +352,7 @@ int git_futils_mkdir_r(const char *path, const char *base, const mode_t mode)
typedef struct { typedef struct {
const char *base; const char *base;
size_t baselen;
uint32_t flags; uint32_t flags;
int error; int error;
} futils__rmdir_data; } futils__rmdir_data;
...@@ -443,9 +444,13 @@ static int futils__rmdir_recurs_foreach(void *opaque, git_buf *path) ...@@ -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) static int futils__rmdir_empty_parent(void *opaque, git_buf *path)
{ {
int error = p_rmdir(path->ptr); futils__rmdir_data *data = opaque;
int error;
if (git_buf_len(path) <= data->baselen)
return GIT_ITEROVER;
GIT_UNUSED(opaque); error = p_rmdir(git_buf_cstr(path));
if (error) { if (error) {
int en = errno; int en = errno;
...@@ -457,7 +462,7 @@ static int futils__rmdir_empty_parent(void *opaque, git_buf *path) ...@@ -457,7 +462,7 @@ static int futils__rmdir_empty_parent(void *opaque, git_buf *path)
giterr_clear(); giterr_clear();
error = GIT_ITEROVER; error = GIT_ITEROVER;
} else { } 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( ...@@ -475,9 +480,10 @@ int git_futils_rmdir_r(
if (git_path_join_unrooted(&fullpath, path, base, NULL) < 0) if (git_path_join_unrooted(&fullpath, path, base, NULL) < 0)
return -1; return -1;
data.base = base ? base : ""; data.base = base ? base : "";
data.flags = flags; data.baselen = base ? strlen(base) : 0;
data.error = 0; data.flags = flags;
data.error = 0;
error = futils__rmdir_recurs_foreach(&data, &fullpath); error = futils__rmdir_recurs_foreach(&data, &fullpath);
......
...@@ -794,6 +794,44 @@ int git_index_remove(git_index *index, const char *path, int stage) ...@@ -794,6 +794,44 @@ int git_index_remove(git_index *index, const char *path, int stage)
return error; 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) static int index_find(git_index *index, const char *path, int stage)
{ {
struct entry_srch_key srch_key; struct entry_srch_key srch_key;
...@@ -814,7 +852,10 @@ int git_index_find(git_index *index, const char *path) ...@@ -814,7 +852,10 @@ int git_index_find(git_index *index, const char *path)
if ((pos = git_vector_bsearch2( if ((pos = git_vector_bsearch2(
&index->entries, index->entries_search_path, path)) < 0) &index->entries, index->entries_search_path, path)) < 0)
{
giterr_set(GITERR_INDEX, "Index does not contain %s", path);
return pos; return pos;
}
/* Since our binary search only looked at path, we may be in the /* Since our binary search only looked at path, we may be in the
* middle of a list of stages. * middle of a list of stages.
......
...@@ -30,8 +30,8 @@ ...@@ -30,8 +30,8 @@
(P)->base.start = start ? git__strdup(start) : NULL; \ (P)->base.start = start ? git__strdup(start) : NULL; \
(P)->base.end = end ? git__strdup(end) : NULL; \ (P)->base.end = end ? git__strdup(end) : NULL; \
(P)->base.ignore_case = false; \ (P)->base.ignore_case = false; \
if ((start && !(P)->base.start) || (end && !(P)->base.end)) \ if ((start && !(P)->base.start) || (end && !(P)->base.end)) { \
return -1; \ git__free(P); return -1; } \
} while (0) } while (0)
static int iterator__reset_range( static int iterator__reset_range(
...@@ -988,6 +988,26 @@ fail: ...@@ -988,6 +988,26 @@ fail:
return -1; return -1;
} }
git_index *git_iterator_index_get_index(git_iterator *iter)
{
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)
return ((spoolandsort_callbacks *)iter->cb)->orig_type;
return iter->type;
}
int git_iterator_current_tree_entry( int git_iterator_current_tree_entry(
git_iterator *iter, const git_tree_entry **tree_entry) git_iterator *iter, const git_tree_entry **tree_entry)
{ {
...@@ -1058,8 +1078,8 @@ int git_iterator_advance_into_directory( ...@@ -1058,8 +1078,8 @@ int git_iterator_advance_into_directory(
if (iter->type == GIT_ITERATOR_WORKDIR && if (iter->type == GIT_ITERATOR_WORKDIR &&
wi->entry.path && wi->entry.path &&
S_ISDIR(wi->entry.mode) && (wi->entry.mode == GIT_FILEMODE_TREE ||
!S_ISGITLINK(wi->entry.mode)) wi->entry.mode == GIT_FILEMODE_COMMIT))
{ {
if (workdir_iterator__expand_dir(wi) < 0) if (workdir_iterator__expand_dir(wi) < 0)
/* if error loading or if empty, skip the directory. */ /* if error loading or if empty, skip the directory. */
......
...@@ -193,4 +193,9 @@ extern int git_iterator_cmp( ...@@ -193,4 +193,9 @@ extern int git_iterator_cmp(
extern int git_iterator_current_workdir_path( extern int git_iterator_current_workdir_path(
git_iterator *iter, git_buf **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);
#endif #endif
...@@ -95,11 +95,14 @@ char *git_oid_tostr(char *out, size_t n, const git_oid *oid) ...@@ -95,11 +95,14 @@ char *git_oid_tostr(char *out, size_t n, const git_oid *oid)
{ {
char str[GIT_OID_HEXSZ]; char str[GIT_OID_HEXSZ];
if (!out || n == 0 || !oid) if (!out || n == 0)
return ""; return "";
n--; /* allow room for terminating NUL */ n--; /* allow room for terminating NUL */
if (oid == NULL)
n = 0;
if (n > 0) { if (n > 0) {
git_oid_fmt(str, oid); git_oid_fmt(str, oid);
if (n > GIT_OID_HEXSZ) if (n > GIT_OID_HEXSZ)
......
...@@ -62,13 +62,10 @@ int git_reset( ...@@ -62,13 +62,10 @@ int git_reset(
git_object *commit = NULL; git_object *commit = NULL;
git_index *index = NULL; git_index *index = NULL;
git_tree *tree = NULL; git_tree *tree = NULL;
int error; int error = 0;
git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
assert(repo && target); 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) { if (git_object_owner(target) != repo) {
giterr_set(GITERR_OBJECT, giterr_set(GITERR_OBJECT,
...@@ -76,56 +73,50 @@ int git_reset( ...@@ -76,56 +73,50 @@ int git_reset(
return -1; return -1;
} }
if (reset_type != GIT_RESET_SOFT if (reset_type != GIT_RESET_SOFT &&
&& (error = git_repository__ensure_not_bare( (error = git_repository__ensure_not_bare(repo,
repo,
reset_type == GIT_RESET_MIXED ? "reset mixed" : "reset hard")) < 0) 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)
goto cleanup;
if ((error = git_repository_index(&index, repo)) < 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; goto cleanup;
if (reset_type == GIT_RESET_SOFT && if (reset_type == GIT_RESET_SOFT &&
(git_repository_state(repo) == GIT_REPOSITORY_STATE_MERGE || (git_repository_state(repo) == GIT_REPOSITORY_STATE_MERGE ||
git_index_has_conflicts(index))) { git_index_has_conflicts(index)))
giterr_set(GITERR_OBJECT, "%s (soft) while in the middle of a merge.", ERROR_MSG); {
error = GIT_EUNMERGED; giterr_set(GITERR_OBJECT, "%s (soft) in the middle of a merge.", ERROR_MSG);
goto cleanup; error = GIT_EUNMERGED;
}
if ((error = update_head(repo, commit)) < 0)
goto cleanup;
if (reset_type == GIT_RESET_SOFT) {
error = 0;
goto cleanup; goto cleanup;
} }
if ((error = git_commit_tree(&tree, (git_commit *)commit)) < 0) /* move HEAD to the new target */
goto cleanup; if ((error = update_head(repo, commit)) < 0)
if ((error = git_index_read_tree(index, tree)) < 0)
goto cleanup; goto cleanup;
if ((error = git_index_write(index)) < 0) if (reset_type == GIT_RESET_HARD) {
goto cleanup; /* overwrite working directory with HEAD */
opts.checkout_strategy = GIT_CHECKOUT_FORCE;
if ((error = git_repository_merge_cleanup(repo)) < 0) { if ((error = git_checkout_tree(repo, (git_object *)tree, &opts)) < 0)
giterr_set(GITERR_REPOSITORY, "%s - Failed to clean up merge data.", ERROR_MSG); goto cleanup;
goto cleanup;
} }
if (reset_type == GIT_RESET_MIXED) { if (reset_type > GIT_RESET_SOFT) {
error = 0; /* reset index to the target content */
goto cleanup;
}
opts.checkout_strategy = GIT_CHECKOUT_FORCE; if ((error = git_repository_index(&index, repo)) < 0 ||
(error = git_index_read_tree(index, tree)) < 0 ||
(error = git_index_write(index)) < 0)
goto cleanup;
error = git_checkout_index(repo, NULL, &opts); if ((error = git_repository_merge_cleanup(repo)) < 0) {
giterr_set(GITERR_INDEX, "%s - failed to clean up merge data", ERROR_MSG);
goto cleanup;
}
}
cleanup: cleanup:
git_object_free(commit); git_object_free(commit);
......
...@@ -22,15 +22,6 @@ static int create_error(int error, const char *msg) ...@@ -22,15 +22,6 @@ static int create_error(int error, const char *msg)
return error; 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) static int retrieve_head(git_reference **out, git_repository *repo)
{ {
int error = git_repository_head(out, repo); int error = git_repository_head(out, repo);
...@@ -89,23 +80,22 @@ static int retrieve_base_commit_and_message( ...@@ -89,23 +80,22 @@ static int retrieve_base_commit_and_message(
if ((error = retrieve_head(&head, repo)) < 0) if ((error = retrieve_head(&head, repo)) < 0)
return error; return error;
error = -1;
if (strcmp("HEAD", git_reference_name(head)) == 0) if (strcmp("HEAD", git_reference_name(head)) == 0)
git_buf_puts(stash_message, "(no branch): "); error = git_buf_puts(stash_message, "(no branch): ");
else else
git_buf_printf( error = git_buf_printf(
stash_message, stash_message,
"%s: ", "%s: ",
git_reference_name(head) + strlen(GIT_REFS_HEADS_DIR)); git_reference_name(head) + strlen(GIT_REFS_HEADS_DIR));
if (error < 0)
if (git_commit_lookup(b_commit, repo, git_reference_target(head)) < 0)
goto cleanup; 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; goto cleanup;
error = 0; if ((error = append_commit_description(stash_message, *b_commit)) < 0)
goto cleanup;
cleanup: cleanup:
git_reference_free(head); git_reference_free(head);
...@@ -114,9 +104,10 @@ cleanup: ...@@ -114,9 +104,10 @@ cleanup:
static int build_tree_from_index(git_tree **out, git_index *index) static int build_tree_from_index(git_tree **out, git_index *index)
{ {
int error;
git_oid i_tree_oid; 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 -1;
return git_tree_lookup(out, git_index_owner(index), &i_tree_oid); return git_tree_lookup(out, git_index_owner(index), &i_tree_oid);
...@@ -132,15 +123,15 @@ static int commit_index( ...@@ -132,15 +123,15 @@ static int commit_index(
git_tree *i_tree = NULL; git_tree *i_tree = NULL;
git_oid i_commit_oid; git_oid i_commit_oid;
git_buf msg = GIT_BUF_INIT; 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; 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; goto cleanup;
if (git_commit_create( if ((error = git_commit_create(
&i_commit_oid, &i_commit_oid,
git_index_owner(index), git_index_owner(index),
NULL, NULL,
...@@ -150,8 +141,8 @@ static int commit_index( ...@@ -150,8 +141,8 @@ static int commit_index(
git_buf_cstr(&msg), git_buf_cstr(&msg),
i_tree, i_tree,
1, 1,
&parent) < 0) &parent)) < 0)
goto cleanup; goto cleanup;
error = git_commit_lookup(i_commit, git_index_owner(index), &i_commit_oid); error = git_commit_lookup(i_commit, git_index_owner(index), &i_commit_oid);
...@@ -164,6 +155,8 @@ cleanup: ...@@ -164,6 +155,8 @@ cleanup:
struct cb_data { struct cb_data {
git_index *index; git_index *index;
int error;
bool include_changed; bool include_changed;
bool include_untracked; bool include_untracked;
bool include_ignored; bool include_ignored;
...@@ -174,52 +167,50 @@ static int update_index_cb( ...@@ -174,52 +167,50 @@ static int update_index_cb(
float progress, float progress,
void *payload) void *payload)
{ {
int pos;
struct cb_data *data = (struct cb_data *)payload; struct cb_data *data = (struct cb_data *)payload;
const char *add_path = NULL;
GIT_UNUSED(progress); GIT_UNUSED(progress);
switch (delta->status) { switch (delta->status) {
case GIT_DELTA_IGNORED: case GIT_DELTA_IGNORED:
if (!data->include_ignored) if (data->include_ignored)
break; add_path = delta->new_file.path;
break;
return git_index_add_from_workdir(data->index, delta->new_file.path);
case GIT_DELTA_UNTRACKED: case GIT_DELTA_UNTRACKED:
if (!data->include_untracked) if (data->include_untracked)
break; add_path = delta->new_file.path;
break;
return git_index_add_from_workdir(data->index, delta->new_file.path);
case GIT_DELTA_ADDED: case GIT_DELTA_ADDED:
/* Fall through */
case GIT_DELTA_MODIFIED: case GIT_DELTA_MODIFIED:
if (!data->include_changed) if (data->include_changed)
break; add_path = delta->new_file.path;
break;
return git_index_add_from_workdir(data->index, delta->new_file.path);
case GIT_DELTA_DELETED: case GIT_DELTA_DELETED:
if (!data->include_changed) if (!data->include_changed)
break; break;
if (git_index_find(data->index, delta->old_file.path) == 0)
if ((pos = git_index_find(data->index, delta->new_file.path)) < 0) data->error = git_index_remove(
return -1; data->index, delta->old_file.path, 0);
break;
if (git_index_remove(data->index, delta->new_file.path, 0) < 0)
return -1;
default: default:
/* Unimplemented */ /* Unimplemented */
giterr_set( giterr_set(
GITERR_INVALID, GITERR_INVALID,
"Cannot update index. Unimplemented status kind (%d)", "Cannot update index. Unimplemented status (%d)",
delta->status); 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( static int build_untracked_tree(
...@@ -232,14 +223,15 @@ static int build_untracked_tree( ...@@ -232,14 +223,15 @@ static int build_untracked_tree(
git_diff_list *diff = NULL; git_diff_list *diff = NULL;
git_diff_options opts = GIT_DIFF_OPTIONS_INIT; git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
struct cb_data data = {0}; struct cb_data data = {0};
int error = -1; int error;
git_index_clear(index); git_index_clear(index);
data.index = index; data.index = index;
if (flags & GIT_STASH_INCLUDE_UNTRACKED) { 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; data.include_untracked = true;
} }
...@@ -248,19 +240,22 @@ static int build_untracked_tree( ...@@ -248,19 +240,22 @@ static int build_untracked_tree(
data.include_ignored = true; 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)
goto cleanup; goto cleanup;
if (git_diff_foreach(diff, update_index_cb, NULL, NULL, &data) < 0) if ((error = git_diff_tree_to_workdir(
&diff, git_index_owner(index), i_tree, &opts)) < 0)
goto cleanup; goto cleanup;
if (build_tree_from_index(tree_out, index) < 0) if ((error = git_diff_foreach(
diff, update_index_cb, NULL, NULL, &data)) < 0)
{
if (error == GIT_EUSER)
error = data.error;
goto cleanup; goto cleanup;
}
error = 0; error = build_tree_from_index(tree_out, index);
cleanup: cleanup:
git_diff_list_free(diff); git_diff_list_free(diff);
...@@ -279,15 +274,15 @@ static int commit_untracked( ...@@ -279,15 +274,15 @@ static int commit_untracked(
git_tree *u_tree = NULL; git_tree *u_tree = NULL;
git_oid u_commit_oid; git_oid u_commit_oid;
git_buf msg = GIT_BUF_INIT; 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; 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; goto cleanup;
if (git_commit_create( if ((error = git_commit_create(
&u_commit_oid, &u_commit_oid,
git_index_owner(index), git_index_owner(index),
NULL, NULL,
...@@ -297,8 +292,8 @@ static int commit_untracked( ...@@ -297,8 +292,8 @@ static int commit_untracked(
git_buf_cstr(&msg), git_buf_cstr(&msg),
u_tree, u_tree,
0, 0,
NULL) < 0) NULL)) < 0)
goto cleanup; goto cleanup;
error = git_commit_lookup(u_commit, git_index_owner(index), &u_commit_oid); error = git_commit_lookup(u_commit, git_index_owner(index), &u_commit_oid);
...@@ -318,35 +313,40 @@ static int build_workdir_tree( ...@@ -318,35 +313,40 @@ static int build_workdir_tree(
git_diff_list *diff = NULL, *diff2 = NULL; git_diff_list *diff = NULL, *diff2 = NULL;
git_diff_options opts = GIT_DIFF_OPTIONS_INIT; git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
struct cb_data data = {0}; 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; 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; 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; goto cleanup;
if (git_diff_merge(diff, diff2) < 0) if ((error = git_diff_merge(diff, diff2)) < 0)
goto cleanup; goto cleanup;
data.index = index; data.index = index;
data.include_changed = true; 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; goto cleanup;
}
if (build_tree_from_index(tree_out, index) < 0)
goto cleanup;
error = 0; if ((error = build_tree_from_index(tree_out, index)) < 0)
goto cleanup;
cleanup: cleanup:
git_diff_list_free(diff); git_diff_list_free(diff);
git_diff_list_free(diff2); git_diff_list_free(diff2);
git_tree_free(b_tree); git_tree_free(b_tree);
return error; return error;
} }
...@@ -359,25 +359,24 @@ static int commit_worktree( ...@@ -359,25 +359,24 @@ static int commit_worktree(
git_commit *b_commit, git_commit *b_commit,
git_commit *u_commit) git_commit *u_commit)
{ {
int error = 0;
git_tree *w_tree = NULL, *i_tree = NULL; git_tree *w_tree = NULL, *i_tree = NULL;
int error = -1;
const git_commit *parents[] = { NULL, NULL, NULL }; const git_commit *parents[] = { NULL, NULL, NULL };
parents[0] = b_commit; parents[0] = b_commit;
parents[1] = i_commit; parents[1] = i_commit;
parents[2] = u_commit; parents[2] = u_commit;
if (git_commit_tree(&i_tree, i_commit) < 0) if ((error = git_commit_tree(&i_tree, i_commit)) < 0)
return -1; goto cleanup;
if (git_index_read_tree(index, i_tree) < 0) if ((error = git_index_read_tree(index, i_tree)) < 0)
goto cleanup; goto cleanup;
if (build_workdir_tree(&w_tree, index, b_commit) < 0) if ((error = build_workdir_tree(&w_tree, index, b_commit)) < 0)
goto cleanup; goto cleanup;
if (git_commit_create( error = git_commit_create(
w_commit_oid, w_commit_oid,
git_index_owner(index), git_index_owner(index),
NULL, NULL,
...@@ -386,10 +385,8 @@ static int commit_worktree( ...@@ -386,10 +385,8 @@ static int commit_worktree(
NULL, NULL,
message, message,
w_tree, w_tree,
u_commit ? 3 : 2, parents) < 0) u_commit ? 3 : 2,
goto cleanup; parents);
error = 0;
cleanup: cleanup:
git_tree_free(i_tree); git_tree_free(i_tree);
...@@ -402,9 +399,11 @@ static int prepare_worktree_commit_message( ...@@ -402,9 +399,11 @@ static int prepare_worktree_commit_message(
const char *user_message) const char *user_message)
{ {
git_buf buf = GIT_BUF_INIT; git_buf buf = GIT_BUF_INIT;
int error = -1; int error;
if ((error = git_buf_set(&buf, git_buf_cstr(msg), git_buf_len(msg))) < 0)
return error;
git_buf_set(&buf, git_buf_cstr(msg), git_buf_len(msg));
git_buf_clear(msg); git_buf_clear(msg);
if (!user_message) if (!user_message)
...@@ -420,10 +419,11 @@ static int prepare_worktree_commit_message( ...@@ -420,10 +419,11 @@ static int prepare_worktree_commit_message(
git_buf_printf(msg, ": %s\n", user_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: cleanup:
git_buf_free(&buf); git_buf_free(&buf);
return error; return error;
} }
...@@ -449,8 +449,6 @@ static int update_reflog( ...@@ -449,8 +449,6 @@ static int update_reflog(
if ((error = git_reflog_write(reflog)) < 0) if ((error = git_reflog_write(reflog)) < 0)
goto cleanup; goto cleanup;
error = 0;
cleanup: cleanup:
git_reference_free(stash); git_reference_free(stash);
git_reflog_free(reflog); git_reflog_free(reflog);
...@@ -500,8 +498,7 @@ static int reset_index_and_workdir( ...@@ -500,8 +498,7 @@ static int reset_index_and_workdir(
{ {
git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
opts.checkout_strategy = opts.checkout_strategy = GIT_CHECKOUT_FORCE;
GIT_CHECKOUT_UPDATE_MODIFIED | GIT_CHECKOUT_UPDATE_UNTRACKED;
if (remove_untracked) if (remove_untracked)
opts.checkout_strategy |= GIT_CHECKOUT_REMOVE_UNTRACKED; opts.checkout_strategy |= GIT_CHECKOUT_REMOVE_UNTRACKED;
...@@ -523,7 +520,7 @@ int git_stash_save( ...@@ -523,7 +520,7 @@ int git_stash_save(
assert(out && repo && stasher); 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; return error;
if ((error = retrieve_base_commit_and_message(&b_commit, &msg, repo)) < 0) if ((error = retrieve_base_commit_and_message(&b_commit, &msg, repo)) < 0)
...@@ -531,47 +528,50 @@ int git_stash_save( ...@@ -531,47 +528,50 @@ int git_stash_save(
if ((error = ensure_there_are_changes_to_stash( if ((error = ensure_there_are_changes_to_stash(
repo, repo,
(flags & GIT_STASH_INCLUDE_UNTRACKED) == GIT_STASH_INCLUDE_UNTRACKED, (flags & GIT_STASH_INCLUDE_UNTRACKED) != 0,
(flags & GIT_STASH_INCLUDE_IGNORED) == GIT_STASH_INCLUDE_IGNORED)) < 0) (flags & GIT_STASH_INCLUDE_IGNORED) != 0)) < 0)
goto cleanup; goto cleanup;
error = -1; if ((error = git_repository_index(&index, repo)) < 0)
if (git_repository_index(&index, repo) < 0)
goto cleanup; 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; goto cleanup;
if ((flags & GIT_STASH_INCLUDE_UNTRACKED || flags & GIT_STASH_INCLUDE_IGNORED) if ((flags & (GIT_STASH_INCLUDE_UNTRACKED | GIT_STASH_INCLUDE_IGNORED)) &&
&& commit_untracked(&u_commit, index, stasher, git_buf_cstr(&msg), i_commit, flags) < 0) (error = commit_untracked(
&u_commit, index, stasher, git_buf_cstr(&msg),
i_commit, flags)) < 0)
goto cleanup; goto cleanup;
if (prepare_worktree_commit_message(&msg, message) < 0) if ((error = prepare_worktree_commit_message(&msg, message)) < 0)
goto cleanup; 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; goto cleanup;
git_buf_rtrim(&msg); 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; goto cleanup;
if (reset_index_and_workdir( if ((error = reset_index_and_workdir(
repo, repo,
((flags & GIT_STASH_KEEP_INDEX) == GIT_STASH_KEEP_INDEX) ? ((flags & GIT_STASH_KEEP_INDEX) != 0) ? i_commit : b_commit,
i_commit : b_commit, (flags & GIT_STASH_INCLUDE_UNTRACKED) != 0)) < 0)
(flags & GIT_STASH_INCLUDE_UNTRACKED) == GIT_STASH_INCLUDE_UNTRACKED) < 0)
goto cleanup; goto cleanup;
error = 0;
cleanup: cleanup:
git_buf_free(&msg); git_buf_free(&msg);
git_commit_free(i_commit); git_commit_free(i_commit);
git_commit_free(b_commit); git_commit_free(b_commit);
git_commit_free(u_commit); git_commit_free(u_commit);
git_index_free(index); git_index_free(index);
return error; return error;
} }
...@@ -589,7 +589,6 @@ int git_stash_foreach( ...@@ -589,7 +589,6 @@ int git_stash_foreach(
error = git_reference_lookup(&stash, repo, GIT_REFS_STASH_FILE); error = git_reference_lookup(&stash, repo, GIT_REFS_STASH_FILE);
if (error == GIT_ENOTFOUND) if (error == GIT_ENOTFOUND)
return 0; return 0;
if (error < 0) if (error < 0)
goto cleanup; goto cleanup;
...@@ -599,18 +598,16 @@ int git_stash_foreach( ...@@ -599,18 +598,16 @@ int git_stash_foreach(
max = git_reflog_entrycount(reflog); max = git_reflog_entrycount(reflog);
for (i = 0; i < max; i++) { for (i = 0; i < max; i++) {
entry = git_reflog_entry_byindex(reflog, i); entry = git_reflog_entry_byindex(reflog, i);
if (callback(i, if (callback(i,
git_reflog_entry_message(entry), git_reflog_entry_message(entry),
git_reflog_entry_id_new(entry), git_reflog_entry_id_new(entry),
payload)) { payload)) {
error = GIT_EUSER; error = GIT_EUSER;
goto cleanup; break;
} }
} }
error = 0;
cleanup: cleanup:
git_reference_free(stash); git_reference_free(stash);
git_reflog_free(reflog); git_reflog_free(reflog);
......
...@@ -66,7 +66,7 @@ __KHASH_IMPL( ...@@ -66,7 +66,7 @@ __KHASH_IMPL(
str, static kh_inline, const char *, void *, 1, str, static kh_inline, const char *, void *, 1,
str_hash_no_trailing_slash, str_equal_no_trailing_slash); 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 git_config_backend *open_gitmodules(git_repository *, bool, const git_oid *);
static int lookup_head_remote(git_buf *url, git_repository *repo); static int lookup_head_remote(git_buf *url, git_repository *repo);
static int submodule_get(git_submodule **, git_repository *, const char *, const char *); static int submodule_get(git_submodule **, git_repository *, const char *, const char *);
...@@ -106,7 +106,7 @@ int git_submodule_lookup( ...@@ -106,7 +106,7 @@ int git_submodule_lookup(
assert(repo && name); assert(repo && name);
if ((error = load_submodule_config(repo, false)) < 0) if ((error = load_submodule_config(repo)) < 0)
return error; return error;
pos = git_strmap_lookup_index(repo->submodules, name); pos = git_strmap_lookup_index(repo->submodules, name);
...@@ -148,7 +148,7 @@ int git_submodule_foreach( ...@@ -148,7 +148,7 @@ int git_submodule_foreach(
assert(repo && callback); assert(repo && callback);
if ((error = load_submodule_config(repo, false)) < 0) if ((error = load_submodule_config(repo)) < 0)
return error; return error;
git_strmap_foreach_value(repo->submodules, sm, { git_strmap_foreach_value(repo->submodules, sm, {
...@@ -708,7 +708,8 @@ int git_submodule_open( ...@@ -708,7 +708,8 @@ int git_submodule_open(
int git_submodule_reload_all(git_repository *repo) int git_submodule_reload_all(git_repository *repo)
{ {
assert(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) int git_submodule_reload(git_submodule *submodule)
...@@ -829,6 +830,20 @@ int git_submodule_status( ...@@ -829,6 +830,20 @@ int git_submodule_status(
return error; 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 * INTERNAL FUNCTIONS
*/ */
...@@ -1225,14 +1240,14 @@ static git_config_backend *open_gitmodules( ...@@ -1225,14 +1240,14 @@ static git_config_backend *open_gitmodules(
return mods; return mods;
} }
static int load_submodule_config(git_repository *repo, bool force) static int load_submodule_config(git_repository *repo)
{ {
int error; int error;
git_oid gitmodules_oid; git_oid gitmodules_oid;
git_buf path = GIT_BUF_INIT; git_buf path = GIT_BUF_INIT;
git_config_backend *mods = NULL; git_config_backend *mods = NULL;
if (repo->submodules && !force) if (repo->submodules)
return 0; return 0;
memset(&gitmodules_oid, 0, sizeof(gitmodules_oid)); memset(&gitmodules_oid, 0, sizeof(gitmodules_oid));
......
#include "clar_libgit2.h" #include "clar_libgit2.h"
#include "refs.h" #include "refs.h"
#include "repo/repo_helpers.h" #include "repo/repo_helpers.h"
#include "path.h"
#include "fileops.h"
static git_repository *g_repo; static git_repository *g_repo;
...@@ -14,9 +16,48 @@ void test_checkout_head__cleanup(void) ...@@ -14,9 +16,48 @@ void test_checkout_head__cleanup(void)
cl_git_sandbox_cleanup(); 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); make_head_orphaned(g_repo, NON_EXISTING_HEAD);
cl_assert_equal_i(GIT_EORPHANEDHEAD, git_checkout_head(g_repo, NULL)); 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);
}
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
#include "repository.h" #include "repository.h"
static git_repository *g_repo; static git_repository *g_repo;
static git_checkout_opts g_opts;
static void reset_index_to_treeish(git_object *treeish) static void reset_index_to_treeish(git_object *treeish)
{ {
...@@ -25,9 +24,6 @@ void test_checkout_index__initialize(void) ...@@ -25,9 +24,6 @@ void test_checkout_index__initialize(void)
{ {
git_tree *tree; 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"); g_repo = cl_git_sandbox_init("testrepo");
cl_git_pass(git_repository_head_tree(&tree, g_repo)); cl_git_pass(git_repository_head_tree(&tree, g_repo));
...@@ -66,7 +62,6 @@ void test_checkout_index__cannot_checkout_a_bare_repository(void) ...@@ -66,7 +62,6 @@ void test_checkout_index__cannot_checkout_a_bare_repository(void)
{ {
test_checkout_index__cleanup(); test_checkout_index__cleanup();
GIT_INIT_STRUCTURE(&g_opts, GIT_CHECKOUT_OPTS_VERSION);
g_repo = cl_git_sandbox_init("testrepo.git"); g_repo = cl_git_sandbox_init("testrepo.git");
cl_git_fail(git_checkout_index(g_repo, NULL, NULL)); cl_git_fail(git_checkout_index(g_repo, NULL, NULL));
...@@ -74,11 +69,15 @@ void test_checkout_index__cannot_checkout_a_bare_repository(void) ...@@ -74,11 +69,15 @@ void test_checkout_index__cannot_checkout_a_bare_repository(void)
void test_checkout_index__can_create_missing_files(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/README"));
cl_assert_equal_i(false, git_path_isfile("./testrepo/branch_file.txt")); cl_assert_equal_i(false, git_path_isfile("./testrepo/branch_file.txt"));
cl_assert_equal_i(false, git_path_isfile("./testrepo/new.txt")); cl_assert_equal_i(false, git_path_isfile("./testrepo/new.txt"));
cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE;
cl_git_pass(git_checkout_index(g_repo, NULL, &opts));
test_file_contents("./testrepo/README", "hey there\n"); test_file_contents("./testrepo/README", "hey there\n");
test_file_contents("./testrepo/branch_file.txt", "hi\nbye!\n"); test_file_contents("./testrepo/branch_file.txt", "hi\nbye!\n");
...@@ -87,30 +86,37 @@ void test_checkout_index__can_create_missing_files(void) ...@@ -87,30 +86,37 @@ void test_checkout_index__can_create_missing_files(void)
void test_checkout_index__can_remove_untracked_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); 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/one", "one\n");
cl_git_mkfile("./testrepo/dir/subdir/two", "two\n"); cl_git_mkfile("./testrepo/dir/subdir/two", "two\n");
cl_assert_equal_i(true, git_path_isdir("./testrepo/dir/subdir/subsubdir")); cl_assert_equal_i(true, git_path_isdir("./testrepo/dir/subdir/subsubdir"));
g_opts.checkout_strategy |= GIT_CHECKOUT_REMOVE_UNTRACKED; opts.checkout_strategy =
cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); GIT_CHECKOUT_SAFE_CREATE | GIT_CHECKOUT_REMOVE_UNTRACKED;
cl_git_pass(git_checkout_index(g_repo, NULL, &opts));
cl_assert_equal_i(false, git_path_isdir("./testrepo/dir")); cl_assert_equal_i(false, git_path_isdir("./testrepo/dir"));
} }
void test_checkout_index__honor_the_specified_pathspecs(void) void test_checkout_index__honor_the_specified_pathspecs(void)
{ {
git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
char *entries[] = { "*.txt" }; char *entries[] = { "*.txt" };
g_opts.paths.strings = entries; opts.paths.strings = entries;
g_opts.paths.count = 1; opts.paths.count = 1;
cl_assert_equal_i(false, git_path_isfile("./testrepo/README")); 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/branch_file.txt"));
cl_assert_equal_i(false, git_path_isfile("./testrepo/new.txt")); cl_assert_equal_i(false, git_path_isfile("./testrepo/new.txt"));
cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE;
cl_git_pass(git_checkout_index(g_repo, NULL, &opts));
cl_assert_equal_i(false, git_path_isfile("./testrepo/README")); cl_assert_equal_i(false, git_path_isfile("./testrepo/README"));
test_file_contents("./testrepo/branch_file.txt", "hi\nbye!\n"); test_file_contents("./testrepo/branch_file.txt", "hi\nbye!\n");
...@@ -134,6 +140,7 @@ static void set_core_autocrlf_to(bool value) ...@@ -134,6 +140,7 @@ static void set_core_autocrlf_to(bool value)
void test_checkout_index__honor_the_gitattributes_directives(void) void test_checkout_index__honor_the_gitattributes_directives(void)
{ {
git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
const char *attributes = const char *attributes =
"branch_file.txt text eol=crlf\n" "branch_file.txt text eol=crlf\n"
"new.txt text eol=lf\n"; "new.txt text eol=lf\n";
...@@ -141,7 +148,9 @@ void test_checkout_index__honor_the_gitattributes_directives(void) ...@@ -141,7 +148,9 @@ void test_checkout_index__honor_the_gitattributes_directives(void)
cl_git_mkfile("./testrepo/.gitattributes", attributes); cl_git_mkfile("./testrepo/.gitattributes", attributes);
set_core_autocrlf_to(false); set_core_autocrlf_to(false);
cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE;
cl_git_pass(git_checkout_index(g_repo, NULL, &opts));
test_file_contents("./testrepo/README", "hey there\n"); test_file_contents("./testrepo/README", "hey there\n");
test_file_contents("./testrepo/new.txt", "my new file\n"); test_file_contents("./testrepo/new.txt", "my new file\n");
...@@ -151,12 +160,15 @@ void test_checkout_index__honor_the_gitattributes_directives(void) ...@@ -151,12 +160,15 @@ void test_checkout_index__honor_the_gitattributes_directives(void)
void test_checkout_index__honor_coreautocrlf_setting_set_to_true(void) void test_checkout_index__honor_coreautocrlf_setting_set_to_true(void)
{ {
#ifdef GIT_WIN32 #ifdef GIT_WIN32
git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
const char *expected_readme_text = "hey there\r\n"; const char *expected_readme_text = "hey there\r\n";
cl_git_pass(p_unlink("./testrepo/.gitattributes")); cl_git_pass(p_unlink("./testrepo/.gitattributes"));
set_core_autocrlf_to(true); set_core_autocrlf_to(true);
cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE;
cl_git_pass(git_checkout_index(g_repo, NULL, &opts));
test_file_contents("./testrepo/README", expected_readme_text); test_file_contents("./testrepo/README", expected_readme_text);
#endif #endif
...@@ -169,9 +181,13 @@ static void set_repo_symlink_handling_cap_to(bool value) ...@@ -169,9 +181,13 @@ static void set_repo_symlink_handling_cap_to(bool value)
void test_checkout_index__honor_coresymlinks_setting_set_to_true(void) 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); set_repo_symlink_handling_cap_to(true);
cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE;
cl_git_pass(git_checkout_index(g_repo, NULL, &opts));
#ifdef GIT_WIN32 #ifdef GIT_WIN32
test_file_contents("./testrepo/link_to_new.txt", "new.txt"); test_file_contents("./testrepo/link_to_new.txt", "new.txt");
...@@ -191,51 +207,63 @@ void test_checkout_index__honor_coresymlinks_setting_set_to_true(void) ...@@ -191,51 +207,63 @@ void test_checkout_index__honor_coresymlinks_setting_set_to_true(void)
void test_checkout_index__honor_coresymlinks_setting_set_to_false(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); set_repo_symlink_handling_cap_to(false);
cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE;
cl_git_pass(git_checkout_index(g_repo, NULL, &opts));
test_file_contents("./testrepo/link_to_new.txt", "new.txt"); test_file_contents("./testrepo/link_to_new.txt", "new.txt");
} }
void test_checkout_index__donot_overwrite_modified_file_by_default(void) 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!"); 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 /* set this up to not return an error code on conflicts, but it
* still will not have permission to overwrite anything... * still will not have permission to overwrite anything...
*/ */
g_opts.checkout_strategy = 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!"); test_file_contents("./testrepo/new.txt", "This isn't what's stored!");
} }
void test_checkout_index__can_overwrite_modified_file(void) 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!"); cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!");
g_opts.checkout_strategy |= GIT_CHECKOUT_UPDATE_MODIFIED; 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"); test_file_contents("./testrepo/new.txt", "my new file\n");
} }
void test_checkout_index__options_disable_filters(void) 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"); cl_git_mkfile("./testrepo/.gitattributes", "*.txt text eol=crlf\n");
g_opts.disable_filters = false; opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE;
cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); opts.disable_filters = false;
cl_git_pass(git_checkout_index(g_repo, NULL, &opts));
test_file_contents("./testrepo/new.txt", "my new file\r\n"); test_file_contents("./testrepo/new.txt", "my new file\r\n");
p_unlink("./testrepo/new.txt"); p_unlink("./testrepo/new.txt");
g_opts.disable_filters = true; opts.disable_filters = true;
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"); test_file_contents("./testrepo/new.txt", "my new file\n");
} }
...@@ -243,6 +271,7 @@ void test_checkout_index__options_disable_filters(void) ...@@ -243,6 +271,7 @@ void test_checkout_index__options_disable_filters(void)
void test_checkout_index__options_dir_modes(void) void test_checkout_index__options_dir_modes(void)
{ {
#ifndef GIT_WIN32 #ifndef GIT_WIN32
git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
struct stat st; struct stat st;
git_oid oid; git_oid oid;
git_commit *commit; git_commit *commit;
...@@ -252,8 +281,10 @@ void test_checkout_index__options_dir_modes(void) ...@@ -252,8 +281,10 @@ void test_checkout_index__options_dir_modes(void)
reset_index_to_treeish((git_object *)commit); reset_index_to_treeish((git_object *)commit);
g_opts.dir_mode = 0701; opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE;
cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); opts.dir_mode = 0701;
cl_git_pass(git_checkout_index(g_repo, NULL, &opts));
cl_git_pass(p_stat("./testrepo/a", &st)); cl_git_pass(p_stat("./testrepo/a", &st));
cl_assert_equal_i(st.st_mode & 0777, 0701); cl_assert_equal_i(st.st_mode & 0777, 0701);
...@@ -269,11 +300,13 @@ void test_checkout_index__options_dir_modes(void) ...@@ -269,11 +300,13 @@ void test_checkout_index__options_dir_modes(void)
void test_checkout_index__options_override_file_modes(void) void test_checkout_index__options_override_file_modes(void)
{ {
#ifndef GIT_WIN32 #ifndef GIT_WIN32
git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
struct stat st; struct stat st;
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_git_pass(p_stat("./testrepo/new.txt", &st));
cl_assert_equal_i(st.st_mode & 0777, 0700); cl_assert_equal_i(st.st_mode & 0777, 0700);
...@@ -282,42 +315,48 @@ void test_checkout_index__options_override_file_modes(void) ...@@ -282,42 +315,48 @@ void test_checkout_index__options_override_file_modes(void)
void test_checkout_index__options_open_flags(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"); cl_git_mkfile("./testrepo/new.txt", "hi\n");
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_UPDATE_MODIFIED; 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", "hi\nmy new file\n"); test_file_contents("./testrepo/new.txt", "hi\nmy new file\n");
} }
struct conflict_data { struct notify_data {
const char *file; const char *file;
const char *sha; const char *sha;
}; };
static int conflict_cb( static int test_checkout_notify_cb(
const char *conflict_file, git_checkout_notify_t why,
const git_oid *blob_oid, const char *path,
unsigned int index_mode, const git_diff_file *baseline,
unsigned int wd_mode, const git_diff_file *target,
const git_diff_file *workdir,
void *payload) void *payload)
{ {
struct conflict_data *expectations = (struct conflict_data *)payload; struct notify_data *expectations = (struct notify_data *)payload;
GIT_UNUSED(index_mode); GIT_UNUSED(workdir);
GIT_UNUSED(wd_mode);
cl_assert_equal_s(expectations->file, conflict_file); cl_assert_equal_i(GIT_CHECKOUT_NOTIFY_CONFLICT, why);
cl_assert_equal_i(0, git_oid_streq(blob_oid, expectations->sha)); 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; return 0;
} }
void test_checkout_index__can_notify_of_skipped_files(void) void test_checkout_index__can_notify_of_skipped_files(void)
{ {
struct conflict_data data; git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
struct notify_data data;
cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!"); cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!");
...@@ -330,24 +369,28 @@ void test_checkout_index__can_notify_of_skipped_files(void) ...@@ -330,24 +369,28 @@ void test_checkout_index__can_notify_of_skipped_files(void)
data.file = "new.txt"; data.file = "new.txt";
data.sha = "a71586c1dfe8a71c6cbf6c129f404c5642ff31bd"; data.sha = "a71586c1dfe8a71c6cbf6c129f404c5642ff31bd";
g_opts.checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS; opts.checkout_strategy =
g_opts.conflict_cb = conflict_cb; GIT_CHECKOUT_SAFE_CREATE | GIT_CHECKOUT_ALLOW_CONFLICTS;
g_opts.conflict_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_conflict_cb( static int dont_notify_cb(
const char *conflict_file, git_checkout_notify_t why,
const git_oid *blob_oid, const char *path,
unsigned int index_mode, const git_diff_file *baseline,
unsigned int wd_mode, const git_diff_file *target,
const git_diff_file *workdir,
void *payload) void *payload)
{ {
GIT_UNUSED(conflict_file); GIT_UNUSED(why);
GIT_UNUSED(blob_oid); GIT_UNUSED(path);
GIT_UNUSED(index_mode); GIT_UNUSED(baseline);
GIT_UNUSED(wd_mode); GIT_UNUSED(target);
GIT_UNUSED(workdir);
GIT_UNUSED(payload); GIT_UNUSED(payload);
cl_assert(false); cl_assert(false);
...@@ -357,37 +400,45 @@ static int dont_conflict_cb( ...@@ -357,37 +400,45 @@ static int dont_conflict_cb(
void test_checkout_index__wont_notify_of_expected_line_ending_changes(void) 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")); cl_git_pass(p_unlink("./testrepo/.gitattributes"));
set_core_autocrlf_to(true); set_core_autocrlf_to(true);
cl_git_mkfile("./testrepo/new.txt", "my new file\r\n"); cl_git_mkfile("./testrepo/new.txt", "my new file\r\n");
g_opts.checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS; opts.checkout_strategy =
g_opts.conflict_cb = dont_conflict_cb; GIT_CHECKOUT_SAFE_CREATE | GIT_CHECKOUT_ALLOW_CONFLICTS;
g_opts.conflict_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 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); GIT_UNUSED(path); GIT_UNUSED(cur); GIT_UNUSED(tot);
*was_called = true; (*(int *)payload)++;
} }
void test_checkout_index__calls_progress_callback(void) void test_checkout_index__calls_progress_callback(void)
{ {
bool was_called = 0; git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
g_opts.progress_cb = progress; int calls = 0;
g_opts.progress_payload = &was_called;
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_equal_i(was_called, true); cl_assert(calls > 0);
} }
void test_checkout_index__can_overcome_name_clashes(void) void test_checkout_index__can_overcome_name_clashes(void)
{ {
git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
git_index *index; git_index *index;
cl_git_pass(git_repository_index(&index, g_repo)); cl_git_pass(git_repository_index(&index, g_repo));
...@@ -400,7 +451,6 @@ void test_checkout_index__can_overcome_name_clashes(void) ...@@ -400,7 +451,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, "path0"));
cl_git_pass(git_index_add_from_workdir(index, "path1/file1")); cl_git_pass(git_index_add_from_workdir(index, "path1/file1"));
cl_git_pass(p_unlink("./testrepo/path0")); cl_git_pass(p_unlink("./testrepo/path0"));
cl_git_pass(git_futils_rmdir_r( cl_git_pass(git_futils_rmdir_r(
"./testrepo/path1", NULL, GIT_RMDIR_REMOVE_FILES)); "./testrepo/path1", NULL, GIT_RMDIR_REMOVE_FILES));
...@@ -412,14 +462,15 @@ void test_checkout_index__can_overcome_name_clashes(void) ...@@ -412,14 +462,15 @@ void test_checkout_index__can_overcome_name_clashes(void)
cl_assert(git_path_isfile("./testrepo/path1")); cl_assert(git_path_isfile("./testrepo/path1"));
cl_assert(git_path_isfile("./testrepo/path0/file0")); cl_assert(git_path_isfile("./testrepo/path0/file0"));
g_opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS; opts.checkout_strategy =
cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts)); GIT_CHECKOUT_SAFE_CREATE | GIT_CHECKOUT_ALLOW_CONFLICTS;
cl_git_pass(git_checkout_index(g_repo, index, &opts));
cl_assert(git_path_isfile("./testrepo/path1")); cl_assert(git_path_isfile("./testrepo/path1"));
cl_assert(git_path_isfile("./testrepo/path0/file0")); cl_assert(git_path_isfile("./testrepo/path0/file0"));
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, index, &opts));
cl_assert(git_path_isfile("./testrepo/path0")); cl_assert(git_path_isfile("./testrepo/path0"));
cl_assert(git_path_isfile("./testrepo/path1/file1")); cl_assert(git_path_isfile("./testrepo/path1/file1"));
...@@ -429,18 +480,44 @@ void test_checkout_index__can_overcome_name_clashes(void) ...@@ -429,18 +480,44 @@ void test_checkout_index__can_overcome_name_clashes(void)
void test_checkout_index__validates_struct_version(void) void test_checkout_index__validates_struct_version(void)
{ {
git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
const git_error *err; const git_error *err;
g_opts.version = 1024; opts.version = 1024;
cl_git_fail(git_checkout_index(g_repo, NULL, &g_opts)); cl_git_fail(git_checkout_index(g_repo, NULL, &opts));
err = giterr_last(); err = giterr_last();
cl_assert_equal_i(err->klass, GITERR_INVALID); cl_assert_equal_i(err->klass, GITERR_INVALID);
g_opts.version = 0; opts.version = 0;
giterr_clear(); 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(); err = giterr_last();
cl_assert_equal_i(err->klass, GITERR_INVALID); 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"));
}
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
#include "git2/checkout.h" #include "git2/checkout.h"
#include "repository.h" #include "repository.h"
#include "buffer.h"
#include "fileops.h"
static git_repository *g_repo; static git_repository *g_repo;
static git_checkout_opts g_opts; static git_checkout_opts g_opts;
...@@ -12,7 +14,7 @@ void test_checkout_tree__initialize(void) ...@@ -12,7 +14,7 @@ void test_checkout_tree__initialize(void)
g_repo = cl_git_sandbox_init("testrepo"); g_repo = cl_git_sandbox_init("testrepo");
GIT_INIT_STRUCTURE(&g_opts, GIT_CHECKOUT_OPTS_VERSION); 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) void test_checkout_tree__cleanup(void)
...@@ -48,6 +50,34 @@ void test_checkout_tree__can_checkout_a_subdirectory_from_a_commit(void) ...@@ -48,6 +50,34 @@ 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")); cl_assert_equal_i(true, git_path_isfile("./testrepo/ab/de/fgh/1.txt"));
} }
void test_checkout_tree__can_checkout_and_remove_directory(void)
{
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_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
*/
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_repository_set_head(g_repo, "refs/heads/master"));
/* 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) void test_checkout_tree__can_checkout_a_subdirectory_from_a_subtree(void)
{ {
char *entries[] = { "de/" }; char *entries[] = { "de/" };
...@@ -85,3 +115,193 @@ void test_checkout_tree__calls_progress_callback(void) ...@@ -85,3 +115,193 @@ void test_checkout_tree__calls_progress_callback(void)
cl_assert_equal_i(was_called, true); 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));
/* 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"));
}
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);
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
#include "git2/checkout.h" #include "git2/checkout.h"
#include "path.h" #include "path.h"
#include "posix.h" #include "posix.h"
#include "fileops.h"
static git_repository *g_repo = NULL; static git_repository *g_repo = NULL;
...@@ -34,28 +35,97 @@ void test_checkout_typechange__cleanup(void) ...@@ -34,28 +35,97 @@ void test_checkout_typechange__cleanup(void)
cl_fixture_cleanup("submod2_target"); 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)
{ {
int i;
git_object *obj; git_object *obj;
git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; 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?!! */
}
}
opts.checkout_strategy = GIT_CHECKOUT_FORCE; git_tree_free(tree);
git_buf_free(&path);
}
/* if you don't include GIT_CHECKOUT_REMOVE_UNTRACKED then on the final void test_checkout_typechange__checkout_typechanges_safe(void)
* checkout which is supposed to remove all the files, we will not {
* actually remove them! int i;
*/ git_object *obj;
git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
for (i = 0; g_typechange_oids[i] != NULL; ++i) { for (i = 0; g_typechange_oids[i] != NULL; ++i) {
cl_git_pass(git_revparse_single(&obj, g_repo, g_typechange_oids[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_checkout_tree(g_repo, obj, &opts));
cl_git_pass( cl_git_pass(
git_repository_set_head_detached(g_repo, git_object_id(obj))); 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); git_object_free(obj);
if (!g_typechange_empty[i]) { if (!g_typechange_empty[i]) {
...@@ -75,3 +145,96 @@ void test_checkout_typechange__checkout_typechanges(void) ...@@ -75,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);
}
}
...@@ -2,6 +2,16 @@ ...@@ -2,6 +2,16 @@
#include "posix.h" #include "posix.h"
#include "path.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 : "<no message>");
clar__assert(0, file, line, fncall, msg, 1);
}
void cl_git_mkfile(const char *filename, const char *content) void cl_git_mkfile(const char *filename, const char *content)
{ {
int fd; int fd;
......
...@@ -6,17 +6,17 @@ ...@@ -6,17 +6,17 @@
#include "common.h" #include "common.h"
/** /**
* Special wrapper for `clar_must_pass` that passes * Replace for `clar_must_pass` that passes the last library error as the
* the last library error as the test failure message. * test failure message.
* *
* Use this wrapper around all `git_` library calls that * Use this wrapper around all `git_` library calls that return error codes!
* return error codes!
*/ */
#define cl_git_pass(expr) do { \ #define cl_git_pass(expr) do { \
int _lg2_error; \
giterr_clear(); \ giterr_clear(); \
if ((expr) != 0) \ if ((_lg2_error = (expr)) != 0) \
clar__assert(0, __FILE__, __LINE__, "Function call failed: " #expr, giterr_last() ? giterr_last()->message : NULL, 1); \ cl_git_report_failure(_lg2_error, __FILE__, __LINE__, "Function call failed: " #expr); \
} while(0) } while (0)
/** /**
* Wrapper for `clar_must_fail` -- this one is * Wrapper for `clar_must_fail` -- this one is
...@@ -25,6 +25,10 @@ ...@@ -25,6 +25,10 @@
*/ */
#define cl_git_fail(expr) cl_must_fail(expr) #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)) #define cl_assert_equal_sz(sz1,sz2) cl_assert((sz1) == (sz2))
/* /*
......
...@@ -290,3 +290,86 @@ void test_index_tests__write_invalid_filename(void) ...@@ -290,3 +290,86 @@ void test_index_tests__write_invalid_filename(void)
cl_fixture_cleanup("read_tree"); 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");
}
...@@ -21,9 +21,9 @@ void test_object_raw_convert__succeed_on_oid_to_string_conversion(void) ...@@ -21,9 +21,9 @@ void test_object_raw_convert__succeed_on_oid_to_string_conversion(void)
str = git_oid_tostr(out, 0, &in); str = git_oid_tostr(out, 0, &in);
cl_assert(str && *str == '\0' && str != out); 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); 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 */ /* n == 1, returns out as an empty string */
str = git_oid_tostr(out, 1, &in); str = git_oid_tostr(out, 1, &in);
......
...@@ -91,7 +91,7 @@ void test_online_clone__can_checkout_a_cloned_repo(void) ...@@ -91,7 +91,7 @@ void test_online_clone__can_checkout_a_cloned_repo(void)
bool checkout_progress_cb_was_called = false, bool checkout_progress_cb_was_called = false,
fetch_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_cb = &checkout_progress;
g_options.checkout_opts.progress_payload = &checkout_progress_cb_was_called; g_options.checkout_opts.progress_payload = &checkout_progress_cb_was_called;
g_options.fetch_progress_cb = &fetch_progress; g_options.fetch_progress_cb = &fetch_progress;
......
...@@ -54,9 +54,7 @@ void test_reset_hard__resetting_reverts_modified_files(void) ...@@ -54,9 +54,7 @@ void test_reset_hard__resetting_reverts_modified_files(void)
static const char *after[4] = { static const char *after[4] = {
"current_file\n", "current_file\n",
"modified_file\n", "modified_file\n",
/* wrong value because reset is still slightly incorrect */ NULL,
"staged_new_file\n",
/* right value: NULL, */
"staged_changes_modified_file\n" "staged_changes_modified_file\n"
}; };
const char *wd = git_repository_workdir(repo); const char *wd = git_repository_workdir(repo);
......
...@@ -36,15 +36,22 @@ static void push_three_states(void) ...@@ -36,15 +36,22 @@ static void push_three_states(void)
cl_git_pass(git_repository_index(&index, repo)); cl_git_pass(git_repository_index(&index, repo));
cl_git_pass(git_index_add_from_workdir(index, "zero.txt")); cl_git_pass(git_index_add_from_workdir(index, "zero.txt"));
commit_staged_files(&oid, index, signature); commit_staged_files(&oid, index, signature);
cl_assert(git_path_exists("stash/zero.txt"));
cl_git_mkfile("stash/one.txt", "content\n"); cl_git_mkfile("stash/one.txt", "content\n");
cl_git_pass(git_stash_save(&oid, repo, signature, "First", GIT_STASH_INCLUDE_UNTRACKED)); 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_mkfile("stash/two.txt", "content\n");
cl_git_pass(git_stash_save(&oid, repo, signature, "Second", GIT_STASH_INCLUDE_UNTRACKED)); 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_mkfile("stash/three.txt", "content\n");
cl_git_pass(git_stash_save(&oid, repo, signature, "Third", GIT_STASH_INCLUDE_UNTRACKED)); 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); git_index_free(index);
} }
......
...@@ -38,10 +38,10 @@ void test_stash_foreach__cleanup(void) ...@@ -38,10 +38,10 @@ void test_stash_foreach__cleanup(void)
} }
static int callback_cb( static int callback_cb(
size_t index, size_t index,
const char* message, const char* message,
const git_oid *stash_oid, const git_oid *stash_oid,
void *payload) void *payload)
{ {
struct callback_data *data = (struct callback_data *)payload; struct callback_data *data = (struct callback_data *)payload;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment