Commit 5e2261ac by Vicent Martí

Merge pull request #1507 from arrbee/fix-look-inside-untracked-directory

Update diff handling of "untracked" directories
parents 7dcda3aa 5fa7e469
...@@ -88,42 +88,61 @@ typedef enum { ...@@ -88,42 +88,61 @@ typedef enum {
GIT_DIFF_INCLUDE_UNTRACKED = (1 << 8), GIT_DIFF_INCLUDE_UNTRACKED = (1 << 8),
/** Include unmodified files in the diff list */ /** Include unmodified files in the diff list */
GIT_DIFF_INCLUDE_UNMODIFIED = (1 << 9), GIT_DIFF_INCLUDE_UNMODIFIED = (1 << 9),
/** Even with GIT_DIFF_INCLUDE_UNTRACKED, an entire untracked directory /** Even with GIT_DIFF_INCLUDE_UNTRACKED, an entire untracked directory
* will be marked with only a single entry in the diff list; this flag * will be marked with only a single entry in the diff list; this flag
* adds all files under the directory as UNTRACKED entries, too. * adds all files under the directory as UNTRACKED entries, too.
*/ */
GIT_DIFF_RECURSE_UNTRACKED_DIRS = (1 << 10), GIT_DIFF_RECURSE_UNTRACKED_DIRS = (1 << 10),
/** If the pathspec is set in the diff options, this flags means to /** If the pathspec is set in the diff options, this flags means to
* apply it as an exact match instead of as an fnmatch pattern. * apply it as an exact match instead of as an fnmatch pattern.
*/ */
GIT_DIFF_DISABLE_PATHSPEC_MATCH = (1 << 11), GIT_DIFF_DISABLE_PATHSPEC_MATCH = (1 << 11),
/** Use case insensitive filename comparisons */ /** Use case insensitive filename comparisons */
GIT_DIFF_DELTAS_ARE_ICASE = (1 << 12), GIT_DIFF_DELTAS_ARE_ICASE = (1 << 12),
/** When generating patch text, include the content of untracked files */ /** When generating patch text, include the content of untracked files */
GIT_DIFF_INCLUDE_UNTRACKED_CONTENT = (1 << 13), GIT_DIFF_INCLUDE_UNTRACKED_CONTENT = (1 << 13),
/** Disable updating of the `binary` flag in delta records. This is /** Disable updating of the `binary` flag in delta records. This is
* useful when iterating over a diff if you don't need hunk and data * useful when iterating over a diff if you don't need hunk and data
* callbacks and want to avoid having to load file completely. * callbacks and want to avoid having to load file completely.
*/ */
GIT_DIFF_SKIP_BINARY_CHECK = (1 << 14), GIT_DIFF_SKIP_BINARY_CHECK = (1 << 14),
/** Normally, a type change between files will be converted into a /** Normally, a type change between files will be converted into a
* DELETED record for the old and an ADDED record for the new; this * DELETED record for the old and an ADDED record for the new; this
* options enabled the generation of TYPECHANGE delta records. * options enabled the generation of TYPECHANGE delta records.
*/ */
GIT_DIFF_INCLUDE_TYPECHANGE = (1 << 15), GIT_DIFF_INCLUDE_TYPECHANGE = (1 << 15),
/** Even with GIT_DIFF_INCLUDE_TYPECHANGE, blob->tree changes still /** Even with GIT_DIFF_INCLUDE_TYPECHANGE, blob->tree changes still
* generally show as a DELETED blob. This flag tries to correctly * generally show as a DELETED blob. This flag tries to correctly
* label blob->tree transitions as TYPECHANGE records with new_file's * label blob->tree transitions as TYPECHANGE records with new_file's
* mode set to tree. Note: the tree SHA will not be available. * mode set to tree. Note: the tree SHA will not be available.
*/ */
GIT_DIFF_INCLUDE_TYPECHANGE_TREES = (1 << 16), GIT_DIFF_INCLUDE_TYPECHANGE_TREES = (1 << 16),
/** Ignore file mode changes */ /** Ignore file mode changes */
GIT_DIFF_IGNORE_FILEMODE = (1 << 17), GIT_DIFF_IGNORE_FILEMODE = (1 << 17),
/** Even with GIT_DIFF_INCLUDE_IGNORED, an entire ignored directory /** Even with GIT_DIFF_INCLUDE_IGNORED, an entire ignored directory
* will be marked with only a single entry in the diff list; this flag * will be marked with only a single entry in the diff list; this flag
* adds all files under the directory as IGNORED entries, too. * adds all files under the directory as IGNORED entries, too.
*/ */
GIT_DIFF_RECURSE_IGNORED_DIRS = (1 << 18), GIT_DIFF_RECURSE_IGNORED_DIRS = (1 << 18),
/** Core Git scans inside untracked directories, labeling them IGNORED
* if they are empty or only contain ignored files; a directory is
* consider UNTRACKED only if it has an actual untracked file in it.
* This scan is extra work for a case you often don't care about. This
* flag makes libgit2 immediately label an untracked directory as
* UNTRACKED without looking insde it (which differs from core Git).
* Of course, ignore rules are still checked for the directory itself.
*/
GIT_DIFF_FAST_UNTRACKED_DIRS = (1 << 19),
} git_diff_option_t; } git_diff_option_t;
/** /**
......
...@@ -103,20 +103,20 @@ typedef enum { ...@@ -103,20 +103,20 @@ typedef enum {
* * WD_UNTRACKED - wd contains untracked files * * WD_UNTRACKED - wd contains untracked files
*/ */
typedef enum { typedef enum {
GIT_SUBMODULE_STATUS_IN_HEAD = (1u << 0), GIT_SUBMODULE_STATUS_IN_HEAD = (1u << 0),
GIT_SUBMODULE_STATUS_IN_INDEX = (1u << 1), GIT_SUBMODULE_STATUS_IN_INDEX = (1u << 1),
GIT_SUBMODULE_STATUS_IN_CONFIG = (1u << 2), GIT_SUBMODULE_STATUS_IN_CONFIG = (1u << 2),
GIT_SUBMODULE_STATUS_IN_WD = (1u << 3), GIT_SUBMODULE_STATUS_IN_WD = (1u << 3),
GIT_SUBMODULE_STATUS_INDEX_ADDED = (1u << 4), GIT_SUBMODULE_STATUS_INDEX_ADDED = (1u << 4),
GIT_SUBMODULE_STATUS_INDEX_DELETED = (1u << 5), GIT_SUBMODULE_STATUS_INDEX_DELETED = (1u << 5),
GIT_SUBMODULE_STATUS_INDEX_MODIFIED = (1u << 6), GIT_SUBMODULE_STATUS_INDEX_MODIFIED = (1u << 6),
GIT_SUBMODULE_STATUS_WD_UNINITIALIZED = (1u << 7), GIT_SUBMODULE_STATUS_WD_UNINITIALIZED = (1u << 7),
GIT_SUBMODULE_STATUS_WD_ADDED = (1u << 8), GIT_SUBMODULE_STATUS_WD_ADDED = (1u << 8),
GIT_SUBMODULE_STATUS_WD_DELETED = (1u << 9), GIT_SUBMODULE_STATUS_WD_DELETED = (1u << 9),
GIT_SUBMODULE_STATUS_WD_MODIFIED = (1u << 10), GIT_SUBMODULE_STATUS_WD_MODIFIED = (1u << 10),
GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED = (1u << 11), GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED = (1u << 11),
GIT_SUBMODULE_STATUS_WD_WD_MODIFIED = (1u << 12), GIT_SUBMODULE_STATUS_WD_WD_MODIFIED = (1u << 12),
GIT_SUBMODULE_STATUS_WD_UNTRACKED = (1u << 13), GIT_SUBMODULE_STATUS_WD_UNTRACKED = (1u << 13),
} git_submodule_status_t; } git_submodule_status_t;
#define GIT_SUBMODULE_STATUS__IN_FLAGS \ #define GIT_SUBMODULE_STATUS__IN_FLAGS \
......
...@@ -327,8 +327,7 @@ static git_diff_list *diff_list_alloc( ...@@ -327,8 +327,7 @@ static git_diff_list *diff_list_alloc(
/* Use case-insensitive compare if either iterator has /* Use case-insensitive compare if either iterator has
* the ignore_case bit set */ * the ignore_case bit set */
if (!git_iterator_ignore_case(old_iter) && if (!git_iterator_ignore_case(old_iter) &&
!git_iterator_ignore_case(new_iter)) !git_iterator_ignore_case(new_iter)) {
{
diff->opts.flags &= ~GIT_DIFF_DELTAS_ARE_ICASE; diff->opts.flags &= ~GIT_DIFF_DELTAS_ARE_ICASE;
diff->strcomp = git__strcmp; diff->strcomp = git__strcmp;
...@@ -530,24 +529,30 @@ cleanup: ...@@ -530,24 +529,30 @@ cleanup:
return result; return result;
} }
typedef struct {
git_repository *repo;
git_iterator *old_iter;
git_iterator *new_iter;
const git_index_entry *oitem;
const git_index_entry *nitem;
git_buf ignore_prefix;
} diff_in_progress;
#define MODE_BITS_MASK 0000777 #define MODE_BITS_MASK 0000777
static int maybe_modified( static int maybe_modified(
git_iterator *old_iter, git_diff_list *diff,
const git_index_entry *oitem, diff_in_progress *info)
git_iterator *new_iter,
const git_index_entry *nitem,
git_diff_list *diff)
{ {
git_oid noid, *use_noid = NULL; git_oid noid, *use_noid = NULL;
git_delta_t status = GIT_DELTA_MODIFIED; git_delta_t status = GIT_DELTA_MODIFIED;
const git_index_entry *oitem = info->oitem;
const git_index_entry *nitem = info->nitem;
unsigned int omode = oitem->mode; unsigned int omode = oitem->mode;
unsigned int nmode = nitem->mode; unsigned int nmode = nitem->mode;
bool new_is_workdir = (new_iter->type == GIT_ITERATOR_TYPE_WORKDIR); bool new_is_workdir = (info->new_iter->type == GIT_ITERATOR_TYPE_WORKDIR);
const char *matched_pathspec; const char *matched_pathspec;
GIT_UNUSED(old_iter);
if (!git_pathspec_match_path( if (!git_pathspec_match_path(
&diff->pathspec, oitem->path, &diff->pathspec, oitem->path,
DIFF_FLAG_IS_SET(diff, GIT_DIFF_DISABLE_PATHSPEC_MATCH), DIFF_FLAG_IS_SET(diff, GIT_DIFF_DISABLE_PATHSPEC_MATCH),
...@@ -692,208 +697,311 @@ static bool entry_is_prefixed( ...@@ -692,208 +697,311 @@ static bool entry_is_prefixed(
item->path[pathlen] == '/'); item->path[pathlen] == '/');
} }
int git_diff__from_iterators( static int diff_scan_inside_untracked_dir(
git_diff_list **diff_ptr, git_diff_list *diff, diff_in_progress *info, git_delta_t *delta_type)
git_repository *repo,
git_iterator *old_iter,
git_iterator *new_iter,
const git_diff_options *opts)
{ {
int error = 0; int error = 0;
const git_index_entry *oitem, *nitem; git_buf base = GIT_BUF_INIT;
git_buf ignore_prefix = GIT_BUF_INIT; bool is_ignored;
git_diff_list *diff;
*diff_ptr = NULL; *delta_type = GIT_DELTA_IGNORED;
git_buf_sets(&base, info->nitem->path);
diff = diff_list_alloc(repo, old_iter, new_iter); /* advance into untracked directory */
GITERR_CHECK_ALLOC(diff); if ((error = git_iterator_advance_into(&info->nitem, info->new_iter)) < 0) {
/* make iterators have matching icase behavior */ /* skip ahead if empty */
if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE)) { if (error == GIT_ENOTFOUND) {
if (git_iterator_set_ignore_case(old_iter, true) < 0 || giterr_clear();
git_iterator_set_ignore_case(new_iter, true) < 0) error = git_iterator_advance(&info->nitem, info->new_iter);
goto fail; }
return error;
} }
if (diff_list_apply_options(diff, opts) < 0 || /* look for actual untracked file */
git_iterator_current(&oitem, old_iter) < 0 || while (!diff->pfxcomp(info->nitem->path, git_buf_cstr(&base))) {
git_iterator_current(&nitem, new_iter) < 0) is_ignored = git_iterator_current_is_ignored(info->new_iter);
goto fail;
/* run iterators building diffs */ /* need to recurse into non-ignored directories */
while (oitem || nitem) { if (!is_ignored && S_ISDIR(info->nitem->mode)) {
int cmp = oitem ? (nitem ? diff->entrycomp(oitem, nitem) : -1) : 1; if ((error = git_iterator_advance_into(
&info->nitem, info->new_iter)) < 0)
break;
continue;
}
/* create DELETED records for old items not matched in new */ /* found a non-ignored item - treat parent dir as untracked */
if (cmp < 0) { if (!is_ignored) {
if (diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem) < 0) *delta_type = GIT_DELTA_UNTRACKED;
goto fail; break;
}
/* if we are generating TYPECHANGE records then check for that if ((error = git_iterator_advance(&info->nitem, info->new_iter)) < 0)
* instead of just generating a DELETE record break;
*/ }
if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) &&
entry_is_prefixed(diff, nitem, oitem))
{
/* this entry has become a tree! convert to TYPECHANGE */
git_diff_delta *last = diff_delta__last_for_item(diff, oitem);
if (last) {
last->status = GIT_DELTA_TYPECHANGE;
last->new_file.mode = GIT_FILEMODE_TREE;
}
/* If new_iter is a workdir iterator, then this situation /* finish off scan */
* will certainly be followed by a series of untracked items. while (!diff->pfxcomp(info->nitem->path, git_buf_cstr(&base))) {
* Unless RECURSE_UNTRACKED_DIRS is set, skip over them... if ((error = git_iterator_advance(&info->nitem, info->new_iter)) < 0)
*/ break;
if (S_ISDIR(nitem->mode) && }
DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS))
{
if (git_iterator_advance(&nitem, new_iter) < 0)
goto fail;
}
}
if (git_iterator_advance(&oitem, old_iter) < 0) git_buf_free(&base);
goto fail;
}
/* create ADDED, TRACKED, or IGNORED records for new items not return error;
* matched in old (and/or descend into directories as needed) }
*/
else if (cmp > 0) {
git_delta_t delta_type = GIT_DELTA_UNTRACKED;
bool contains_oitem = entry_is_prefixed(diff, oitem, nitem);
/* check if contained in ignored parent directory */
if (git_buf_len(&ignore_prefix) &&
diff->pfxcomp(nitem->path, git_buf_cstr(&ignore_prefix)) == 0)
delta_type = GIT_DELTA_IGNORED;
if (S_ISDIR(nitem->mode)) {
/* recurse into directory only if there are tracked items in
* it or if the user requested the contents of untracked
* directories and it is not under an ignored directory.
*/
bool recurse_into_dir =
(delta_type == GIT_DELTA_UNTRACKED &&
DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) ||
(delta_type == GIT_DELTA_IGNORED &&
DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS));
/* do not advance into directories that contain a .git file */
if (!contains_oitem && recurse_into_dir) {
git_buf *full = NULL;
if (git_iterator_current_workdir_path(&full, new_iter) < 0)
goto fail;
if (git_path_contains_dir(full, DOT_GIT))
recurse_into_dir = false;
}
/* if directory is ignored, remember ignore_prefix */ static int handle_unmatched_new_item(
if ((contains_oitem || recurse_into_dir) && git_diff_list *diff, diff_in_progress *info)
delta_type == GIT_DELTA_UNTRACKED && {
git_iterator_current_is_ignored(new_iter)) int error = 0;
{ const git_index_entry *nitem = info->nitem;
git_buf_sets(&ignore_prefix, nitem->path); git_delta_t delta_type = GIT_DELTA_UNTRACKED;
delta_type = GIT_DELTA_IGNORED; bool contains_oitem;
/* skip recursion if we've just learned this is ignored */ /* check if this is a prefix of the other side */
if (DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)) contains_oitem = entry_is_prefixed(diff, info->oitem, nitem);
recurse_into_dir = false;
}
if (contains_oitem || recurse_into_dir) { /* check if this is contained in an ignored parent directory */
/* advance into directory */ if (git_buf_len(&info->ignore_prefix)) {
error = git_iterator_advance_into(&nitem, new_iter); if (diff->pfxcomp(nitem->path, git_buf_cstr(&info->ignore_prefix)) == 0)
delta_type = GIT_DELTA_IGNORED;
else
git_buf_clear(&info->ignore_prefix);
}
/* if directory is empty, can't advance into it, so skip */ if (S_ISDIR(nitem->mode)) {
if (error == GIT_ENOTFOUND) { bool recurse_into_dir = contains_oitem;
giterr_clear();
error = git_iterator_advance(&nitem, new_iter);
git_buf_clear(&ignore_prefix); /* if not already inside an ignored dir, check if this is ignored */
} if (delta_type != GIT_DELTA_IGNORED &&
git_iterator_current_is_ignored(info->new_iter)) {
delta_type = GIT_DELTA_IGNORED;
git_buf_sets(&info->ignore_prefix, nitem->path);
}
if (error < 0) /* check if user requests recursion into this type of dir */
goto fail; recurse_into_dir = contains_oitem ||
continue; (delta_type == GIT_DELTA_UNTRACKED &&
DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) ||
(delta_type == GIT_DELTA_IGNORED &&
DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS));
/* do not advance into directories that contain a .git file */
if (recurse_into_dir) {
git_buf *full = NULL;
if (git_iterator_current_workdir_path(&full, info->new_iter) < 0)
return -1;
if (full && git_path_contains_dir(full, DOT_GIT))
recurse_into_dir = false;
}
/* still have to look into untracked directories to match core git -
* with no untracked files, directory is treated as ignored
*/
if (!recurse_into_dir &&
delta_type == GIT_DELTA_UNTRACKED &&
DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_FAST_UNTRACKED_DIRS))
{
git_diff_delta *last;
/* attempt to insert record for this directory */
if ((error = diff_delta__from_one(diff, delta_type, nitem)) < 0)
return error;
/* if delta wasn't created (because of rules), just skip ahead */
last = diff_delta__last_for_item(diff, nitem);
if (!last)
return git_iterator_advance(&info->nitem, info->new_iter);
/* iterate into dir looking for an actual untracked file */
if (diff_scan_inside_untracked_dir(diff, info, &delta_type) < 0)
return -1;
/* it iteration changed delta type, the update the record */
if (delta_type == GIT_DELTA_IGNORED) {
last->status = GIT_DELTA_IGNORED;
/* remove the record if we don't want ignored records */
if (DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_IGNORED)) {
git_vector_pop(&diff->deltas);
git__free(last);
} }
} }
/* In core git, the next two "else if" clauses are effectively return 0;
* reversed -- i.e. when an untracked file contained in an }
* ignored directory is individually ignored, it shows up as an
* ignored file in the diff list, even though other untracked /* try to advance into directory if necessary */
* files in the same directory are skipped completely. if (recurse_into_dir) {
* error = git_iterator_advance_into(&info->nitem, info->new_iter);
* To me, this is odd. If the directory is ignored and the file
* is untracked, we should skip it consistently, regardless of /* if real error or no error, proceed with iteration */
* whether it happens to match a pattern in the ignore file. if (error != GIT_ENOTFOUND)
* return error;
* To match the core git behavior, just reverse the following giterr_clear();
* two "else if" cases so that individual file ignores are
* checked before container directory exclusions are used to /* if directory is empty, can't advance into it, so either skip
* skip the file. * it or ignore it
*/ */
else if (delta_type == GIT_DELTA_IGNORED && if (contains_oitem)
DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)) { return git_iterator_advance(&info->nitem, info->new_iter);
if (git_iterator_advance(&nitem, new_iter) < 0) delta_type = GIT_DELTA_IGNORED;
goto fail; }
continue; /* ignored parent directory, so skip completely */ }
}
else if (git_iterator_current_is_ignored(new_iter)) /* In core git, the next two checks are effectively reversed --
delta_type = GIT_DELTA_IGNORED; * i.e. when an file contained in an ignored directory is explicitly
* ignored, it shows up as an ignored file in the diff list, even though
* other untracked files in the same directory are skipped completely.
*
* To me, this seems odd. If the directory is ignored and the file is
* untracked, we should skip it consistently, regardless of whether it
* happens to match a pattern in the ignore file.
*
* To match the core git behavior, reverse the following two if checks
* so that individual file ignores are checked before container
* directory exclusions are used to skip the file.
*/
else if (delta_type == GIT_DELTA_IGNORED &&
DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS))
/* item contained in ignored directory, so skip over it */
return git_iterator_advance(&info->nitem, info->new_iter);
else if (new_iter->type != GIT_ITERATOR_TYPE_WORKDIR) else if (git_iterator_current_is_ignored(info->new_iter))
delta_type = GIT_DELTA_ADDED; delta_type = GIT_DELTA_IGNORED;
if (diff_delta__from_one(diff, delta_type, nitem) < 0) else if (info->new_iter->type != GIT_ITERATOR_TYPE_WORKDIR)
goto fail; delta_type = GIT_DELTA_ADDED;
/* if we are generating TYPECHANGE records then check for that /* Actually create the record for this item if necessary */
* instead of just generating an ADDED/UNTRACKED record if ((error = diff_delta__from_one(diff, delta_type, nitem)) < 0)
*/ return error;
if (delta_type != GIT_DELTA_IGNORED &&
DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) &&
contains_oitem)
{
/* this entry was prefixed with a tree - make TYPECHANGE */
git_diff_delta *last = diff_delta__last_for_item(diff, nitem);
if (last) {
last->status = GIT_DELTA_TYPECHANGE;
last->old_file.mode = GIT_FILEMODE_TREE;
}
}
if (git_iterator_advance(&nitem, new_iter) < 0) /* If user requested TYPECHANGE records, then check for that instead of
goto fail; * just generating an ADDED/UNTRACKED record
*/
if (delta_type != GIT_DELTA_IGNORED &&
DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) &&
contains_oitem)
{
/* this entry was prefixed with a tree - make TYPECHANGE */
git_diff_delta *last = diff_delta__last_for_item(diff, nitem);
if (last) {
last->status = GIT_DELTA_TYPECHANGE;
last->old_file.mode = GIT_FILEMODE_TREE;
} }
}
/* otherwise item paths match, so create MODIFIED record return git_iterator_advance(&info->nitem, info->new_iter);
* (or ADDED and DELETED pair if type changed) }
*/
else {
assert(oitem && nitem && cmp == 0);
if (maybe_modified(old_iter, oitem, new_iter, nitem, diff) < 0 || static int handle_unmatched_old_item(
git_iterator_advance(&oitem, old_iter) < 0 || git_diff_list *diff, diff_in_progress *info)
git_iterator_advance(&nitem, new_iter) < 0) {
goto fail; int error = diff_delta__from_one(diff, GIT_DELTA_DELETED, info->oitem);
if (error < 0)
return error;
/* if we are generating TYPECHANGE records then check for that
* instead of just generating a DELETE record
*/
if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) &&
entry_is_prefixed(diff, info->nitem, info->oitem))
{
/* this entry has become a tree! convert to TYPECHANGE */
git_diff_delta *last = diff_delta__last_for_item(diff, info->oitem);
if (last) {
last->status = GIT_DELTA_TYPECHANGE;
last->new_file.mode = GIT_FILEMODE_TREE;
} }
/* If new_iter is a workdir iterator, then this situation
* will certainly be followed by a series of untracked items.
* Unless RECURSE_UNTRACKED_DIRS is set, skip over them...
*/
if (S_ISDIR(info->nitem->mode) &&
DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS))
return git_iterator_advance(&info->nitem, info->new_iter);
} }
*diff_ptr = diff; return git_iterator_advance(&info->oitem, info->old_iter);
}
static int handle_matched_item(
git_diff_list *diff, diff_in_progress *info)
{
int error = 0;
fail: if (!(error = maybe_modified(diff, info)) &&
if (!*diff_ptr) { !(error = git_iterator_advance(&info->oitem, info->old_iter)))
git_diff_list_free(diff); error = git_iterator_advance(&info->nitem, info->new_iter);
error = -1;
return error;
}
int git_diff__from_iterators(
git_diff_list **diff_ptr,
git_repository *repo,
git_iterator *old_iter,
git_iterator *new_iter,
const git_diff_options *opts)
{
int error = 0;
diff_in_progress info;
git_diff_list *diff;
*diff_ptr = NULL;
diff = diff_list_alloc(repo, old_iter, new_iter);
GITERR_CHECK_ALLOC(diff);
info.repo = repo;
info.old_iter = old_iter;
info.new_iter = new_iter;
git_buf_init(&info.ignore_prefix, 0);
/* make iterators have matching icase behavior */
if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE)) {
if (!(error = git_iterator_set_ignore_case(old_iter, true)))
error = git_iterator_set_ignore_case(new_iter, true);
}
/* finish initialization */
if (!error &&
!(error = diff_list_apply_options(diff, opts)) &&
!(error = git_iterator_current(&info.oitem, old_iter)))
error = git_iterator_current(&info.nitem, new_iter);
/* run iterators building diffs */
while (!error && (info.oitem || info.nitem)) {
int cmp = info.oitem ?
(info.nitem ? diff->entrycomp(info.oitem, info.nitem) : -1) : 1;
/* create DELETED records for old items not matched in new */
if (cmp < 0)
error = handle_unmatched_old_item(diff, &info);
/* create ADDED, TRACKED, or IGNORED records for new items not
* matched in old (and/or descend into directories as needed)
*/
else if (cmp > 0)
error = handle_unmatched_new_item(diff, &info);
/* otherwise item paths match, so create MODIFIED record
* (or ADDED and DELETED pair if type changed)
*/
else
error = handle_matched_item(diff, &info);
} }
git_buf_free(&ignore_prefix); if (!error)
*diff_ptr = diff;
else
git_diff_list_free(diff);
git_buf_free(&info.ignore_prefix);
return error; return error;
} }
......
...@@ -101,8 +101,8 @@ static bool diff_delta_is_binary_forced( ...@@ -101,8 +101,8 @@ static bool diff_delta_is_binary_forced(
/* make sure files are conceivably mmap-able */ /* make sure files are conceivably mmap-able */
if ((git_off_t)((size_t)delta->old_file.size) != delta->old_file.size || if ((git_off_t)((size_t)delta->old_file.size) != delta->old_file.size ||
(git_off_t)((size_t)delta->new_file.size) != delta->new_file.size) (git_off_t)((size_t)delta->new_file.size) != delta->new_file.size) {
{
delta->old_file.flags |= GIT_DIFF_FLAG_BINARY; delta->old_file.flags |= GIT_DIFF_FLAG_BINARY;
delta->new_file.flags |= GIT_DIFF_FLAG_BINARY; delta->new_file.flags |= GIT_DIFF_FLAG_BINARY;
delta->flags |= GIT_DIFF_FLAG_BINARY; delta->flags |= GIT_DIFF_FLAG_BINARY;
...@@ -232,8 +232,7 @@ static int get_blob_content( ...@@ -232,8 +232,7 @@ static int get_blob_content(
if (git_oid_iszero(&file->oid)) if (git_oid_iszero(&file->oid))
return 0; return 0;
if (file->mode == GIT_FILEMODE_COMMIT) if (file->mode == GIT_FILEMODE_COMMIT) {
{
char oidstr[GIT_OID_HEXSZ+1]; char oidstr[GIT_OID_HEXSZ+1];
git_buf content = GIT_BUF_INIT; git_buf content = GIT_BUF_INIT;
...@@ -299,8 +298,8 @@ static int get_workdir_sm_content( ...@@ -299,8 +298,8 @@ static int get_workdir_sm_content(
char oidstr[GIT_OID_HEXSZ+1]; char oidstr[GIT_OID_HEXSZ+1];
if ((error = git_submodule_lookup(&sm, ctxt->repo, file->path)) < 0 || if ((error = git_submodule_lookup(&sm, ctxt->repo, file->path)) < 0 ||
(error = git_submodule_status(&sm_status, sm)) < 0) (error = git_submodule_status(&sm_status, sm)) < 0) {
{
/* GIT_EEXISTS means a "submodule" that has not been git added */ /* GIT_EEXISTS means a "submodule" that has not been git added */
if (error == GIT_EEXISTS) if (error == GIT_EEXISTS)
error = 0; error = 0;
...@@ -312,8 +311,8 @@ static int get_workdir_sm_content( ...@@ -312,8 +311,8 @@ static int get_workdir_sm_content(
const git_oid* sm_head; const git_oid* sm_head;
if ((sm_head = git_submodule_wd_id(sm)) != NULL || if ((sm_head = git_submodule_wd_id(sm)) != NULL ||
(sm_head = git_submodule_head_id(sm)) != NULL) (sm_head = git_submodule_head_id(sm)) != NULL) {
{
git_oid_cpy(&file->oid, sm_head); git_oid_cpy(&file->oid, sm_head);
file->flags |= GIT_DIFF_FLAG_VALID_OID; file->flags |= GIT_DIFF_FLAG_VALID_OID;
} }
...@@ -660,8 +659,8 @@ static int diff_patch_load( ...@@ -660,8 +659,8 @@ static int diff_patch_load(
*/ */
if (check_if_unmodified && if (check_if_unmodified &&
delta->old_file.mode == delta->new_file.mode && delta->old_file.mode == delta->new_file.mode &&
!git_oid__cmp(&delta->old_file.oid, &delta->new_file.oid)) !git_oid__cmp(&delta->old_file.oid, &delta->new_file.oid)) {
{
delta->status = GIT_DELTA_UNMODIFIED; delta->status = GIT_DELTA_UNMODIFIED;
if ((ctxt->opts->flags & GIT_DIFF_INCLUDE_UNMODIFIED) == 0) if ((ctxt->opts->flags & GIT_DIFF_INCLUDE_UNMODIFIED) == 0)
...@@ -1049,6 +1048,12 @@ char git_diff_status_char(git_delta_t status) ...@@ -1049,6 +1048,12 @@ char git_diff_status_char(git_delta_t status)
return code; return code;
} }
static int callback_error(void)
{
giterr_clear();
return GIT_EUSER;
}
static int print_compact( static int print_compact(
const git_diff_delta *delta, float progress, void *data) const git_diff_delta *delta, float progress, void *data)
{ {
...@@ -1083,10 +1088,7 @@ static int print_compact( ...@@ -1083,10 +1088,7 @@ static int print_compact(
if (pi->print_cb(delta, NULL, GIT_DIFF_LINE_FILE_HDR, if (pi->print_cb(delta, NULL, GIT_DIFF_LINE_FILE_HDR,
git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload))
{ return callback_error();
giterr_clear();
return GIT_EUSER;
}
return 0; return 0;
} }
...@@ -1200,10 +1202,7 @@ static int print_patch_file( ...@@ -1200,10 +1202,7 @@ static int print_patch_file(
if (pi->print_cb(delta, NULL, GIT_DIFF_LINE_FILE_HDR, if (pi->print_cb(delta, NULL, GIT_DIFF_LINE_FILE_HDR,
git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload))
{ return callback_error();
giterr_clear();
return GIT_EUSER;
}
if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0) if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0)
return 0; return 0;
...@@ -1217,10 +1216,7 @@ static int print_patch_file( ...@@ -1217,10 +1216,7 @@ static int print_patch_file(
if (pi->print_cb(delta, NULL, GIT_DIFF_LINE_BINARY, if (pi->print_cb(delta, NULL, GIT_DIFF_LINE_BINARY,
git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload))
{ return callback_error();
giterr_clear();
return GIT_EUSER;
}
return 0; return 0;
} }
...@@ -1243,10 +1239,7 @@ static int print_patch_hunk( ...@@ -1243,10 +1239,7 @@ static int print_patch_hunk(
if (pi->print_cb(d, r, GIT_DIFF_LINE_HUNK_HDR, if (pi->print_cb(d, r, GIT_DIFF_LINE_HUNK_HDR,
git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload))
{ return callback_error();
giterr_clear();
return GIT_EUSER;
}
return 0; return 0;
} }
...@@ -1278,10 +1271,7 @@ static int print_patch_line( ...@@ -1278,10 +1271,7 @@ static int print_patch_line(
if (pi->print_cb(delta, range, line_origin, if (pi->print_cb(delta, range, line_origin,
git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload))
{ return callback_error();
giterr_clear();
return GIT_EUSER;
}
return 0; return 0;
} }
......
...@@ -277,15 +277,13 @@ void git_vector_swap(git_vector *a, git_vector *b) ...@@ -277,15 +277,13 @@ void git_vector_swap(git_vector *a, git_vector *b)
int git_vector_resize_to(git_vector *v, size_t new_length) int git_vector_resize_to(git_vector *v, size_t new_length)
{ {
if (new_length <= v->length)
return 0;
if (new_length > v->_alloc_size && if (new_length > v->_alloc_size &&
resize_vector(v, new_length) < 0) resize_vector(v, new_length) < 0)
return -1; return -1;
memset(&v->contents[v->length], 0, if (new_length > v->length)
sizeof(void *) * (new_length - v->length)); memset(&v->contents[v->length], 0,
sizeof(void *) * (new_length - v->length));
v->length = new_length; v->length = new_length;
......
...@@ -28,7 +28,15 @@ int diff_file_cb( ...@@ -28,7 +28,15 @@ int diff_file_cb(
{ {
diff_expects *e = payload; diff_expects *e = payload;
GIT_UNUSED(progress); if (e->debug)
fprintf(stderr, "%c %s (%.3f)\n",
git_diff_status_char(delta->status),
delta->old_file.path, progress);
if (e->names)
cl_assert_equal_s(e->names[e->files], delta->old_file.path);
if (e->statuses)
cl_assert_equal_i(e->statuses[e->files], (int)delta->status);
e->files++; e->files++;
......
...@@ -18,6 +18,13 @@ typedef struct { ...@@ -18,6 +18,13 @@ typedef struct {
int line_ctxt; int line_ctxt;
int line_adds; int line_adds;
int line_dels; int line_dels;
/* optional arrays of expected specific values */
const char **names;
int *statuses;
int debug;
} diff_expects; } diff_expects;
typedef struct { typedef struct {
......
...@@ -1033,3 +1033,190 @@ void test_diff_workdir__to_tree_issue_1397(void) ...@@ -1033,3 +1033,190 @@ void test_diff_workdir__to_tree_issue_1397(void)
git_diff_list_free(diff); git_diff_list_free(diff);
git_tree_free(a); git_tree_free(a);
} }
void test_diff_workdir__untracked_directory_scenarios(void)
{
git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
git_diff_list *diff = NULL;
diff_expects exp;
char *pathspec = NULL;
static const char *files0[] = {
"subdir/deleted_file",
"subdir/modified_file",
"subdir/new_file",
NULL
};
static const char *files1[] = {
"subdir/deleted_file",
"subdir/directory/",
"subdir/modified_file",
"subdir/new_file",
NULL
};
static const char *files2[] = {
"subdir/deleted_file",
"subdir/directory/more/notignored",
"subdir/modified_file",
"subdir/new_file",
NULL
};
g_repo = cl_git_sandbox_init("status");
cl_git_mkfile("status/.gitignore", "ignored\n");
opts.context_lines = 3;
opts.interhunk_lines = 1;
opts.flags |= GIT_DIFF_INCLUDE_IGNORED | GIT_DIFF_INCLUDE_UNTRACKED;
opts.pathspec.strings = &pathspec;
opts.pathspec.count = 1;
pathspec = "subdir";
/* baseline for "subdir" pathspec */
memset(&exp, 0, sizeof(exp));
exp.names = files0;
cl_git_pass(git_diff_index_to_workdir(&diff, g_repo, NULL, &opts));
cl_git_pass(git_diff_foreach(diff, diff_file_cb, NULL, NULL, &exp));
cl_assert_equal_i(3, exp.files);
cl_assert_equal_i(0, exp.file_status[GIT_DELTA_ADDED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_DELETED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_MODIFIED]);
cl_assert_equal_i(0, exp.file_status[GIT_DELTA_IGNORED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_UNTRACKED]);
git_diff_list_free(diff);
/* empty directory */
cl_git_pass(p_mkdir("status/subdir/directory", 0777));
memset(&exp, 0, sizeof(exp));
exp.names = files1;
cl_git_pass(git_diff_index_to_workdir(&diff, g_repo, NULL, &opts));
cl_git_pass(git_diff_foreach(diff, diff_file_cb, NULL, NULL, &exp));
cl_assert_equal_i(4, exp.files);
cl_assert_equal_i(0, exp.file_status[GIT_DELTA_ADDED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_DELETED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_MODIFIED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_IGNORED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_UNTRACKED]);
git_diff_list_free(diff);
/* directory with only ignored files */
cl_git_pass(p_mkdir("status/subdir/directory/deeper", 0777));
cl_git_mkfile("status/subdir/directory/deeper/ignored", "ignore me\n");
cl_git_pass(p_mkdir("status/subdir/directory/another", 0777));
cl_git_mkfile("status/subdir/directory/another/ignored", "ignore me\n");
memset(&exp, 0, sizeof(exp));
exp.names = files1;
cl_git_pass(git_diff_index_to_workdir(&diff, g_repo, NULL, &opts));
cl_git_pass(git_diff_foreach(diff, diff_file_cb, NULL, NULL, &exp));
cl_assert_equal_i(4, exp.files);
cl_assert_equal_i(0, exp.file_status[GIT_DELTA_ADDED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_DELETED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_MODIFIED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_IGNORED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_UNTRACKED]);
git_diff_list_free(diff);
/* directory with ignored directory (contents irrelevant) */
cl_git_pass(p_mkdir("status/subdir/directory/more", 0777));
cl_git_pass(p_mkdir("status/subdir/directory/more/ignored", 0777));
cl_git_mkfile("status/subdir/directory/more/ignored/notignored",
"inside ignored dir\n");
memset(&exp, 0, sizeof(exp));
exp.names = files1;
cl_git_pass(git_diff_index_to_workdir(&diff, g_repo, NULL, &opts));
cl_git_pass(git_diff_foreach(diff, diff_file_cb, NULL, NULL, &exp));
cl_assert_equal_i(4, exp.files);
cl_assert_equal_i(0, exp.file_status[GIT_DELTA_ADDED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_DELETED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_MODIFIED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_IGNORED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_UNTRACKED]);
git_diff_list_free(diff);
/* quick version avoids directory scan */
opts.flags = opts.flags | GIT_DIFF_FAST_UNTRACKED_DIRS;
memset(&exp, 0, sizeof(exp));
exp.names = files1;
cl_git_pass(git_diff_index_to_workdir(&diff, g_repo, NULL, &opts));
cl_git_pass(git_diff_foreach(diff, diff_file_cb, NULL, NULL, &exp));
cl_assert_equal_i(4, exp.files);
cl_assert_equal_i(0, exp.file_status[GIT_DELTA_ADDED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_DELETED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_MODIFIED]);
cl_assert_equal_i(0, exp.file_status[GIT_DELTA_IGNORED]);
cl_assert_equal_i(2, exp.file_status[GIT_DELTA_UNTRACKED]);
git_diff_list_free(diff);
/* directory with nested non-ignored content */
opts.flags = opts.flags & ~GIT_DIFF_FAST_UNTRACKED_DIRS;
cl_git_mkfile("status/subdir/directory/more/notignored",
"not ignored deep under untracked\n");
memset(&exp, 0, sizeof(exp));
exp.names = files1;
cl_git_pass(git_diff_index_to_workdir(&diff, g_repo, NULL, &opts));
cl_git_pass(git_diff_foreach(diff, diff_file_cb, NULL, NULL, &exp));
cl_assert_equal_i(4, exp.files);
cl_assert_equal_i(0, exp.file_status[GIT_DELTA_ADDED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_DELETED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_MODIFIED]);
cl_assert_equal_i(0, exp.file_status[GIT_DELTA_IGNORED]);
cl_assert_equal_i(2, exp.file_status[GIT_DELTA_UNTRACKED]);
git_diff_list_free(diff);
/* use RECURSE_UNTRACKED_DIRS to get actual untracked files (no ignores) */
opts.flags = opts.flags & ~GIT_DIFF_INCLUDE_IGNORED;
opts.flags = opts.flags | GIT_DIFF_RECURSE_UNTRACKED_DIRS;
memset(&exp, 0, sizeof(exp));
exp.names = files2;
cl_git_pass(git_diff_index_to_workdir(&diff, g_repo, NULL, &opts));
cl_git_pass(git_diff_foreach(diff, diff_file_cb, NULL, NULL, &exp));
cl_assert_equal_i(4, exp.files);
cl_assert_equal_i(0, exp.file_status[GIT_DELTA_ADDED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_DELETED]);
cl_assert_equal_i(1, exp.file_status[GIT_DELTA_MODIFIED]);
cl_assert_equal_i(0, exp.file_status[GIT_DELTA_IGNORED]);
cl_assert_equal_i(2, exp.file_status[GIT_DELTA_UNTRACKED]);
git_diff_list_free(diff);
}
...@@ -40,7 +40,8 @@ int cb_status__single(const char *p, unsigned int s, void *payload) ...@@ -40,7 +40,8 @@ int cb_status__single(const char *p, unsigned int s, void *payload)
{ {
status_entry_single *data = (status_entry_single *)payload; status_entry_single *data = (status_entry_single *)payload;
GIT_UNUSED(p); if (data->debug)
fprintf(stderr, "%02d: %s (%04x)\n", data->count, p, s);
data->count++; data->count++;
data->status = s; data->status = s;
......
...@@ -24,6 +24,7 @@ extern int cb_status__count(const char *p, unsigned int s, void *payload); ...@@ -24,6 +24,7 @@ extern int cb_status__count(const char *p, unsigned int s, void *payload);
typedef struct { typedef struct {
int count; int count;
unsigned int status; unsigned int status;
bool debug;
} status_entry_single; } status_entry_single;
/* cb_status__single takes payload of "status_entry_single *" */ /* cb_status__single takes payload of "status_entry_single *" */
......
...@@ -258,9 +258,8 @@ void test_status_worktree__ignores(void) ...@@ -258,9 +258,8 @@ void test_status_worktree__ignores(void)
static int cb_status__check_592(const char *p, unsigned int s, void *payload) static int cb_status__check_592(const char *p, unsigned int s, void *payload)
{ {
GIT_UNUSED(payload); if (s != GIT_STATUS_WT_DELETED ||
(payload != NULL && strcmp(p, (const char *)payload) != 0))
if (s != GIT_STATUS_WT_DELETED || (payload != NULL && strcmp(p, (const char *)payload) != 0))
return -1; return -1;
return 0; return 0;
......
...@@ -383,3 +383,30 @@ void test_submodule_status__iterator(void) ...@@ -383,3 +383,30 @@ void test_submodule_status__iterator(void)
cl_git_pass(git_status_foreach_ext(g_repo, &opts, confirm_submodule_status, &exp)); cl_git_pass(git_status_foreach_ext(g_repo, &opts, confirm_submodule_status, &exp));
} }
void test_submodule_status__untracked_dirs_containing_ignored_files(void)
{
git_buf path = GIT_BUF_INIT;
unsigned int status, expected;
git_submodule *sm;
cl_git_pass(git_buf_joinpath(&path, git_repository_path(g_repo), "modules/sm_unchanged/info/exclude"));
cl_git_append2file(git_buf_cstr(&path), "\n*.ignored\n");
cl_git_pass(git_buf_joinpath(&path, git_repository_workdir(g_repo), "sm_unchanged/directory"));
cl_git_pass(git_futils_mkdir(git_buf_cstr(&path), NULL, 0755, 0));
cl_git_pass(git_buf_joinpath(&path, git_buf_cstr(&path), "i_am.ignored"));
cl_git_mkfile(git_buf_cstr(&path), "ignored this file, please\n");
cl_git_pass(git_submodule_lookup(&sm, g_repo, "sm_unchanged"));
cl_git_pass(git_submodule_status(&status, sm));
cl_assert(GIT_SUBMODULE_STATUS_IS_UNMODIFIED(status));
expected = GIT_SUBMODULE_STATUS_IN_HEAD |
GIT_SUBMODULE_STATUS_IN_INDEX |
GIT_SUBMODULE_STATUS_IN_CONFIG |
GIT_SUBMODULE_STATUS_IN_WD;
cl_assert(status == expected);
}
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