diff options
author | Vicent Marti <tanoku@gmail.com> | 2013-04-16 19:46:41 +0400 |
---|---|---|
committer | Vicent Marti <tanoku@gmail.com> | 2013-04-16 19:46:41 +0400 |
commit | a50086d174658914d4d6462afbc83b02825b1f5b (patch) | |
tree | e8daa1c7bf678222cf351445179837bed7db3a72 /src/diff.c | |
parent | 5b9fac39d8a76b9139667c26a63e6b3f204b3977 (diff) | |
parent | f124ebd457bfbf43de3516629aaba5a279636e04 (diff) |
Merge branch 'development'v0.18.0
Diffstat (limited to 'src/diff.c')
-rw-r--r-- | src/diff.c | 914 |
1 files changed, 533 insertions, 381 deletions
diff --git a/src/diff.c b/src/diff.c index 0b2f8fb50..37c89f3f1 100644 --- a/src/diff.c +++ b/src/diff.c @@ -1,74 +1,19 @@ /* - * Copyright (C) 2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ #include "common.h" -#include "git2/diff.h" #include "diff.h" #include "fileops.h" #include "config.h" #include "attr_file.h" +#include "filter.h" +#include "pathspec.h" -static char *diff_prefix_from_pathspec(const git_strarray *pathspec) -{ - git_buf prefix = GIT_BUF_INIT; - const char *scan; - - if (git_buf_common_prefix(&prefix, pathspec) < 0) - return NULL; - - /* diff prefix will only be leading non-wildcards */ - for (scan = prefix.ptr; *scan && !git__iswildcard(*scan); ++scan); - git_buf_truncate(&prefix, scan - prefix.ptr); - - if (prefix.size > 0) - return git_buf_detach(&prefix); - - git_buf_free(&prefix); - return NULL; -} - -static bool diff_pathspec_is_interesting(const git_strarray *pathspec) -{ - const char *str; - - if (pathspec == NULL || pathspec->count == 0) - return false; - if (pathspec->count > 1) - return true; - - str = pathspec->strings[0]; - if (!str || !str[0] || (!str[1] && (str[0] == '*' || str[0] == '.'))) - return false; - return true; -} - -static bool diff_path_matches_pathspec(git_diff_list *diff, const char *path) -{ - unsigned int i; - git_attr_fnmatch *match; - - if (!diff->pathspec.length) - return true; - - git_vector_foreach(&diff->pathspec, i, match) { - int result = p_fnmatch(match->pattern, path, 0); - - /* if we didn't match, look for exact dirname prefix match */ - if (result == FNM_NOMATCH && - (match->flags & GIT_ATTR_FNMATCH_HASWILD) == 0 && - strncmp(path, match->pattern, match->length) == 0 && - path[match->length] == '/') - result = 0; - - if (result == 0) - return (match->flags & GIT_ATTR_FNMATCH_NEGATIVE) ? false : true; - } - - return false; -} +#define DIFF_FLAG_IS_SET(DIFF,FLAG) (((DIFF)->opts.flags & (FLAG)) != 0) +#define DIFF_FLAG_ISNT_SET(DIFF,FLAG) (((DIFF)->opts.flags & (FLAG)) == 0) static git_diff_delta *diff_delta__alloc( git_diff_list *diff, @@ -87,7 +32,7 @@ static git_diff_delta *diff_delta__alloc( delta->new_file.path = delta->old_file.path; - if (diff->opts.flags & GIT_DIFF_REVERSE) { + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { switch (status) { case GIT_DELTA_ADDED: status = GIT_DELTA_DELETED; break; case GIT_DELTA_DELETED: status = GIT_DELTA_ADDED; break; @@ -99,70 +44,16 @@ static git_diff_delta *diff_delta__alloc( return delta; } -static git_diff_delta *diff_delta__dup( - const git_diff_delta *d, git_pool *pool) -{ - git_diff_delta *delta = git__malloc(sizeof(git_diff_delta)); - if (!delta) - return NULL; - - memcpy(delta, d, sizeof(git_diff_delta)); - - delta->old_file.path = git_pool_strdup(pool, d->old_file.path); - if (delta->old_file.path == NULL) - goto fail; - - if (d->new_file.path != d->old_file.path) { - delta->new_file.path = git_pool_strdup(pool, d->new_file.path); - if (delta->new_file.path == NULL) - goto fail; - } else { - delta->new_file.path = delta->old_file.path; - } - - return delta; - -fail: - git__free(delta); - return NULL; -} - -static git_diff_delta *diff_delta__merge_like_cgit( - const git_diff_delta *a, const git_diff_delta *b, git_pool *pool) +static int diff_notify( + const git_diff_list *diff, + const git_diff_delta *delta, + const char *matched_pathspec) { - git_diff_delta *dup = diff_delta__dup(a, pool); - if (!dup) - return NULL; - - if (git_oid_cmp(&dup->new_file.oid, &b->new_file.oid) == 0) - return dup; - - git_oid_cpy(&dup->new_file.oid, &b->new_file.oid); - - dup->new_file.mode = b->new_file.mode; - dup->new_file.size = b->new_file.size; - dup->new_file.flags = b->new_file.flags; - - /* Emulate C git for merging two diffs (a la 'git diff <sha>'). - * - * When C git does a diff between the work dir and a tree, it actually - * diffs with the index but uses the workdir contents. This emulates - * those choices so we can emulate the type of diff. - */ - if (git_oid_cmp(&dup->old_file.oid, &dup->new_file.oid) == 0) { - if (dup->status == GIT_DELTA_DELETED) - /* preserve pending delete info */; - else if (b->status == GIT_DELTA_UNTRACKED || - b->status == GIT_DELTA_IGNORED) - dup->status = b->status; - else - dup->status = GIT_DELTA_UNMODIFIED; - } - else if (dup->status == GIT_DELTA_UNMODIFIED || - b->status == GIT_DELTA_DELETED) - dup->status = b->status; + if (!diff->opts.notify_cb) + return 0; - return dup; + return diff->opts.notify_cb( + diff, delta, matched_pathspec, diff->opts.notify_payload); } static int diff_delta__from_one( @@ -171,16 +62,26 @@ static int diff_delta__from_one( const git_index_entry *entry) { git_diff_delta *delta; + const char *matched_pathspec; + int notify_res; if (status == GIT_DELTA_IGNORED && - (diff->opts.flags & GIT_DIFF_INCLUDE_IGNORED) == 0) + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_IGNORED)) return 0; if (status == GIT_DELTA_UNTRACKED && - (diff->opts.flags & GIT_DIFF_INCLUDE_UNTRACKED) == 0) + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNTRACKED)) + return 0; + + if (entry->mode == GIT_FILEMODE_COMMIT && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) return 0; - if (!diff_path_matches_pathspec(diff, entry->path)) + if (!git_pathspec_match_path( + &diff->pathspec, entry->path, + DIFF_FLAG_IS_SET(diff, GIT_DIFF_DISABLE_PATHSPEC_MATCH), + DIFF_FLAG_IS_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE), + &matched_pathspec)) return 0; delta = diff_delta__alloc(diff, status, entry->path); @@ -199,54 +100,122 @@ static int diff_delta__from_one( git_oid_cpy(&delta->new_file.oid, &entry->oid); } - delta->old_file.flags |= GIT_DIFF_FILE_VALID_OID; - delta->new_file.flags |= GIT_DIFF_FILE_VALID_OID; + delta->old_file.flags |= GIT_DIFF_FLAG_VALID_OID; + + if (delta->status == GIT_DELTA_DELETED || + !git_oid_iszero(&delta->new_file.oid)) + delta->new_file.flags |= GIT_DIFF_FLAG_VALID_OID; - if (git_vector_insert(&diff->deltas, delta) < 0) { + notify_res = diff_notify(diff, delta, matched_pathspec); + + if (notify_res) + git__free(delta); + else if (git_vector_insert(&diff->deltas, delta) < 0) { git__free(delta); return -1; } - return 0; + return notify_res < 0 ? GIT_EUSER : 0; } static int diff_delta__from_two( git_diff_list *diff, git_delta_t status, const git_index_entry *old_entry, + uint32_t old_mode, const git_index_entry *new_entry, - git_oid *new_oid) + uint32_t new_mode, + git_oid *new_oid, + const char *matched_pathspec) { git_diff_delta *delta; + int notify_res; if (status == GIT_DELTA_UNMODIFIED && - (diff->opts.flags & GIT_DIFF_INCLUDE_UNMODIFIED) == 0) + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNMODIFIED)) + return 0; + + if (old_entry->mode == GIT_FILEMODE_COMMIT && + new_entry->mode == GIT_FILEMODE_COMMIT && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) return 0; - if ((diff->opts.flags & GIT_DIFF_REVERSE) != 0) { - const git_index_entry *temp = old_entry; + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { + uint32_t temp_mode = old_mode; + const git_index_entry *temp_entry = old_entry; old_entry = new_entry; - new_entry = temp; + new_entry = temp_entry; + old_mode = new_mode; + new_mode = temp_mode; } delta = diff_delta__alloc(diff, status, old_entry->path); GITERR_CHECK_ALLOC(delta); - delta->old_file.mode = old_entry->mode; git_oid_cpy(&delta->old_file.oid, &old_entry->oid); - delta->old_file.flags |= GIT_DIFF_FILE_VALID_OID; + delta->old_file.size = old_entry->file_size; + delta->old_file.mode = old_mode; + delta->old_file.flags |= GIT_DIFF_FLAG_VALID_OID; + + git_oid_cpy(&delta->new_file.oid, &new_entry->oid); + delta->new_file.size = new_entry->file_size; + delta->new_file.mode = new_mode; + + if (new_oid) { + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) + git_oid_cpy(&delta->old_file.oid, new_oid); + else + git_oid_cpy(&delta->new_file.oid, new_oid); + } - delta->new_file.mode = new_entry->mode; - git_oid_cpy(&delta->new_file.oid, new_oid ? new_oid : &new_entry->oid); if (new_oid || !git_oid_iszero(&new_entry->oid)) - delta->new_file.flags |= GIT_DIFF_FILE_VALID_OID; + delta->new_file.flags |= GIT_DIFF_FLAG_VALID_OID; + + notify_res = diff_notify(diff, delta, matched_pathspec); - if (git_vector_insert(&diff->deltas, delta) < 0) { + if (notify_res) + git__free(delta); + else if (git_vector_insert(&diff->deltas, delta) < 0) { git__free(delta); return -1; } - return 0; + return notify_res < 0 ? GIT_EUSER : 0; +} + +static git_diff_delta *diff_delta__last_for_item( + git_diff_list *diff, + const git_index_entry *item) +{ + git_diff_delta *delta = git_vector_last(&diff->deltas); + if (!delta) + return NULL; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: + case GIT_DELTA_DELETED: + if (git_oid_cmp(&delta->old_file.oid, &item->oid) == 0) + return delta; + break; + case GIT_DELTA_ADDED: + if (git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) + return delta; + break; + case GIT_DELTA_UNTRACKED: + if (diff->strcomp(delta->new_file.path, item->path) == 0 && + git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) + return delta; + break; + case GIT_DELTA_MODIFIED: + if (git_oid_cmp(&delta->old_file.oid, &item->oid) == 0 || + git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) + return delta; + break; + default: + break; + } + + return NULL; } static char *diff_strdup_prefix(git_pool *pool, const char *prefix) @@ -260,13 +229,34 @@ static char *diff_strdup_prefix(git_pool *pool, const char *prefix) return git_pool_strndup(pool, prefix, len + 1); } -static int diff_delta__cmp(const void *a, const void *b) +int git_diff_delta__cmp(const void *a, const void *b) { const git_diff_delta *da = a, *db = b; int val = strcmp(da->old_file.path, db->old_file.path); return val ? val : ((int)da->status - (int)db->status); } +bool git_diff_delta__should_skip( + const git_diff_options *opts, const git_diff_delta *delta) +{ + uint32_t flags = opts ? opts->flags : 0; + + if (delta->status == GIT_DELTA_UNMODIFIED && + (flags & GIT_DIFF_INCLUDE_UNMODIFIED) == 0) + return true; + + if (delta->status == GIT_DELTA_IGNORED && + (flags & GIT_DIFF_INCLUDE_IGNORED) == 0) + return true; + + if (delta->status == GIT_DELTA_UNTRACKED && + (flags & GIT_DIFF_INCLUDE_UNTRACKED) == 0) + return true; + + return false; +} + + static int config_bool(git_config *cfg, const char *name, int defvalue) { int val = defvalue; @@ -281,14 +271,14 @@ static git_diff_list *git_diff_list_alloc( git_repository *repo, const git_diff_options *opts) { git_config *cfg; - size_t i; git_diff_list *diff = git__calloc(1, sizeof(git_diff_list)); if (diff == NULL) return NULL; + GIT_REFCOUNT_INC(diff); diff->repo = repo; - if (git_vector_init(&diff->deltas, 0, diff_delta__cmp) < 0 || + if (git_vector_init(&diff->deltas, 0, git_diff_delta__cmp) < 0 || git_pool_init(&diff->pool, 1, 0) < 0) goto fail; @@ -300,16 +290,36 @@ static git_diff_list *git_diff_list_alloc( if (config_bool(cfg, "core.ignorestat", 0)) diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_ASSUME_UNCHANGED; if (config_bool(cfg, "core.filemode", 1)) - diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_TRUST_EXEC_BIT; + diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_TRUST_MODE_BITS; if (config_bool(cfg, "core.trustctime", 1)) diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_TRUST_CTIME; /* Don't set GIT_DIFFCAPS_USE_DEV - compile time option in core git */ - if (opts == NULL) + /* TODO: there are certain config settings where even if we were + * not given an options structure, we need the diff list to have one + * so that we can store the altered default values. + * + * - diff.ignoreSubmodules + * - diff.mnemonicprefix + * - diff.noprefix + */ + + if (opts == NULL) { + /* Make sure we default to 3 lines */ + diff->opts.context_lines = 3; return diff; + } memcpy(&diff->opts, opts, sizeof(git_diff_options)); - memset(&diff->opts.pathspec, 0, sizeof(diff->opts.pathspec)); + + if(opts->flags & GIT_DIFF_IGNORE_FILEMODE) + diff->diffcaps = diff->diffcaps & ~GIT_DIFFCAPS_TRUST_MODE_BITS; + + /* pathspec init will do nothing for empty pathspec */ + if (git_pathspec_init(&diff->pathspec, &opts->pathspec, &diff->pool) < 0) + goto fail; + + /* TODO: handle config diff.mnemonicprefix, diff.noprefix */ diff->opts.old_prefix = diff_strdup_prefix(&diff->pool, opts->old_prefix ? opts->old_prefix : DIFF_OLD_PREFIX_DEFAULT); @@ -319,39 +329,15 @@ static git_diff_list *git_diff_list_alloc( if (!diff->opts.old_prefix || !diff->opts.new_prefix) goto fail; - if (diff->opts.flags & GIT_DIFF_REVERSE) { - char *swap = diff->opts.old_prefix; + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { + const char *swap = diff->opts.old_prefix; diff->opts.old_prefix = diff->opts.new_prefix; diff->opts.new_prefix = swap; } - /* only copy pathspec if it is "interesting" so we can test - * diff->pathspec.length > 0 to know if it is worth calling - * fnmatch as we iterate. - */ - if (!diff_pathspec_is_interesting(&opts->pathspec)) - return diff; - - if (git_vector_init( - &diff->pathspec, (unsigned int)opts->pathspec.count, NULL) < 0) - goto fail; - - for (i = 0; i < opts->pathspec.count; ++i) { - int ret; - const char *pattern = opts->pathspec.strings[i]; - git_attr_fnmatch *match = git__calloc(1, sizeof(git_attr_fnmatch)); - if (!match) - goto fail; - ret = git_attr_fnmatch__parse(match, &diff->pool, NULL, &pattern); - if (ret == GIT_ENOTFOUND) { - git__free(match); - continue; - } else if (ret < 0) - goto fail; - - if (git_vector_insert(&diff->pathspec, match) < 0) - goto fail; - } + /* INCLUDE_TYPECHANGE_TREES implies INCLUDE_TYPECHANGE */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES)) + diff->opts.flags |= GIT_DIFF_INCLUDE_TYPECHANGE; return diff; @@ -360,65 +346,106 @@ fail: return NULL; } -void git_diff_list_free(git_diff_list *diff) +static void diff_list_free(git_diff_list *diff) { git_diff_delta *delta; - git_attr_fnmatch *match; unsigned int i; - if (!diff) - return; - git_vector_foreach(&diff->deltas, i, delta) { git__free(delta); diff->deltas.contents[i] = NULL; } git_vector_free(&diff->deltas); - git_vector_foreach(&diff->pathspec, i, match) { - git__free(match); - diff->pathspec.contents[i] = NULL; - } - git_vector_free(&diff->pathspec); - + git_pathspec_free(&diff->pathspec); git_pool_clear(&diff->pool); git__free(diff); } -static int oid_for_workdir_item( +void git_diff_list_free(git_diff_list *diff) +{ + if (!diff) + return; + + GIT_REFCOUNT_DEC(diff, diff_list_free); +} + +void git_diff_list_addref(git_diff_list *diff) +{ + GIT_REFCOUNT_INC(diff); +} + +int git_diff__oid_for_file( git_repository *repo, - const git_index_entry *item, + const char *path, + uint16_t mode, + git_off_t size, git_oid *oid) { - int result; + int result = 0; git_buf full_path = GIT_BUF_INIT; - if (git_buf_joinpath(&full_path, git_repository_workdir(repo), item->path) < 0) + if (git_buf_joinpath( + &full_path, git_repository_workdir(repo), path) < 0) return -1; - /* calculate OID for file if possible*/ - if (S_ISLNK(item->mode)) + if (!mode) { + struct stat st; + + if (p_stat(path, &st) < 0) { + giterr_set(GITERR_OS, "Could not stat '%s'", path); + result = -1; + goto cleanup; + } + + mode = st.st_mode; + size = st.st_size; + } + + /* calculate OID for file if possible */ + if (S_ISGITLINK(mode)) { + git_submodule *sm; + const git_oid *sm_oid; + + if (!git_submodule_lookup(&sm, repo, path) && + (sm_oid = git_submodule_wd_id(sm)) != NULL) + git_oid_cpy(oid, sm_oid); + else { + /* if submodule lookup failed probably just in an intermediate + * state where some init hasn't happened, so ignore the error + */ + giterr_clear(); + memset(oid, 0, sizeof(*oid)); + } + } else if (S_ISLNK(mode)) { result = git_odb__hashlink(oid, full_path.ptr); - else if (!git__is_sizet(item->file_size)) { - giterr_set(GITERR_OS, "File size overflow for 32-bit systems"); + } else if (!git__is_sizet(size)) { + giterr_set(GITERR_OS, "File size overflow (for 32-bits) on '%s'", path); result = -1; } else { - int fd = git_futils_open_ro(full_path.ptr); - if (fd < 0) - result = fd; - else { - result = git_odb__hashfd( - oid, fd, (size_t)item->file_size, GIT_OBJ_BLOB); - p_close(fd); + git_vector filters = GIT_VECTOR_INIT; + + result = git_filters_load(&filters, repo, path, GIT_FILTER_TO_ODB); + if (result >= 0) { + int fd = git_futils_open_ro(full_path.ptr); + if (fd < 0) + result = fd; + else { + result = git_odb__hashfd_filtered( + oid, fd, (size_t)size, GIT_OBJ_BLOB, &filters); + p_close(fd); + } } + + git_filters_free(&filters); } +cleanup: git_buf_free(&full_path); - return result; } -#define EXEC_BIT_MASK 0000111 +#define MODE_BITS_MASK 0000777 static int maybe_modified( git_iterator *old_iter, @@ -431,24 +458,30 @@ static int maybe_modified( git_delta_t status = GIT_DELTA_MODIFIED; unsigned int omode = oitem->mode; unsigned int nmode = nitem->mode; + bool new_is_workdir = (new_iter->type == GIT_ITERATOR_TYPE_WORKDIR); + const char *matched_pathspec; GIT_UNUSED(old_iter); - if (!diff_path_matches_pathspec(diff, oitem->path)) + if (!git_pathspec_match_path( + &diff->pathspec, oitem->path, + DIFF_FLAG_IS_SET(diff, GIT_DIFF_DISABLE_PATHSPEC_MATCH), + DIFF_FLAG_IS_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE), + &matched_pathspec)) return 0; - /* on platforms with no symlinks, promote plain files to symlinks */ - if (S_ISLNK(omode) && S_ISREG(nmode) && + /* on platforms with no symlinks, preserve mode of existing symlinks */ + if (S_ISLNK(omode) && S_ISREG(nmode) && new_is_workdir && !(diff->diffcaps & GIT_DIFFCAPS_HAS_SYMLINKS)) - nmode = GIT_MODE_TYPE(omode) | (nmode & GIT_MODE_PERMS_MASK); + nmode = omode; - /* on platforms with no execmode, clear exec bit from comparisons */ - if (!(diff->diffcaps & GIT_DIFFCAPS_TRUST_EXEC_BIT)) { - omode = omode & ~EXEC_BIT_MASK; - nmode = nmode & ~EXEC_BIT_MASK; - } + /* on platforms with no execmode, just preserve old mode */ + if (!(diff->diffcaps & GIT_DIFFCAPS_TRUST_MODE_BITS) && + (nmode & MODE_BITS_MASK) != (omode & MODE_BITS_MASK) && + new_is_workdir) + nmode = (nmode & ~MODE_BITS_MASK) | (omode & MODE_BITS_MASK); - /* support "assume unchanged" (badly, b/c we still stat everything) */ + /* support "assume unchanged" (poorly, b/c we still stat everything) */ if ((diff->diffcaps & GIT_DIFFCAPS_ASSUME_UNCHANGED) != 0) status = (oitem->flags_extended & GIT_IDXENTRY_INTENT_TO_ADD) ? GIT_DELTA_MODIFIED : GIT_DELTA_UNMODIFIED; @@ -459,23 +492,29 @@ static int maybe_modified( /* if basic type of file changed, then split into delete and add */ else if (GIT_MODE_TYPE(omode) != GIT_MODE_TYPE(nmode)) { - if (diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem) < 0 || - diff_delta__from_one(diff, GIT_DELTA_ADDED, nitem) < 0) - return -1; - return 0; + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE)) + status = GIT_DELTA_TYPECHANGE; + else { + if (diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem) < 0 || + diff_delta__from_one(diff, GIT_DELTA_ADDED, nitem) < 0) + return -1; + return 0; + } } /* if oids and modes match, then file is unmodified */ - else if (git_oid_cmp(&oitem->oid, &nitem->oid) == 0 && - omode == nmode) + else if (git_oid_equal(&oitem->oid, &nitem->oid) && omode == nmode) status = GIT_DELTA_UNMODIFIED; - /* if we have a workdir item with an unknown oid, check deeper */ - else if (git_oid_iszero(&nitem->oid) && new_iter->type == GIT_ITERATOR_WORKDIR) { + /* if we have an unknown OID and a workdir iterator, then check some + * circumstances that can accelerate things or need special handling + */ + else if (git_oid_iszero(&nitem->oid) && new_is_workdir) { /* TODO: add check against index file st_mtime to avoid racy-git */ - /* if they files look exactly alike, then we'll assume the same */ - if (oitem->file_size == nitem->file_size && + /* if the stat data looks exactly alike, then assume the same */ + if (omode == nmode && + oitem->file_size == nitem->file_size && (!(diff->diffcaps & GIT_DIFFCAPS_TRUST_CTIME) || (oitem->ctime.seconds == nitem->ctime.seconds)) && oitem->mtime.seconds == nitem->mtime.seconds && @@ -487,77 +526,193 @@ static int maybe_modified( status = GIT_DELTA_UNMODIFIED; else if (S_ISGITLINK(nmode)) { + int err; git_submodule *sub; - if ((diff->opts.flags & GIT_DIFF_IGNORE_SUBMODULES) != 0) + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) status = GIT_DELTA_UNMODIFIED; - else if (git_submodule_lookup(&sub, diff->repo, nitem->path) < 0) - return -1; - else if (sub->ignore == GIT_SUBMODULE_IGNORE_ALL) + else if ((err = git_submodule_lookup(&sub, diff->repo, nitem->path)) < 0) { + if (err == GIT_EEXISTS) + status = GIT_DELTA_UNMODIFIED; + else + return err; + } else if (git_submodule_ignore(sub) == GIT_SUBMODULE_IGNORE_ALL) status = GIT_DELTA_UNMODIFIED; else { - /* TODO: support other GIT_SUBMODULE_IGNORE values */ - status = GIT_DELTA_UNMODIFIED; + unsigned int sm_status = 0; + if (git_submodule_status(&sm_status, sub) < 0) + return -1; + + /* check IS_WD_UNMODIFIED because this case is only used + * when the new side of the diff is the working directory + */ + status = GIT_SUBMODULE_STATUS_IS_WD_UNMODIFIED(sm_status) + ? GIT_DELTA_UNMODIFIED : GIT_DELTA_MODIFIED; + + /* grab OID while we are here */ + if (git_oid_iszero(&nitem->oid)) { + const git_oid *sm_oid = git_submodule_wd_id(sub); + if (sm_oid != NULL) { + git_oid_cpy(&noid, sm_oid); + use_noid = &noid; + } + } } } + } - /* TODO: check git attributes so we will not have to read the file - * in if it is marked binary. - */ + /* if mode is GITLINK and submodules are ignored, then skip */ + else if (S_ISGITLINK(nmode) && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) + status = GIT_DELTA_UNMODIFIED; - else if (oid_for_workdir_item(diff->repo, nitem, &noid) < 0) - return -1; + /* if we got here and decided that the files are modified, but we + * haven't calculated the OID of the new item, then calculate it now + */ + if (status != GIT_DELTA_UNMODIFIED && git_oid_iszero(&nitem->oid)) { + if (!use_noid) { + if (git_diff__oid_for_file(diff->repo, + nitem->path, nitem->mode, nitem->file_size, &noid) < 0) + return -1; + use_noid = &noid; + } - else if (git_oid_cmp(&oitem->oid, &noid) == 0 && - omode == nmode) + /* if oid matches, then mark unmodified (except submodules, where + * the filesystem content may be modified even if the oid still + * matches between the index and the workdir HEAD) + */ + if (omode == nmode && !S_ISGITLINK(omode) && + git_oid_equal(&oitem->oid, use_noid)) status = GIT_DELTA_UNMODIFIED; + } + + return diff_delta__from_two( + diff, status, oitem, omode, nitem, nmode, use_noid, matched_pathspec); +} - /* store calculated oid so we don't have to recalc later */ - use_noid = &noid; +static bool entry_is_prefixed( + git_diff_list *diff, + const git_index_entry *item, + const git_index_entry *prefix_item) +{ + size_t pathlen; + + if (!item || diff->pfxcomp(item->path, prefix_item->path) != 0) + return false; + + pathlen = strlen(prefix_item->path); + + return (prefix_item->path[pathlen - 1] == '/' || + item->path[pathlen] == '\0' || + item->path[pathlen] == '/'); +} + +static int diff_list_init_from_iterators( + git_diff_list *diff, + git_iterator *old_iter, + git_iterator *new_iter) +{ + diff->old_src = old_iter->type; + diff->new_src = new_iter->type; + + /* Use case-insensitive compare if either iterator has + * the ignore_case bit set */ + if (!git_iterator_ignore_case(old_iter) && + !git_iterator_ignore_case(new_iter)) + { + diff->opts.flags &= ~GIT_DIFF_DELTAS_ARE_ICASE; + + diff->strcomp = git__strcmp; + diff->strncomp = git__strncmp; + diff->pfxcomp = git__prefixcmp; + diff->entrycomp = git_index_entry__cmp; + } else { + diff->opts.flags |= GIT_DIFF_DELTAS_ARE_ICASE; + + diff->strcomp = git__strcasecmp; + diff->strncomp = git__strncasecmp; + diff->pfxcomp = git__prefixcmp_icase; + diff->entrycomp = git_index_entry__cmp_icase; } - return diff_delta__from_two(diff, status, oitem, nitem, use_noid); + return 0; } -static int diff_from_iterators( +int git_diff__from_iterators( + git_diff_list **diff_ptr, git_repository *repo, - const git_diff_options *opts, /**< can be NULL for defaults */ git_iterator *old_iter, git_iterator *new_iter, - git_diff_list **diff_ptr) + const git_diff_options *opts) { + int error = 0; const git_index_entry *oitem, *nitem; git_buf ignore_prefix = GIT_BUF_INIT; git_diff_list *diff = git_diff_list_alloc(repo, opts); - if (!diff) + + *diff_ptr = NULL; + + if (!diff || diff_list_init_from_iterators(diff, old_iter, new_iter) < 0) goto fail; - diff->old_src = old_iter->type; - diff->new_src = new_iter->type; + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE)) { + if (git_iterator_set_ignore_case(old_iter, true) < 0 || + git_iterator_set_ignore_case(new_iter, true) < 0) + goto fail; + } - if (git_iterator_current(old_iter, &oitem) < 0 || - git_iterator_current(new_iter, &nitem) < 0) + if (git_iterator_current(&oitem, old_iter) < 0 || + git_iterator_current(&nitem, new_iter) < 0) goto fail; /* run iterators building diffs */ while (oitem || nitem) { + int cmp = oitem ? (nitem ? diff->entrycomp(oitem, nitem) : -1) : 1; /* create DELETED records for old items not matched in new */ - if (oitem && (!nitem || strcmp(oitem->path, nitem->path) < 0)) { - if (diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem) < 0 || - git_iterator_advance(old_iter, &oitem) < 0) + if (cmp < 0) { + if (diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem) < 0) + goto fail; + + /* 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, 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 + * 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_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) goto fail; } /* create ADDED, TRACKED, or IGNORED records for new items not * matched in old (and/or descend into directories as needed) */ - else if (nitem && (!oitem || strcmp(oitem->path, nitem->path) > 0)) { + 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) && - git__prefixcmp(nitem->path, git_buf_cstr(&ignore_prefix)) == 0) + diff->pfxcomp(nitem->path, git_buf_cstr(&ignore_prefix)) == 0) delta_type = GIT_DELTA_IGNORED; if (S_ISDIR(nitem->mode)) { @@ -565,20 +720,48 @@ static int diff_from_iterators( * it or if the user requested the contents of untracked * directories and it is not under an ignored directory. */ - if ((oitem && git__prefixcmp(oitem->path, nitem->path) == 0) || + bool recurse_into_dir = (delta_type == GIT_DELTA_UNTRACKED && - (diff->opts.flags & GIT_DIFF_RECURSE_UNTRACKED_DIRS) != 0)) - { - /* if this directory is ignored, remember it as the - * "ignore_prefix" for processing contained items - */ - if (delta_type == GIT_DELTA_UNTRACKED && - git_iterator_current_is_ignored(new_iter)) - git_buf_sets(&ignore_prefix, nitem->path); - - if (git_iterator_advance_into_directory(new_iter, &nitem) < 0) + 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 */ + if ((contains_oitem || recurse_into_dir) && + delta_type == GIT_DELTA_UNTRACKED && + git_iterator_current_is_ignored(new_iter)) + { + git_buf_sets(&ignore_prefix, nitem->path); + delta_type = GIT_DELTA_IGNORED; + + /* skip recursion if we've just learned this is ignored */ + if (DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)) + recurse_into_dir = false; + } + + if (contains_oitem || recurse_into_dir) { + /* advance into directory */ + error = git_iterator_advance_into(&nitem, new_iter); + + /* if directory is empty, can't advance into it, so skip */ + if (error == GIT_ENOTFOUND) { + giterr_clear(); + error = git_iterator_advance(&nitem, new_iter); + + git_buf_clear(&ignore_prefix); + } + if (error < 0) + goto fail; continue; } } @@ -598,8 +781,9 @@ static int diff_from_iterators( * checked before container directory exclusions are used to * skip the file. */ - else if (delta_type == GIT_DELTA_IGNORED) { - if (git_iterator_advance(new_iter, &nitem) < 0) + else if (delta_type == GIT_DELTA_IGNORED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)) { + if (git_iterator_advance(&nitem, new_iter) < 0) goto fail; continue; /* ignored parent directory, so skip completely */ } @@ -607,11 +791,28 @@ static int diff_from_iterators( else if (git_iterator_current_is_ignored(new_iter)) delta_type = GIT_DELTA_IGNORED; - else if (new_iter->type != GIT_ITERATOR_WORKDIR) + else if (new_iter->type != GIT_ITERATOR_TYPE_WORKDIR) delta_type = GIT_DELTA_ADDED; - if (diff_delta__from_one(diff, delta_type, nitem) < 0 || - git_iterator_advance(new_iter, &nitem) < 0) + if (diff_delta__from_one(diff, delta_type, nitem) < 0) + goto fail; + + /* if we are generating TYPECHANGE records then check for that + * instead of 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; + } + } + + if (git_iterator_advance(&nitem, new_iter) < 0) goto fail; } @@ -619,165 +820,116 @@ static int diff_from_iterators( * (or ADDED and DELETED pair if type changed) */ else { - assert(oitem && nitem && strcmp(oitem->path, nitem->path) == 0); + assert(oitem && nitem && cmp == 0); if (maybe_modified(old_iter, oitem, new_iter, nitem, diff) < 0 || - git_iterator_advance(old_iter, &oitem) < 0 || - git_iterator_advance(new_iter, &nitem) < 0) + git_iterator_advance(&oitem, old_iter) < 0 || + git_iterator_advance(&nitem, new_iter) < 0) goto fail; } } - git_iterator_free(old_iter); - git_iterator_free(new_iter); - git_buf_free(&ignore_prefix); - *diff_ptr = diff; - return 0; fail: - git_iterator_free(old_iter); - git_iterator_free(new_iter); + if (!*diff_ptr) { + git_diff_list_free(diff); + error = -1; + } + git_buf_free(&ignore_prefix); - git_diff_list_free(diff); - *diff_ptr = NULL; - return -1; + return error; } +#define DIFF_FROM_ITERATORS(MAKE_FIRST, MAKE_SECOND) do { \ + git_iterator *a = NULL, *b = NULL; \ + char *pfx = opts ? git_pathspec_prefix(&opts->pathspec) : NULL; \ + GITERR_CHECK_VERSION(opts, GIT_DIFF_OPTIONS_VERSION, "git_diff_options"); \ + if (!(error = MAKE_FIRST) && !(error = MAKE_SECOND)) \ + error = git_diff__from_iterators(diff, repo, a, b, opts); \ + git__free(pfx); git_iterator_free(a); git_iterator_free(b); \ +} while (0) int git_diff_tree_to_tree( + git_diff_list **diff, git_repository *repo, - const git_diff_options *opts, /**< can be NULL for defaults */ git_tree *old_tree, git_tree *new_tree, - git_diff_list **diff) + const git_diff_options *opts) { - git_iterator *a = NULL, *b = NULL; - char *prefix = opts ? diff_prefix_from_pathspec(&opts->pathspec) : NULL; - - assert(repo && old_tree && new_tree && diff); + int error = 0; - if (git_iterator_for_tree_range(&a, repo, old_tree, prefix, prefix) < 0 || - git_iterator_for_tree_range(&b, repo, new_tree, prefix, prefix) < 0) - return -1; + assert(diff && repo); - git__free(prefix); + DIFF_FROM_ITERATORS( + git_iterator_for_tree(&a, old_tree, 0, pfx, pfx), + git_iterator_for_tree(&b, new_tree, 0, pfx, pfx) + ); - return diff_from_iterators(repo, opts, a, b, diff); + return error; } -int git_diff_index_to_tree( +int git_diff_tree_to_index( + git_diff_list **diff, git_repository *repo, - const git_diff_options *opts, git_tree *old_tree, - git_diff_list **diff) + git_index *index, + const git_diff_options *opts) { - git_iterator *a = NULL, *b = NULL; - char *prefix = opts ? diff_prefix_from_pathspec(&opts->pathspec) : NULL; + int error = 0; - assert(repo && diff); + assert(diff && repo); - if (git_iterator_for_tree_range(&a, repo, old_tree, prefix, prefix) < 0 || - git_iterator_for_index_range(&b, repo, prefix, prefix) < 0) - return -1; + if (!index && (error = git_repository_index__weakptr(&index, repo)) < 0) + return error; - git__free(prefix); + DIFF_FROM_ITERATORS( + git_iterator_for_tree(&a, old_tree, 0, pfx, pfx), + git_iterator_for_index(&b, index, 0, pfx, pfx) + ); - return diff_from_iterators(repo, opts, a, b, diff); + return error; } -int git_diff_workdir_to_index( +int git_diff_index_to_workdir( + git_diff_list **diff, git_repository *repo, - const git_diff_options *opts, - git_diff_list **diff) + git_index *index, + const git_diff_options *opts) { - git_iterator *a = NULL, *b = NULL; - char *prefix = opts ? diff_prefix_from_pathspec(&opts->pathspec) : NULL; + int error = 0; - assert(repo && diff); + assert(diff && repo); - if (git_iterator_for_index_range(&a, repo, prefix, prefix) < 0 || - git_iterator_for_workdir_range(&b, repo, prefix, prefix) < 0) - return -1; + if (!index && (error = git_repository_index__weakptr(&index, repo)) < 0) + return error; - git__free(prefix); + DIFF_FROM_ITERATORS( + git_iterator_for_index(&a, index, 0, pfx, pfx), + git_iterator_for_workdir( + &b, repo, GIT_ITERATOR_DONT_AUTOEXPAND, pfx, pfx) + ); - return diff_from_iterators(repo, opts, a, b, diff); + return error; } -int git_diff_workdir_to_tree( +int git_diff_tree_to_workdir( + git_diff_list **diff, git_repository *repo, - const git_diff_options *opts, git_tree *old_tree, - git_diff_list **diff) -{ - git_iterator *a = NULL, *b = NULL; - char *prefix = opts ? diff_prefix_from_pathspec(&opts->pathspec) : NULL; - - assert(repo && old_tree && diff); - - if (git_iterator_for_tree_range(&a, repo, old_tree, prefix, prefix) < 0 || - git_iterator_for_workdir_range(&b, repo, prefix, prefix) < 0) - return -1; - - git__free(prefix); - - return diff_from_iterators(repo, opts, a, b, diff); -} - -int git_diff_merge( - git_diff_list *onto, - const git_diff_list *from) + const git_diff_options *opts) { int error = 0; - git_pool onto_pool; - git_vector onto_new; - git_diff_delta *delta; - unsigned int i, j; - - assert(onto && from); - - if (!from->deltas.length) - return 0; - - if (git_vector_init(&onto_new, onto->deltas.length, diff_delta__cmp) < 0 || - git_pool_init(&onto_pool, 1, 0) < 0) - return -1; - - for (i = 0, j = 0; i < onto->deltas.length || j < from->deltas.length; ) { - git_diff_delta *o = GIT_VECTOR_GET(&onto->deltas, i); - const git_diff_delta *f = GIT_VECTOR_GET(&from->deltas, j); - int cmp = !f ? -1 : !o ? 1 : strcmp(o->old_file.path, f->old_file.path); - if (cmp < 0) { - delta = diff_delta__dup(o, &onto_pool); - i++; - } else if (cmp > 0) { - delta = diff_delta__dup(f, &onto_pool); - j++; - } else { - delta = diff_delta__merge_like_cgit(o, f, &onto_pool); - i++; - j++; - } - - if ((error = !delta ? -1 : git_vector_insert(&onto_new, delta)) < 0) - break; - } + assert(diff && repo); - if (!error) { - git_vector_swap(&onto->deltas, &onto_new); - git_pool_swap(&onto->pool, &onto_pool); - onto->new_src = from->new_src; - } - - git_vector_foreach(&onto_new, i, delta) - git__free(delta); - git_vector_free(&onto_new); - git_pool_clear(&onto_pool); + DIFF_FROM_ITERATORS( + git_iterator_for_tree(&a, old_tree, 0, pfx, pfx), + git_iterator_for_workdir( + &b, repo, GIT_ITERATOR_DONT_AUTOEXPAND, pfx, pfx) + ); return error; } - |