From d39fff36484e908438beb17ee043689962182460 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Sun, 23 Jun 2013 20:33:57 -0700 Subject: [PATCH 01/17] Basic framework for log command --- examples/Makefile | 2 +- examples/log.c | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 examples/log.c diff --git a/examples/Makefile b/examples/Makefile index 140cc4da9..6288906df 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -3,7 +3,7 @@ CC = gcc CFLAGS = -g -I../include -I../src -Wall -Wextra -Wmissing-prototypes -Wno-missing-field-initializers LFLAGS = -L../build -lgit2 -lz -APPS = general showindex diff rev-list cat-file status +APPS = general showindex diff rev-list cat-file status log all: $(APPS) diff --git a/examples/log.c b/examples/log.c new file mode 100644 index 000000000..f92b66996 --- /dev/null +++ b/examples/log.c @@ -0,0 +1,58 @@ +#include +#include +#include +#include + +static void check(int error, const char *message) +{ + if (error) { + fprintf(stderr, "%s (%d)\n", message, error); + exit(1); + } +} + +static int check_str_param(const char *arg, const char *pat, const char **val) +{ + size_t len = strlen(pat); + if (strncmp(arg, pat, len)) + return 0; + *val = (const char *)(arg + len); + return 1; +} + +static void usage(const char *message, const char *arg) +{ + if (message && arg) + fprintf(stderr, "%s: %s\n", message, arg); + else if (message) + fprintf(stderr, "%s\n", message); + fprintf(stderr, "usage: log []\n"); + exit(1); +} + +int main(int argc, char *argv[]) +{ + int i; + char *a; + const char *dir = "."; + git_repository *repo; + + git_threads_init(); + + for (i = 1; i < argc; ++i) { + a = argv[i]; + + if (a[0] != '-') { + } + else if (!check_str_param(a, "--git-dir=", &dir)) + usage("Unknown argument", a); + } + + check(git_repository_open_ext(&repo, dir, 0, NULL), + "Could not open repository"); + + git_repository_free(repo); + git_threads_shutdown(); + + return 0; +} From d2ce27dd494b65f54b2d110b4defd69aea976115 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Mon, 24 Jun 2013 23:16:06 -0700 Subject: [PATCH 02/17] Add public API for pathspec matching This adds a new public API for compiling pathspecs and matching them against the working directory, the index, or a tree from the repository. This also reworks the pathspec internals to allow the sharing of code between the existing internal usage of pathspec matching and the new external API. While this is working and the new API is ready for discussion, I think there is still an incorrect behavior in which patterns are always matched against the full path of an entry without taking the subdirectories into account (so "s*" will match "subdir/file" even though it wouldn't with core Git). Further enhancements are coming, but this was a good place to take a functional snapshot. --- include/git2/index.h | 8 + include/git2/pathspec.h | 202 +++++++++++++++++ src/checkout.c | 10 +- src/diff.c | 12 +- src/index.c | 42 ++-- src/index.h | 6 +- src/pathspec.c | 452 +++++++++++++++++++++++++++++++++---- src/pathspec.h | 43 ++-- tests-clar/repo/pathspec.c | 385 +++++++++++++++++++++++++++++++ 9 files changed, 1063 insertions(+), 97 deletions(-) create mode 100644 include/git2/pathspec.h create mode 100644 tests-clar/repo/pathspec.c diff --git a/include/git2/index.h b/include/git2/index.h index 51694aded..1fb77efa3 100644 --- a/include/git2/index.h +++ b/include/git2/index.h @@ -138,6 +138,14 @@ typedef enum { GIT_INDEX_ADD_CHECK_PATHSPEC = (1u << 2), } git_index_add_option_t; +/** + * Match any index stage. + * + * Some index APIs take a stage to match; pass this value to match + * any entry matching the path regardless of stage. + */ +#define GIT_INDEX_STAGE_ANY -1 + /** @name Index File Functions * * These functions work on the index file itself. diff --git a/include/git2/pathspec.h b/include/git2/pathspec.h new file mode 100644 index 000000000..8122d9927 --- /dev/null +++ b/include/git2/pathspec.h @@ -0,0 +1,202 @@ +/* + * 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. + */ +#ifndef INCLUDE_git_pathspec_h__ +#define INCLUDE_git_pathspec_h__ + +#include "common.h" +#include "types.h" +#include "strarray.h" + +/** + * Compiled pathspec + */ +typedef struct git_pathspec git_pathspec; + +/** + * List of filenames matching a pathspec + */ +typedef struct git_pathspec_match_list git_pathspec_match_list; + +/** + * Options controlling how pathspec match should be executed + * + * - GIT_PATHSPEC_IGNORE_CASE forces match to ignore case; otherwise + * match will use native case sensitivity of platform + * - GIT_PATHSPEC_USE_CASE forces case sensitive match; otherwise + * match will use native case sensitivity of platform + * - GIT_PATHSPEC_NO_GLOB disables glob patterns and just uses simple + * string comparison for matching + * - GIT_PATHSPEC_NO_MATCH_ERROR means the match function will return + * GIT_ENOTFOUND if no matches are found; otherwise it will return 0 + * for success and `git_pathspec_match_list_entrycount` will be 0. + * - GIT_PATHSPEC_FIND_FAILURES only applies to a git_pathspec_match_list; + * it means to check file names against all unmatched patterns so that + * at the end of a match we can identify patterns that did not match any + * files. + * - GIT_PATHSPEC_FAILURES_ONLY only applies to a git_pathspec_match_list; + * it means to only check for mismatches and not record matched paths. + */ +typedef enum { + GIT_PATHSPEC_DEFAULT = 0, + GIT_PATHSPEC_IGNORE_CASE = (1u << 0), + GIT_PATHSPEC_USE_CASE = (1u << 1), + GIT_PATHSPEC_NO_GLOB = (1u << 2), + GIT_PATHSPEC_NO_MATCH_ERROR = (1u << 3), + GIT_PATHSPEC_FIND_FAILURES = (1u << 4), + GIT_PATHSPEC_FAILURES_ONLY = (1u << 5), +} git_pathspec_flag_t; + +/** + * Compile a pathspec + * + * @param out Output of the compiled pathspec + * @param flags Combination of git_pathspec_flag_t values + * @param pathspec A git_strarray of the paths to match + * @return 0 on success, <0 on failure + */ +GIT_EXTERN(int) git_pathspec_new( + git_pathspec **out, const git_strarray *pathspec); + +/** + * Free a pathspec + * + * @param ps The compiled pathspec + */ +GIT_EXTERN(void) git_pathspec_free(git_pathspec *ps); + +/** + * Try to match a path against a pathspec + * + * Unlike most of the other pathspec matching functions, this will not + * fall back on the native case-sensitivity for your platform. You must + * explicitly pass flags to control case sensitivity or else this will + * fall back on being case sensitive. + * + * @param ps The compiled pathspec + * @param flags Match flags to influence matching behavior + * @param path The pathname to attempt to match + * @return 1 is path matches spec, 0 if it does not + */ +GIT_EXTERN(int) git_pathspec_matches_path( + const git_pathspec *ps, uint32_t flags, const char *path); + +/** + * Match a pathspec against the working directory of a repository. + * + * This returns a `git_patchspec_match` object that contains the list of + * all files matching the given pathspec in the working directory of the + * repository. This handles git ignores (i.e. ignored files will not be + * considered to match the `pathspec` unless the file is tracked in the + * index). + * + * @param out Object with list of matching items + * @param repo The repository in which to match; bare repo is an error + * @param flags Options to control matching behavior + * @param ps Pathspec to be matched + * @return 0 on success, -1 on error, GIT_ENOTFOUND if no matches and + * the GIT_PATHSPEC_NO_MATCH_ERROR flag is used + */ +GIT_EXTERN(int) git_pathspec_match_workdir( + git_pathspec_match_list **out, + git_repository *repo, + uint32_t flags, + git_pathspec *ps); + +/** + * Match a pathspec against entries in an index. + * + * This returns a `git_patchspec_match` object that contains the list of + * all files matching the given pathspec in the index. + * + * NOTE: At the moment, the case sensitivity of this match is controlled + * by the current case-sensitivity of the index object itself and the + * USE_CASE and IGNORE_CASE flags will have no effect. This behavior will + * be corrected in a future release. + * + * @param out Object with list of matching items + * @param inex The index in which to match + * @param flags Options to control matching behavior + * @param ps Pathspec to be matched + * @return 0 on success, -1 on error, GIT_ENOTFOUND if no matches and + * the GIT_PATHSPEC_NO_MATCH_ERROR flag is used + */ +GIT_EXTERN(int) git_pathspec_match_index( + git_pathspec_match_list **out, + git_index *index, + uint32_t flags, + git_pathspec *ps); + +/** + * Match a pathspec against files in a tree. + * + * This returns a `git_patchspec_match` object that contains the list of + * all files matching the given pathspec in the given tree. + * + * @param out Object with list of matching items + * @param inex The index in which to match + * @param flags Options to control matching behavior + * @param ps Pathspec to be matched + * @return 0 on success, -1 on error, GIT_ENOTFOUND if no matches and + * the GIT_PATHSPEC_NO_MATCH_ERROR flag is used + */ +GIT_EXTERN(int) git_pathspec_match_tree( + git_pathspec_match_list **out, + git_tree *tree, + uint32_t flags, + git_pathspec *ps); + +/** + * Free memory associates with a git_pathspec_match_list + * + * @param m The git_pathspec_match_list to be freed + */ +GIT_EXTERN(void) git_pathspec_match_list_free(git_pathspec_match_list *m); + +/** + * Get the number of items in a match list. + * + * @param m The git_pathspec_match_list object + * @return Number of items in match list + */ +GIT_EXTERN(size_t) git_pathspec_match_list_entrycount( + const git_pathspec_match_list *m); + +/** + * Get a matching filename by position. + * + * @param m The git_pathspec_match_list object + * @param pos The index into the list + * @return The filename of the match + */ +GIT_EXTERN(const char *) git_pathspec_match_list_entry( + const git_pathspec_match_list *m, size_t pos); + +/** + * Get the number of pathspec items that did not match. + * + * This will be zero unless you passed GIT_PATHSPEC_FIND_FAILURES when + * generating the git_pathspec_match_list. + * + * @param m The git_pathspec_match_list object + * @return Number of items in original pathspec that had no matches + */ +GIT_EXTERN(size_t) git_pathspec_match_list_failed_entrycount( + const git_pathspec_match_list *m); + +/** + * Get an original pathspec string that had no matches. + * + * This will be return NULL for positions out of range. + * + * @param m The git_pathspec_match_list object + * @param pos The index into the failed items + * @return The pathspec pattern that didn't match anything + */ +GIT_EXTERN(const char *) git_pathspec_match_list_failed_entry( + const git_pathspec_match_list *m, size_t pos); + +#endif diff --git a/src/checkout.c b/src/checkout.c index 8f9ec64e4..ec9da7e2e 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -246,10 +246,10 @@ static int checkout_action_wd_only( bool remove = false; git_checkout_notify_t notify = GIT_CHECKOUT_NOTIFY_NONE; - if (!git_pathspec_match_path( + if (!git_pathspec__match( pathspec, wd->path, (data->strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH) != 0, - git_iterator_ignore_case(workdir), NULL)) + git_iterator_ignore_case(workdir), NULL, NULL)) return 0; /* check if item is tracked in the index but not in the checkout diff */ @@ -607,7 +607,7 @@ static int checkout_get_actions( uint32_t *actions = NULL; if (data->opts.paths.count > 0 && - git_pathspec_init(&pathspec, &data->opts.paths, &pathpool) < 0) + git_pathspec__vinit(&pathspec, &data->opts.paths, &pathpool) < 0) return -1; if ((error = git_iterator_current(&wditem, workdir)) < 0 && @@ -659,7 +659,7 @@ static int checkout_get_actions( goto fail; } - git_pathspec_free(&pathspec); + git_pathspec__vfree(&pathspec); git_pool_clear(&pathpool); return 0; @@ -670,7 +670,7 @@ fail: *actions_ptr = NULL; git__free(actions); - git_pathspec_free(&pathspec); + git_pathspec__vfree(&pathspec); git_pool_clear(&pathpool); return error; diff --git a/src/diff.c b/src/diff.c index 0980f412a..56232ebf4 100644 --- a/src/diff.c +++ b/src/diff.c @@ -81,11 +81,11 @@ static int diff_delta__from_one( DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) return 0; - if (!git_pathspec_match_path( + if (!git_pathspec__match( &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)) + &matched_pathspec, NULL)) return 0; delta = diff_delta__alloc(diff, status, entry->path); @@ -387,7 +387,7 @@ static int diff_list_apply_options( DIFF_FLAG_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE, icase); /* initialize pathspec from options */ - if (git_pathspec_init(&diff->pathspec, &opts->pathspec, pool) < 0) + if (git_pathspec__vinit(&diff->pathspec, &opts->pathspec, pool) < 0) return -1; } @@ -473,7 +473,7 @@ static void diff_list_free(git_diff_list *diff) } git_vector_free(&diff->deltas); - git_pathspec_free(&diff->pathspec); + git_pathspec__vfree(&diff->pathspec); git_pool_clear(&diff->pool); git__memzero(diff, sizeof(*diff)); @@ -634,11 +634,11 @@ static int maybe_modified( bool new_is_workdir = (info->new_iter->type == GIT_ITERATOR_TYPE_WORKDIR); const char *matched_pathspec; - if (!git_pathspec_match_path( + if (!git_pathspec__match( &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)) + &matched_pathspec, NULL)) return 0; memset(&noid, 0, sizeof(noid)); diff --git a/src/index.c b/src/index.c index ffa84e53f..21efd2c63 100644 --- a/src/index.c +++ b/src/index.c @@ -101,8 +101,6 @@ static int parse_index(git_index *index, const char *buffer, size_t buffer_size) static bool is_index_extended(git_index *index); static int write_index(git_index *index, git_filebuf *file); -static int index_find(size_t *at_pos, git_index *index, const char *path, int stage); - static void index_entry_free(git_index_entry *entry); static void index_entry_reuc_free(git_index_reuc_entry *reuc); @@ -114,7 +112,7 @@ static int index_srch(const void *key, const void *array_member) ret = strcmp(srch_key->path, entry->path); - if (ret == 0) + if (ret == 0 && srch_key->stage != GIT_INDEX_STAGE_ANY) ret = srch_key->stage - GIT_IDXENTRY_STAGE(entry); return ret; @@ -128,7 +126,7 @@ static int index_isrch(const void *key, const void *array_member) ret = strcasecmp(srch_key->path, entry->path); - if (ret == 0) + if (ret == 0 && srch_key->stage != GIT_INDEX_STAGE_ANY) ret = srch_key->stage - GIT_IDXENTRY_STAGE(entry); return ret; @@ -562,7 +560,7 @@ const git_index_entry *git_index_get_bypath( git_vector_sort(&index->entries); - if (index_find(&pos, index, path, stage) < 0) { + if (git_index__find(&pos, index, path, stage) < 0) { giterr_set(GITERR_INDEX, "Index does not contain %s", path); return NULL; } @@ -719,7 +717,8 @@ static int index_insert(git_index *index, git_index_entry *entry, int replace) entry->flags |= GIT_IDXENTRY_NAMEMASK; /* look if an entry with this path already exists */ - if (!index_find(&position, index, entry->path, GIT_IDXENTRY_STAGE(entry))) { + if (!git_index__find( + &position, index, entry->path, GIT_IDXENTRY_STAGE(entry))) { existing = (git_index_entry **)&index->entries.contents[position]; /* update filemode to existing values if stat is not trusted */ @@ -831,7 +830,7 @@ int git_index_remove(git_index *index, const char *path, int stage) git_vector_sort(&index->entries); - if (index_find(&position, index, path, stage) < 0) { + if (git_index__find(&position, index, path, stage) < 0) { giterr_set(GITERR_INDEX, "Index does not contain %s at stage %d", path, stage); return GIT_ENOTFOUND; @@ -887,7 +886,8 @@ int git_index_remove_directory(git_index *index, const char *dir, int stage) return error; } -static int index_find(size_t *at_pos, git_index *index, const char *path, int stage) +int git_index__find( + size_t *at_pos, git_index *index, const char *path, int stage) { struct entry_srch_key srch_key; @@ -896,7 +896,8 @@ static int index_find(size_t *at_pos, git_index *index, const char *path, int st srch_key.path = path; srch_key.stage = stage; - return git_vector_bsearch2(at_pos, &index->entries, index->entries_search, &srch_key); + return git_vector_bsearch2( + at_pos, &index->entries, index->entries_search, &srch_key); } int git_index_find(size_t *at_pos, git_index *index, const char *path) @@ -2053,7 +2054,7 @@ int git_index_add_all( git_iterator *wditer = NULL; const git_index_entry *wd = NULL; git_index_entry *entry; - git_pathspec_context ps; + git_pathspec ps; const char *match; size_t existing; bool no_fnmatch = (flags & GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH) != 0; @@ -2074,7 +2075,7 @@ int git_index_add_all( if (git_repository__cvar(&ignorecase, repo, GIT_CVAR_IGNORECASE) < 0) return -1; - if ((error = git_pathspec_context_init(&ps, paths)) < 0) + if ((error = git_pathspec__init(&ps, paths)) < 0) return error; /* optionally check that pathspec doesn't mention any ignored files */ @@ -2091,14 +2092,14 @@ int git_index_add_all( while (!(error = git_iterator_advance(&wd, wditer))) { /* check if path actually matches */ - if (!git_pathspec_match_path( - &ps.pathspec, wd->path, no_fnmatch, ignorecase, &match)) + if (!git_pathspec__match( + &ps.pathspec, wd->path, no_fnmatch, ignorecase, &match, NULL)) continue; /* skip ignored items that are not already in the index */ if ((flags & GIT_INDEX_ADD_FORCE) == 0 && git_iterator_current_is_ignored(wditer) && - index_find(&existing, index, wd->path, 0) < 0) + git_index__find(&existing, index, wd->path, 0) < 0) continue; /* issue notification callback if requested */ @@ -2148,7 +2149,7 @@ int git_index_add_all( cleanup: git_iterator_free(wditer); - git_pathspec_context_free(&ps); + git_pathspec__clear(&ps); return error; } @@ -2168,13 +2169,13 @@ static int index_apply_to_all( { int error = 0; size_t i; - git_pathspec_context ps; + git_pathspec ps; const char *match; git_buf path = GIT_BUF_INIT; assert(index); - if ((error = git_pathspec_context_init(&ps, paths)) < 0) + if ((error = git_pathspec__init(&ps, paths)) < 0) return error; git_vector_sort(&index->entries); @@ -2183,8 +2184,9 @@ static int index_apply_to_all( git_index_entry *entry = git_vector_get(&index->entries, i); /* check if path actually matches */ - if (!git_pathspec_match_path( - &ps.pathspec, entry->path, false, index->ignore_case, &match)) + if (!git_pathspec__match( + &ps.pathspec, entry->path, false, index->ignore_case, + &match, NULL)) continue; /* issue notification callback if requested */ @@ -2231,7 +2233,7 @@ static int index_apply_to_all( } git_buf_free(&path); - git_pathspec_context_free(&ps); + git_pathspec__clear(&ps); return error; } diff --git a/src/index.h b/src/index.h index a59107a7b..40577e105 100644 --- a/src/index.h +++ b/src/index.h @@ -47,13 +47,17 @@ struct git_index_conflict_iterator { size_t cur; }; -extern void git_index_entry__init_from_stat(git_index_entry *entry, struct stat *st); +extern void git_index_entry__init_from_stat( + git_index_entry *entry, struct stat *st); extern size_t git_index__prefix_position(git_index *index, const char *path); extern int git_index_entry__cmp(const void *a, const void *b); extern int git_index_entry__cmp_icase(const void *a, const void *b); +extern int git_index__find( + size_t *at_pos, git_index *index, const char *path, int stage); + extern void git_index__set_ignore_case(git_index *index, bool ignore_case); #endif diff --git a/src/pathspec.c b/src/pathspec.c index f029836d0..35421dbef 100644 --- a/src/pathspec.c +++ b/src/pathspec.c @@ -5,9 +5,13 @@ * a Linking Exception. For full terms see the included COPYING file. */ +#include "git2/pathspec.h" #include "pathspec.h" #include "buf_text.h" #include "attr_file.h" +#include "iterator.h" +#include "repository.h" +#include "index.h" /* what is the common non-wildcard prefix for all items in the pathspec */ char *git_pathspec_prefix(const git_strarray *pathspec) @@ -56,7 +60,7 @@ bool git_pathspec_is_empty(const git_strarray *pathspec) } /* build a vector of fnmatch patterns to evaluate efficiently */ -int git_pathspec_init( +int git_pathspec__vinit( git_vector *vspec, const git_strarray *strspec, git_pool *strpool) { size_t i; @@ -93,7 +97,7 @@ int git_pathspec_init( } /* free data from the pathspec vector */ -void git_pathspec_free(git_vector *vspec) +void git_pathspec__vfree(git_vector *vspec) { git_attr_fnmatch *match; unsigned int i; @@ -106,60 +110,91 @@ void git_pathspec_free(git_vector *vspec) git_vector_free(vspec); } +struct pathspec_match_context { + int fnmatch_flags; + int (*strcomp)(const char *, const char *); + int (*strncomp)(const char *, const char *, size_t); +}; + +static void pathspec_match_context_init( + struct pathspec_match_context *ctxt, + bool disable_fnmatch, + bool casefold) +{ + if (disable_fnmatch) + ctxt->fnmatch_flags = -1; + else if (casefold) + ctxt->fnmatch_flags = FNM_CASEFOLD; + else + ctxt->fnmatch_flags = 0; + + if (casefold) { + ctxt->strcomp = git__strcasecmp; + ctxt->strncomp = git__strncasecmp; + } else { + ctxt->strcomp = git__strcmp; + ctxt->strncomp = git__strncmp; + } +} + +static int pathspec_match_one( + const git_attr_fnmatch *match, + struct pathspec_match_context *ctxt, + const char *path) +{ + int result = (match->flags & GIT_ATTR_FNMATCH_MATCH_ALL) ? 0 : FNM_NOMATCH; + + if (result == FNM_NOMATCH) + result = ctxt->strcomp(match->pattern, path) ? FNM_NOMATCH : 0; + + if (ctxt->fnmatch_flags >= 0 && result == FNM_NOMATCH) + result = p_fnmatch(match->pattern, path, ctxt->fnmatch_flags); + + /* if we didn't match, look for exact dirname prefix match */ + if (result == FNM_NOMATCH && + (match->flags & GIT_ATTR_FNMATCH_HASWILD) == 0 && + ctxt->strncomp(path, match->pattern, match->length) == 0 && + path[match->length] == '/') + result = 0; + + if (result == 0) + return (match->flags & GIT_ATTR_FNMATCH_NEGATIVE) ? 0 : 1; + return -1; +} + /* match a path against the vectorized pathspec */ -bool git_pathspec_match_path( - git_vector *vspec, +bool git_pathspec__match( + const git_vector *vspec, const char *path, bool disable_fnmatch, bool casefold, - const char **matched_pathspec) + const char **matched_pathspec, + size_t *matched_at) { size_t i; - git_attr_fnmatch *match; - int fnmatch_flags = 0; - int (*use_strcmp)(const char *, const char *); - int (*use_strncmp)(const char *, const char *, size_t); + const git_attr_fnmatch *match; + struct pathspec_match_context ctxt; if (matched_pathspec) *matched_pathspec = NULL; + if (matched_at) + *matched_at = GIT_PATHSPEC_NOMATCH; if (!vspec || !vspec->length) return true; - if (disable_fnmatch) - fnmatch_flags = -1; - else if (casefold) - fnmatch_flags = FNM_CASEFOLD; - - if (casefold) { - use_strcmp = git__strcasecmp; - use_strncmp = git__strncasecmp; - } else { - use_strcmp = git__strcmp; - use_strncmp = git__strncmp; - } + pathspec_match_context_init(&ctxt, disable_fnmatch, casefold); git_vector_foreach(vspec, i, match) { - int result = (match->flags & GIT_ATTR_FNMATCH_MATCH_ALL) ? 0 : FNM_NOMATCH; + int result = pathspec_match_one(match, &ctxt, path); - if (result == FNM_NOMATCH) - result = use_strcmp(match->pattern, path) ? FNM_NOMATCH : 0; - - if (fnmatch_flags >= 0 && result == FNM_NOMATCH) - result = p_fnmatch(match->pattern, path, fnmatch_flags); - - /* if we didn't match, look for exact dirname prefix match */ - if (result == FNM_NOMATCH && - (match->flags & GIT_ATTR_FNMATCH_HASWILD) == 0 && - use_strncmp(path, match->pattern, match->length) == 0 && - path[match->length] == '/') - result = 0; - - if (result == 0) { + if (result >= 0) { if (matched_pathspec) *matched_pathspec = match->pattern; + if (matched_at) + *matched_at = i; - return (match->flags & GIT_ATTR_FNMATCH_NEGATIVE) ? false : true; + return (result != 0); } } @@ -167,27 +202,344 @@ bool git_pathspec_match_path( } -int git_pathspec_context_init( - git_pathspec_context *ctxt, const git_strarray *paths) +int git_pathspec__init(git_pathspec *ps, const git_strarray *paths) { int error = 0; - memset(ctxt, 0, sizeof(*ctxt)); + memset(ps, 0, sizeof(*ps)); - ctxt->prefix = git_pathspec_prefix(paths); + ps->prefix = git_pathspec_prefix(paths); - if ((error = git_pool_init(&ctxt->pool, 1, 0)) < 0 || - (error = git_pathspec_init(&ctxt->pathspec, paths, &ctxt->pool)) < 0) - git_pathspec_context_free(ctxt); + if ((error = git_pool_init(&ps->pool, 1, 0)) < 0 || + (error = git_pathspec__vinit(&ps->pathspec, paths, &ps->pool)) < 0) + git_pathspec__clear(ps); return error; } -void git_pathspec_context_free( - git_pathspec_context *ctxt) +void git_pathspec__clear(git_pathspec *ps) { - git__free(ctxt->prefix); - git_pathspec_free(&ctxt->pathspec); - git_pool_clear(&ctxt->pool); - memset(ctxt, 0, sizeof(*ctxt)); + git__free(ps->prefix); + git_pathspec__vfree(&ps->pathspec); + git_pool_clear(&ps->pool); + memset(ps, 0, sizeof(*ps)); +} + +int git_pathspec_new(git_pathspec **out, const git_strarray *pathspec) +{ + int error = 0; + git_pathspec *ps = git__malloc(sizeof(git_pathspec)); + GITERR_CHECK_ALLOC(ps); + + if ((error = git_pathspec__init(ps, pathspec)) < 0) { + git__free(ps); + return error; + } + + GIT_REFCOUNT_INC(ps); + *out = ps; + return 0; +} + +static void pathspec_free(git_pathspec *ps) +{ + git_pathspec__clear(ps); + git__free(ps); +} + +void git_pathspec_free(git_pathspec *ps) +{ + if (!ps) + return; + GIT_REFCOUNT_DEC(ps, pathspec_free); +} + +int git_pathspec_matches_path( + const git_pathspec *ps, uint32_t flags, const char *path) +{ + bool no_fnmatch = (flags & GIT_PATHSPEC_NO_GLOB) != 0; + bool casefold = (flags & GIT_PATHSPEC_IGNORE_CASE) != 0; + + assert(ps && path); + + return (0 != git_pathspec__match( + &ps->pathspec, path, no_fnmatch, casefold, NULL, NULL)); +} + +static void pathspec_match_free(git_pathspec_match_list *m) +{ + git_pathspec_free(m->pathspec); + m->pathspec = NULL; + + git_array_clear(m->matches); + git_array_clear(m->failures); + git_pool_clear(&m->pool); + git__free(m); +} + +static git_pathspec_match_list *pathspec_match_alloc(git_pathspec *ps) +{ + git_pathspec_match_list *m = git__calloc(1, sizeof(git_pathspec_match_list)); + + if (m != NULL && git_pool_init(&m->pool, 1, 0) < 0) { + pathspec_match_free(m); + m = NULL; + } + + /* need to keep reference to pathspec and increment refcount because + * failures array stores pointers to the pattern strings of the + * pathspec that had no matches + */ + GIT_REFCOUNT_INC(ps); + m->pathspec = ps; + + return m; +} + +GIT_INLINE(void) pathspec_mark_pattern(uint8_t *used, size_t pos, size_t *ct) +{ + if (!used[pos]) { + used[pos] = 1; + (*ct)++; + } +} + +static int pathspec_match_from_iterator( + git_pathspec_match_list **out, + git_iterator *iter, + uint32_t flags, + git_pathspec *ps) +{ + int error = 0; + git_pathspec_match_list *m; + const git_index_entry *entry = NULL; + struct pathspec_match_context ctxt; + git_vector *patterns = &ps->pathspec; + bool find_failures = (flags & GIT_PATHSPEC_FIND_FAILURES) != 0; + bool failures_only = (flags & GIT_PATHSPEC_FAILURES_ONLY) != 0; + size_t pos, used_ct = 0, found_files = 0; + git_index *index = NULL; + uint8_t *used_patterns = NULL; + char **file; + + *out = m = pathspec_match_alloc(ps); + GITERR_CHECK_ALLOC(m); + + if ((error = git_iterator_reset(iter, ps->prefix, ps->prefix)) < 0) + goto done; + + if (patterns->length > 0) { + used_patterns = git__calloc(patterns->length, sizeof(uint8_t)); + GITERR_CHECK_ALLOC(used_patterns); + } + + if (git_iterator_type(iter) == GIT_ITERATOR_TYPE_WORKDIR && + (error = git_repository_index__weakptr( + &index, git_iterator_owner(iter))) < 0) + goto done; + + pathspec_match_context_init(&ctxt, + (flags & GIT_PATHSPEC_NO_GLOB) != 0, git_iterator_ignore_case(iter)); + + while (!(error = git_iterator_advance(&entry, iter))) { + int result = -1; + + for (pos = 0; pos < patterns->length; ++pos) { + const git_attr_fnmatch *pat = git_vector_get(patterns, pos); + + result = pathspec_match_one(pat, &ctxt, entry->path); + if (result >= 0) + break; + } + + /* no matches for this path */ + if (result < 0) + continue; + + /* if result was a negative pattern match, then don't list file */ + if (!result) { + pathspec_mark_pattern(used_patterns, pos, &used_ct); + continue; + } + + /* check if path is untracked and ignored */ + if (index != NULL && + git_iterator_current_is_ignored(iter) && + git_index__find(NULL, index, entry->path, GIT_INDEX_STAGE_ANY) < 0) + continue; + + /* mark the matched pattern as used */ + pathspec_mark_pattern(used_patterns, pos, &used_ct); + ++found_files; + + /* if find_failures is on, check if any later patterns also match */ + if (find_failures && used_ct < patterns->length) { + for (++pos; pos < patterns->length; ++pos) { + const git_attr_fnmatch *pat = git_vector_get(patterns, pos); + if (used_patterns[pos]) + continue; + + if (pathspec_match_one(pat, &ctxt, entry->path) > 0) + pathspec_mark_pattern(used_patterns, pos, &used_ct); + } + } + + /* if only looking at failures, exit early or just continue */ + if (failures_only) { + if (used_ct == patterns->length) + break; + continue; + } + + /* insert matched path into matches array */ + if ((file = git_array_alloc(m->matches)) == NULL || + (*file = git_pool_strdup(&m->pool, entry->path)) == NULL) { + error = -1; + goto done; + } + } + + if (error < 0 && error != GIT_ITEROVER) + goto done; + error = 0; + + /* insert patterns that had no matches into failures array */ + if (find_failures && used_ct < patterns->length) { + for (pos = 0; pos < patterns->length; ++pos) { + const git_attr_fnmatch *pat = git_vector_get(patterns, pos); + if (used_patterns[pos]) + continue; + + if ((file = git_array_alloc(m->failures)) == NULL || + (*file = git_pool_strdup(&m->pool, pat->pattern)) == NULL) { + error = -1; + goto done; + } + } + } + + /* if every pattern failed to match, then we have failed */ + if ((flags & GIT_PATHSPEC_NO_MATCH_ERROR) != 0 && !found_files) { + giterr_set(GITERR_INVALID, "No matching files were found"); + error = GIT_ENOTFOUND; + } + +done: + git__free(used_patterns); + + if (error < 0) { + pathspec_match_free(m); + *out = NULL; + } + + return error; +} + +static git_iterator_flag_t pathspec_match_iter_flags(uint32_t flags) +{ + git_iterator_flag_t f = 0; + + if ((flags & GIT_PATHSPEC_IGNORE_CASE) != 0) + f |= GIT_ITERATOR_IGNORE_CASE; + else if ((flags & GIT_PATHSPEC_USE_CASE) != 0) + f |= GIT_ITERATOR_DONT_IGNORE_CASE; + + return f; +} + +int git_pathspec_match_workdir( + git_pathspec_match_list **out, + git_repository *repo, + uint32_t flags, + git_pathspec *ps) +{ + int error = 0; + git_iterator *iter; + + assert(out && repo); + + if (!(error = git_iterator_for_workdir( + &iter, repo, pathspec_match_iter_flags(flags), NULL, NULL))) { + + error = pathspec_match_from_iterator(out, iter, flags, ps); + + git_iterator_free(iter); + } + + return error; +} + +int git_pathspec_match_index( + git_pathspec_match_list **out, + git_index *index, + uint32_t flags, + git_pathspec *ps) +{ + int error = 0; + git_iterator *iter; + + assert(out && index); + + if (!(error = git_iterator_for_index( + &iter, index, pathspec_match_iter_flags(flags), NULL, NULL))) { + + error = pathspec_match_from_iterator(out, iter, flags, ps); + + git_iterator_free(iter); + } + + return error; +} + +int git_pathspec_match_tree( + git_pathspec_match_list **out, + git_tree *tree, + uint32_t flags, + git_pathspec *ps) +{ + int error = 0; + git_iterator *iter; + + assert(out && tree); + + if (!(error = git_iterator_for_tree( + &iter, tree, pathspec_match_iter_flags(flags), NULL, NULL))) { + + error = pathspec_match_from_iterator(out, iter, flags, ps); + + git_iterator_free(iter); + } + + return error; +} + +void git_pathspec_match_list_free(git_pathspec_match_list *m) +{ + pathspec_match_free(m); +} + +size_t git_pathspec_match_list_entrycount( + const git_pathspec_match_list *m) +{ + return git_array_size(m->matches); +} + +const char *git_pathspec_match_list_entry( + const git_pathspec_match_list *m, size_t pos) +{ + char **entry = git_array_get(m->matches, pos); + return entry ? *entry : NULL; +} + +size_t git_pathspec_match_list_failed_entrycount( + const git_pathspec_match_list *m) +{ + return git_array_size(m->failures); +} + +const char * git_pathspec_match_list_failed_entry( + const git_pathspec_match_list *m, size_t pos) +{ + char **entry = git_array_get(m->failures, pos); + return entry ? *entry : NULL; } diff --git a/src/pathspec.h b/src/pathspec.h index f6509df4c..e7edfea38 100644 --- a/src/pathspec.h +++ b/src/pathspec.h @@ -8,9 +8,27 @@ #define INCLUDE_pathspec_h__ #include "common.h" +#include #include "buffer.h" #include "vector.h" #include "pool.h" +#include "array.h" + +/* public compiled pathspec */ +struct git_pathspec { + git_refcount rc; + char *prefix; + git_vector pathspec; + git_pool pool; +}; + +/* public interface to pathspec matching */ +struct git_pathspec_match_list { + git_pathspec *pathspec; + git_array_t(char *) matches; + git_array_t(char *) failures; + git_pool pool; +}; /* what is the common non-wildcard prefix for all items in the pathspec */ extern char *git_pathspec_prefix(const git_strarray *pathspec); @@ -19,36 +37,31 @@ extern char *git_pathspec_prefix(const git_strarray *pathspec); extern bool git_pathspec_is_empty(const git_strarray *pathspec); /* build a vector of fnmatch patterns to evaluate efficiently */ -extern int git_pathspec_init( +extern int git_pathspec__vinit( git_vector *vspec, const git_strarray *strspec, git_pool *strpool); /* free data from the pathspec vector */ -extern void git_pathspec_free(git_vector *vspec); +extern void git_pathspec__vfree(git_vector *vspec); + +#define GIT_PATHSPEC_NOMATCH ((size_t)-1) /* * Match a path against the vectorized pathspec. * The matched pathspec is passed back into the `matched_pathspec` parameter, * unless it is passed as NULL by the caller. */ -extern bool git_pathspec_match_path( - git_vector *vspec, +extern bool git_pathspec__match( + const git_vector *vspec, const char *path, bool disable_fnmatch, bool casefold, - const char **matched_pathspec); + const char **matched_pathspec, + size_t *matched_at); /* easy pathspec setup */ -typedef struct { - char *prefix; - git_vector pathspec; - git_pool pool; -} git_pathspec_context; +extern int git_pathspec__init(git_pathspec *ps, const git_strarray *paths); -extern int git_pathspec_context_init( - git_pathspec_context *ctxt, const git_strarray *paths); - -extern void git_pathspec_context_free( - git_pathspec_context *ctxt); +extern void git_pathspec__clear(git_pathspec *ps); #endif diff --git a/tests-clar/repo/pathspec.c b/tests-clar/repo/pathspec.c new file mode 100644 index 000000000..334066b67 --- /dev/null +++ b/tests-clar/repo/pathspec.c @@ -0,0 +1,385 @@ +#include "clar_libgit2.h" +#include "git2/pathspec.h" + +static git_repository *g_repo; + +void test_repo_pathspec__initialize(void) +{ + g_repo = cl_git_sandbox_init("status"); +} + +void test_repo_pathspec__cleanup(void) +{ + cl_git_sandbox_cleanup(); + g_repo = NULL; +} + +static char *str0[] = { "*_file", "new_file", "garbage" }; +static char *str1[] = { "*_FILE", "NEW_FILE", "GARBAGE" }; +static char *str2[] = { "staged_*" }; +static char *str3[] = { "!subdir", "*_file", "new_file" }; +static char *str4[] = { "*" }; +static char *str5[] = { "S*" }; + +void test_repo_pathspec__workdir0(void) +{ + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "*_file", "new_file", "garbage" } */ + s.strings = str0; s.count = ARRAY_SIZE(str0); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, 0, ps)); + cl_assert_equal_sz(10, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(10, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(1, git_pathspec_match_list_failed_entrycount(m)); + cl_assert_equal_s("garbage", git_pathspec_match_list_failed_entry(m, 0)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_FIND_FAILURES | GIT_PATHSPEC_FAILURES_ONLY, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(1, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); +} + +void test_repo_pathspec__workdir1(void) +{ + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "*_FILE", "NEW_FILE", "GARBAGE" } */ + s.strings = str1; s.count = ARRAY_SIZE(str1); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_IGNORE_CASE, ps)); + cl_assert_equal_sz(10, git_pathspec_match_list_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_USE_CASE, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_fail(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_USE_CASE | GIT_PATHSPEC_NO_MATCH_ERROR, ps)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_IGNORE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(10, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(1, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_USE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(3, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); +} + +void test_repo_pathspec__workdir2(void) +{ + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "staged_*" } */ + s.strings = str2; s.count = ARRAY_SIZE(str2); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, 0, ps)); + cl_assert_equal_sz(5, git_pathspec_match_list_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(5, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_fail(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_NO_GLOB | GIT_PATHSPEC_NO_MATCH_ERROR, ps)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_NO_GLOB | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(1, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); +} + +void test_repo_pathspec__workdir3(void) +{ + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "!subdir", "*_file", "new_file" } */ + s.strings = str3; s.count = ARRAY_SIZE(str3); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, 0, ps)); + cl_assert_equal_sz(7, git_pathspec_match_list_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, + GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(7, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + + cl_assert_equal_s("current_file", git_pathspec_match_list_entry(m, 0)); + cl_assert_equal_s("modified_file", git_pathspec_match_list_entry(m, 1)); + cl_assert_equal_s("new_file", git_pathspec_match_list_entry(m, 2)); + cl_assert_equal_s("staged_changes_modified_file", git_pathspec_match_list_entry(m, 3)); + cl_assert_equal_s("staged_delete_modified_file", git_pathspec_match_list_entry(m, 4)); + cl_assert_equal_s("staged_new_file", git_pathspec_match_list_entry(m, 5)); + cl_assert_equal_s("staged_new_file_modified_file", git_pathspec_match_list_entry(m, 6)); + cl_assert_equal_s(NULL, git_pathspec_match_list_entry(m, 7)); + + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); +} + +void test_repo_pathspec__workdir4(void) +{ + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "*" } */ + s.strings = str4; s.count = ARRAY_SIZE(str4); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_workdir(&m, g_repo, 0, ps)); + cl_assert_equal_sz(13, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_s("θΏ™", git_pathspec_match_list_entry(m, 12)); + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); +} + + +void test_repo_pathspec__index0(void) +{ + git_index *idx; + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + cl_git_pass(git_repository_index(&idx, g_repo)); + + /* { "*_file", "new_file", "garbage" } */ + s.strings = str0; s.count = ARRAY_SIZE(str0); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_index(&m, idx, 0, ps)); + cl_assert_equal_sz(9, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + cl_assert_equal_s("current_file", git_pathspec_match_list_entry(m, 0)); + cl_assert_equal_s("modified_file", git_pathspec_match_list_entry(m, 1)); + cl_assert_equal_s("staged_changes_modified_file", git_pathspec_match_list_entry(m, 2)); + cl_assert_equal_s("staged_new_file", git_pathspec_match_list_entry(m, 3)); + cl_assert_equal_s("staged_new_file_deleted_file", git_pathspec_match_list_entry(m, 4)); + cl_assert_equal_s("staged_new_file_modified_file", git_pathspec_match_list_entry(m, 5)); + cl_assert_equal_s("subdir/current_file", git_pathspec_match_list_entry(m, 6)); + cl_assert_equal_s("subdir/deleted_file", git_pathspec_match_list_entry(m, 7)); + cl_assert_equal_s("subdir/modified_file", git_pathspec_match_list_entry(m, 8)); + cl_assert_equal_s(NULL, git_pathspec_match_list_entry(m, 9)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_index(&m, idx, + GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(9, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(2, git_pathspec_match_list_failed_entrycount(m)); + cl_assert_equal_s("new_file", git_pathspec_match_list_failed_entry(m, 0)); + cl_assert_equal_s("garbage", git_pathspec_match_list_failed_entry(m, 1)); + cl_assert_equal_s(NULL, git_pathspec_match_list_failed_entry(m, 2)); + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); + git_index_free(idx); +} + +void test_repo_pathspec__index1(void) +{ + /* Currently the USE_CASE and IGNORE_CASE flags don't work on the + * index because the index sort order for the index iterator is + * set by the index itself. I think the correct fix is for the + * index not to embed a global sort order but to support traversal + * in either case sensitive or insensitive order in a stateless + * manner. + * + * Anyhow, as it is, there is no point in doing this test. + */ +#if 0 + git_index *idx; + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + cl_git_pass(git_repository_index(&idx, g_repo)); + + /* { "*_FILE", "NEW_FILE", "GARBAGE" } */ + s.strings = str1; s.count = ARRAY_SIZE(str1); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_pathspec_match_index(&m, idx, + GIT_PATHSPEC_USE_CASE, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_index(&m, idx, + GIT_PATHSPEC_USE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(3, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_index(&m, idx, + GIT_PATHSPEC_IGNORE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(10, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(2, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + git_pathspec_free(ps); + git_index_free(idx); +#endif +} + +void test_repo_pathspec__tree0(void) +{ + git_object *tree; + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "*_file", "new_file", "garbage" } */ + s.strings = str0; s.count = ARRAY_SIZE(str0); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_revparse_single(&tree, g_repo, "HEAD~2^{tree}")); + + cl_git_pass(git_pathspec_match_tree(&m, (git_tree *)tree, + GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(4, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_s("current_file", git_pathspec_match_list_entry(m, 0)); + cl_assert_equal_s("modified_file", git_pathspec_match_list_entry(m, 1)); + cl_assert_equal_s("staged_changes_modified_file", git_pathspec_match_list_entry(m, 2)); + cl_assert_equal_s("staged_delete_modified_file", git_pathspec_match_list_entry(m, 3)); + cl_assert_equal_s(NULL, git_pathspec_match_list_entry(m, 4)); + cl_assert_equal_sz(2, git_pathspec_match_list_failed_entrycount(m)); + cl_assert_equal_s("new_file", git_pathspec_match_list_failed_entry(m, 0)); + cl_assert_equal_s("garbage", git_pathspec_match_list_failed_entry(m, 1)); + cl_assert_equal_s(NULL, git_pathspec_match_list_failed_entry(m, 2)); + git_pathspec_match_list_free(m); + + git_object_free(tree); + + cl_git_pass(git_revparse_single(&tree, g_repo, "HEAD^{tree}")); + + cl_git_pass(git_pathspec_match_tree(&m, (git_tree *)tree, + GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(7, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_s("current_file", git_pathspec_match_list_entry(m, 0)); + cl_assert_equal_s("modified_file", git_pathspec_match_list_entry(m, 1)); + cl_assert_equal_s("staged_changes_modified_file", git_pathspec_match_list_entry(m, 2)); + cl_assert_equal_s("staged_delete_modified_file", git_pathspec_match_list_entry(m, 3)); + cl_assert_equal_s("subdir/current_file", git_pathspec_match_list_entry(m, 4)); + cl_assert_equal_s("subdir/deleted_file", git_pathspec_match_list_entry(m, 5)); + cl_assert_equal_s("subdir/modified_file", git_pathspec_match_list_entry(m, 6)); + cl_assert_equal_s(NULL, git_pathspec_match_list_entry(m, 7)); + cl_assert_equal_sz(2, git_pathspec_match_list_failed_entrycount(m)); + cl_assert_equal_s("new_file", git_pathspec_match_list_failed_entry(m, 0)); + cl_assert_equal_s("garbage", git_pathspec_match_list_failed_entry(m, 1)); + cl_assert_equal_s(NULL, git_pathspec_match_list_failed_entry(m, 2)); + git_pathspec_match_list_free(m); + + git_object_free(tree); + + git_pathspec_free(ps); +} + +void test_repo_pathspec__tree5(void) +{ + git_object *tree; + git_strarray s; + git_pathspec *ps; + git_pathspec_match_list *m; + + /* { "S*" } */ + s.strings = str5; s.count = ARRAY_SIZE(str5); + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_git_pass(git_revparse_single(&tree, g_repo, "HEAD~2^{tree}")); + + cl_git_pass(git_pathspec_match_tree(&m, (git_tree *)tree, + GIT_PATHSPEC_USE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(0, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_sz(1, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + cl_git_pass(git_pathspec_match_tree(&m, (git_tree *)tree, + GIT_PATHSPEC_IGNORE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(5, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_s("staged_changes", git_pathspec_match_list_entry(m, 0)); + cl_assert_equal_s("staged_delete_modified_file", git_pathspec_match_list_entry(m, 4)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + git_object_free(tree); + + cl_git_pass(git_revparse_single(&tree, g_repo, "HEAD^{tree}")); + + cl_git_pass(git_pathspec_match_tree(&m, (git_tree *)tree, + GIT_PATHSPEC_IGNORE_CASE | GIT_PATHSPEC_FIND_FAILURES, ps)); + cl_assert_equal_sz(9, git_pathspec_match_list_entrycount(m)); + cl_assert_equal_s("staged_changes", git_pathspec_match_list_entry(m, 0)); + cl_assert_equal_s("subdir.txt", git_pathspec_match_list_entry(m, 5)); + cl_assert_equal_s("subdir/current_file", git_pathspec_match_list_entry(m, 6)); + cl_assert_equal_sz(0, git_pathspec_match_list_failed_entrycount(m)); + git_pathspec_match_list_free(m); + + git_object_free(tree); + + git_pathspec_free(ps); +} + +void test_repo_pathspec__in_memory(void) +{ + static char *strings[] = { "one", "two*", "!three*", "*four" }; + git_strarray s = { strings, ARRAY_SIZE(strings) }; + git_pathspec *ps; + + cl_git_pass(git_pathspec_new(&ps, &s)); + + cl_assert(git_pathspec_matches_path(ps, 0, "one")); + cl_assert(!git_pathspec_matches_path(ps, 0, "ONE")); + cl_assert(git_pathspec_matches_path(ps, GIT_PATHSPEC_IGNORE_CASE, "ONE")); + cl_assert(git_pathspec_matches_path(ps, 0, "two")); + cl_assert(git_pathspec_matches_path(ps, 0, "two.txt")); + cl_assert(!git_pathspec_matches_path(ps, 0, "three.txt")); + cl_assert(git_pathspec_matches_path(ps, 0, "anything.four")); + cl_assert(!git_pathspec_matches_path(ps, 0, "three.four")); + cl_assert(!git_pathspec_matches_path(ps, 0, "nomatch")); + cl_assert(!git_pathspec_matches_path(ps, GIT_PATHSPEC_NO_GLOB, "two")); + cl_assert(git_pathspec_matches_path(ps, GIT_PATHSPEC_NO_GLOB, "two*")); + cl_assert(!git_pathspec_matches_path(ps, GIT_PATHSPEC_NO_GLOB, "anyfour")); + cl_assert(git_pathspec_matches_path(ps, GIT_PATHSPEC_NO_GLOB, "*four")); + + git_pathspec_free(ps); +} From 0d44d3dc84d6996c72d49e6bb8036b9c8edec9e0 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Mon, 24 Jun 2013 23:21:23 -0700 Subject: [PATCH 03/17] Extending log example code This adds more command line processing to the example version of log. In particular, this adds the funky command line processing that allows an arbitrary series of revisions followed by an arbitrary number of paths and/or glob patterns. The actual logging part still isn't implemented. --- examples/log.c | 53 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/examples/log.c b/examples/log.c index f92b66996..83e5cbcc0 100644 --- a/examples/log.c +++ b/examples/log.c @@ -32,24 +32,71 @@ static void usage(const char *message, const char *arg) int main(int argc, char *argv[]) { - int i; + int i, j, last_nonoption, force_files = -1; char *a; const char *dir = "."; git_repository *repo; + git_revwalk *walker; + git_revspec revs; git_threads_init(); - for (i = 1; i < argc; ++i) { + for (i = 1, last_nonoption = 1; i < argc; ++i) { a = argv[i]; - if (a[0] != '-') { + if (a[0] != '-' || force_files > 0) { + /* condense args not prefixed with '-' to start of argv */ + if (last_nonoption != i) + argv[last_nonoption] = a; + last_nonoption++; } + else if (!strcmp(a, "--")) + force_files = last_nonoption; /* copy all args as filenames */ else if (!check_str_param(a, "--git-dir=", &dir)) usage("Unknown argument", a); } check(git_repository_open_ext(&repo, dir, 0, NULL), "Could not open repository"); + check(git_revwalk_new(&walker, repo), + "Could not create revision walker"); + + if (force_files < 0) + force_files = last_nonoption; + + for (i = 1; i < force_files; ) { + printf("option '%s'\n", argv[i]); + + if (!git_revparse(&revs, repo, argv[i])) { + char str[GIT_OID_HEXSZ+1]; + + if (revs.from) { + git_oid_tostr(str, sizeof(str), git_object_id(revs.from)); + printf("revwalk from %s\n", str); + } + if (revs.to) { + git_oid_tostr(str, sizeof(str), git_object_id(revs.to)); + printf("revwalk to %s\n", str); + } + + /* push / hide / merge-base in revwalker */ + + ++i; + } else { + /* shift array down */ + for (a = argv[i], j = i + 1; j < force_files; ++j) + argv[j - 1] = argv[j]; + argv[--force_files] = a; + } + } + + if (i == 1) { + /* no revs pushed so push HEAD */ + printf("revwalk HEAD\n"); + } + + for (i = force_files; i < last_nonoption; ++i) + printf("file %s\n", argv[i]); git_repository_free(repo); git_threads_shutdown(); From d0628e2feeaeebf78a6b67fa4ca3fc658aa4744a Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Tue, 25 Jun 2013 15:39:13 -0700 Subject: [PATCH 04/17] More progress on log example --- examples/log.c | 220 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 152 insertions(+), 68 deletions(-) diff --git a/examples/log.c b/examples/log.c index 83e5cbcc0..5603d743d 100644 --- a/examples/log.c +++ b/examples/log.c @@ -3,21 +3,15 @@ #include #include -static void check(int error, const char *message) +static void check(int error, const char *message, const char *arg) { - if (error) { + if (!error) + return; + if (arg) + fprintf(stderr, "%s '%s' (%d)\n", message, arg, error); + else fprintf(stderr, "%s (%d)\n", message, error); - exit(1); - } -} - -static int check_str_param(const char *arg, const char *pat, const char **val) -{ - size_t len = strlen(pat); - if (strncmp(arg, pat, len)) - return 0; - *val = (const char *)(arg + len); - return 1; + exit(1); } static void usage(const char *message, const char *arg) @@ -30,75 +24,165 @@ static void usage(const char *message, const char *arg) exit(1); } +struct log_state { + git_repository *repo; + const char *repodir; + git_revwalk *walker; + int hide; + int sorting; +}; + +static void set_sorting(struct log_state *s, unsigned int sort_mode) +{ + if (!s->repo) { + if (!s->repodir) s->repodir = "."; + check(git_repository_open_ext(&s->repo, s->repodir, 0, NULL), + "Could not open repository", s->repodir); + } + + if (!s->walker) + check(git_revwalk_new(&s->walker, s->repo), + "Could not create revision walker", NULL); + + if (sort_mode == GIT_SORT_REVERSE) + s->sorting = s->sorting ^ GIT_SORT_REVERSE; + else + s->sorting = sort_mode | (s->sorting & GIT_SORT_REVERSE); + + git_revwalk_sorting(s->walker, s->sorting); +} + +static void push_rev(struct log_state *s, git_object *obj, int hide) +{ + hide = s->hide ^ hide; + + if (!s->walker) + check(git_revwalk_new(&s->walker, s->repo), + "Could not create revision walker", NULL); + + if (!obj) + check(git_revwalk_push_head(s->walker), + "Could not find repository HEAD", NULL); + else if (hide) + check(git_revwalk_hide(s->walker, git_object_id(obj)), + "Reference does not refer to a commit", NULL); + else + check(git_revwalk_push(s->walker, git_object_id(obj)), + "Reference does not refer to a commit", NULL); + + git_object_free(obj); +} + +static int add_revision(struct log_state *s, const char *revstr) +{ + git_revspec revs; + int hide = 0; + + if (!s->repo) { + if (!s->repodir) s->repodir = "."; + check(git_repository_open_ext(&s->repo, s->repodir, 0, NULL), + "Could not open repository", s->repodir); + } + + if (!revstr) + push_rev(s, NULL, hide); + else if (*revstr == '^') { + revs.flags = GIT_REVPARSE_SINGLE; + hide = !hide; + if (!git_revparse_single(&revs.from, s->repo, revstr + 1)) + return -1; + } else + if (!git_revparse(&revs, s->repo, revstr)) + return -1; + + if ((revs.flags & GIT_REVPARSE_SINGLE) != 0) + push_rev(s, revs.from, hide); + else { + push_rev(s, revs.to, hide); + + if ((revs.flags & GIT_REVPARSE_MERGE_BASE) != 0) { + git_oid base; + check(git_merge_base(&base, s->repo, + git_object_id(revs.from), git_object_id(revs.to)), + "Could not find merge base", revstr); + check(git_object_lookup(&revs.to, s->repo, &base, GIT_OBJ_COMMIT), + "Could not find merge base commit", NULL); + + push_rev(s, revs.to, hide); + } + + push_rev(s, revs.from, !hide); + } + + return 0; +} + +struct log_options { + int show_diff; + int skip; + int min_parents, max_parents; + git_time_t before; + git_time_t after; + char *author; + char *committer; + +}; + int main(int argc, char *argv[]) { - int i, j, last_nonoption, force_files = -1; + int i, count = 0; char *a; - const char *dir = "."; - git_repository *repo; - git_revwalk *walker; - git_revspec revs; + struct log_state s; + git_strarray paths; + git_oid oid; + git_commit *commit; + char buf[GIT_OID_HEXSZ + 1]; git_threads_init(); - for (i = 1, last_nonoption = 1; i < argc; ++i) { + memset(&s, 0, sizeof(s)); + + for (i = 1; i < argc; ++i) { a = argv[i]; - if (a[0] != '-' || force_files > 0) { - /* condense args not prefixed with '-' to start of argv */ - if (last_nonoption != i) - argv[last_nonoption] = a; - last_nonoption++; - } - else if (!strcmp(a, "--")) - force_files = last_nonoption; /* copy all args as filenames */ - else if (!check_str_param(a, "--git-dir=", &dir)) - usage("Unknown argument", a); - } - - check(git_repository_open_ext(&repo, dir, 0, NULL), - "Could not open repository"); - check(git_revwalk_new(&walker, repo), - "Could not create revision walker"); - - if (force_files < 0) - force_files = last_nonoption; - - for (i = 1; i < force_files; ) { - printf("option '%s'\n", argv[i]); - - if (!git_revparse(&revs, repo, argv[i])) { - char str[GIT_OID_HEXSZ+1]; - - if (revs.from) { - git_oid_tostr(str, sizeof(str), git_object_id(revs.from)); - printf("revwalk from %s\n", str); - } - if (revs.to) { - git_oid_tostr(str, sizeof(str), git_object_id(revs.to)); - printf("revwalk to %s\n", str); - } - - /* push / hide / merge-base in revwalker */ - + if (a[0] != '-') { + if (!add_revision(&s, a)) + ++count; + else /* try failed revision parse as filename */ + break; + } else if (!strcmp(a, "--")) { ++i; - } else { - /* shift array down */ - for (a = argv[i], j = i + 1; j < force_files; ++j) - argv[j - 1] = argv[j]; - argv[--force_files] = a; + break; } + else if (!strcmp(a, "--date-order")) + set_sorting(&s, GIT_SORT_TIME); + else if (!strcmp(a, "--topo-order")) + set_sorting(&s, GIT_SORT_TOPOLOGICAL); + else if (!strcmp(a, "--reverse")) + set_sorting(&s, GIT_SORT_REVERSE); + else if (!strncmp(a, "--git-dir=", strlen("--git-dir="))) + s.repodir = a + strlen("--git-dir="); + else + usage("Unsupported argument", a); } - if (i == 1) { - /* no revs pushed so push HEAD */ - printf("revwalk HEAD\n"); + if (!count) + add_revision(&s, NULL); + + paths.strings = &argv[i]; + paths.count = argc - i; + + while (!git_revwalk_next(&oid, s.walker)) { + check(git_commit_lookup(&commit, s.repo, &oid), + "Failed to look up commit", NULL); + git_commit_free(commit); + + git_oid_tostr(buf, sizeof(buf), &oid); + printf("%s\n", buf); } - for (i = force_files; i < last_nonoption; ++i) - printf("file %s\n", argv[i]); - - git_repository_free(repo); + git_revwalk_free(s.walker); + git_repository_free(s.repo); git_threads_shutdown(); return 0; From 8ba0ff69725251fa375520d9c69c8a053725c4b6 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Tue, 25 Jun 2013 15:39:44 -0700 Subject: [PATCH 05/17] rev-parse example --- examples/Makefile | 2 +- examples/rev-parse.c | 106 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 examples/rev-parse.c diff --git a/examples/Makefile b/examples/Makefile index 6288906df..95e46f0c6 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -3,7 +3,7 @@ CC = gcc CFLAGS = -g -I../include -I../src -Wall -Wextra -Wmissing-prototypes -Wno-missing-field-initializers LFLAGS = -L../build -lgit2 -lz -APPS = general showindex diff rev-list cat-file status log +APPS = general showindex diff rev-list cat-file status log rev-parse all: $(APPS) diff --git a/examples/rev-parse.c b/examples/rev-parse.c new file mode 100644 index 000000000..cdbb61e46 --- /dev/null +++ b/examples/rev-parse.c @@ -0,0 +1,106 @@ +#include +#include +#include +#include + +static void check(int error, const char *message, const char *arg) +{ + if (!error) + return; + if (arg) + fprintf(stderr, "%s %s (%d)\n", message, arg, error); + else + fprintf(stderr, "%s(%d)\n", message, error); + exit(1); +} + +static void usage(const char *message, const char *arg) +{ + if (message && arg) + fprintf(stderr, "%s: %s\n", message, arg); + else if (message) + fprintf(stderr, "%s\n", message); + fprintf(stderr, "usage: rev-parse [ --option ] ...\n"); + exit(1); +} + +struct parse_state { + git_repository *repo; + const char *repodir; + int not; +}; + +static int parse_revision(struct parse_state *ps, const char *revstr) +{ + git_revspec rs; + char str[GIT_OID_HEXSZ + 1]; + + if (!ps->repo) { + if (!ps->repodir) + ps->repodir = "."; + check(git_repository_open_ext(&ps->repo, ps->repodir, 0, NULL), + "Could not open repository from", ps->repodir); + } + + check(git_revparse(&rs, ps->repo, revstr), "Could not parse", revstr); + + if ((rs.flags & GIT_REVPARSE_SINGLE) != 0) { + git_oid_tostr(str, sizeof(str), git_object_id(rs.from)); + printf("%s\n", str); + git_object_free(rs.from); + } + else if ((rs.flags & GIT_REVPARSE_RANGE) != 0) { + git_oid_tostr(str, sizeof(str), git_object_id(rs.to)); + printf("%s\n", str); + git_object_free(rs.to); + + if ((rs.flags & GIT_REVPARSE_MERGE_BASE) != 0) { + git_oid base; + check(git_merge_base(&base, ps->repo, + git_object_id(rs.from), git_object_id(rs.to)), + "Could not find merge base", revstr); + + git_oid_tostr(str, sizeof(str), &base); + printf("%s\n", str); + } + + git_oid_tostr(str, sizeof(str), git_object_id(rs.from)); + printf("^%s\n", str); + git_object_free(rs.from); + } + else { + check(0, "Invalid results from git_revparse", revstr); + } + + return 0; +} + +int main(int argc, char *argv[]) +{ + int i; + char *a; + struct parse_state ps; + + git_threads_init(); + + memset(&ps, 0, sizeof(ps)); + + for (i = 1; i < argc; ++i) { + a = argv[i]; + + if (a[0] != '-') { + if (parse_revision(&ps, a) != 0) + break; + } else if (!strcmp(a, "--not")) + ps.not = !ps.not; + else if (!strncmp(a, "--git-dir=", strlen("--git-dir="))) + ps.repodir = a + strlen("--git-dir="); + else + usage("Cannot handle argument", a); + } + + git_repository_free(ps.repo); + git_threads_shutdown(); + + return 0; +} From f094f9052fba43707cb5662a362511eeea4c4af5 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Mon, 1 Jul 2013 15:41:01 -0700 Subject: [PATCH 06/17] Add raw header access to commit API --- include/git2/commit.h | 8 ++++++++ src/commit.c | 47 ++++++++++++++++++++++++++++++++++--------- src/commit.h | 1 + 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/include/git2/commit.h b/include/git2/commit.h index 544d21d87..fc0551be1 100644 --- a/include/git2/commit.h +++ b/include/git2/commit.h @@ -129,6 +129,14 @@ GIT_EXTERN(const git_signature *) git_commit_committer(const git_commit *commit) */ GIT_EXTERN(const git_signature *) git_commit_author(const git_commit *commit); +/** + * Get the full raw text of the commit header. + * + * @param commit a previously loaded commit + * @return the header text of the commit + */ +GIT_EXTERN(const char *) git_commit_raw_header(const git_commit *commit); + /** * Get the tree pointed to by a commit. * diff --git a/src/commit.c b/src/commit.c index 1ab9b34f7..cf50c2d37 100644 --- a/src/commit.c +++ b/src/commit.c @@ -41,6 +41,7 @@ void git_commit__free(void *_commit) git_signature_free(commit->author); git_signature_free(commit->committer); + git__free(commit->raw_header); git__free(commit->message); git__free(commit->message_encoding); git__free(commit); @@ -171,11 +172,33 @@ int git_commit_create( int git_commit__parse(void *_commit, git_odb_object *odb_obj) { git_commit *commit = _commit; - const char *buffer = git_odb_object_data(odb_obj); - const char *buffer_end = buffer + git_odb_object_size(odb_obj); + const char *buffer_start = git_odb_object_data(odb_obj), *buffer; + const char *buffer_end = buffer_start + git_odb_object_size(odb_obj); git_oid parent_id; + size_t parent_count = 0, header_len; - if (git_vector_init(&commit->parent_ids, 4, NULL) < 0) + /* find end-of-header (counting parents as we go) */ + for (buffer = buffer_start; buffer < buffer_end; ++buffer) { + if (!strncmp("\n\n", buffer, 2)) { + ++buffer; + break; + } + if (!strncmp("\nparent ", buffer, strlen("\nparent "))) + ++parent_count; + } + + header_len = buffer - buffer_start; + commit->raw_header = git__strndup(buffer_start, header_len); + GITERR_CHECK_ALLOC(commit->raw_header); + + /* point "buffer" to header data */ + buffer = commit->raw_header; + buffer_end = commit->raw_header + header_len; + + if (parent_count < 1) + parent_count = 1; + + if (git_vector_init(&commit->parent_ids, parent_count, NULL) < 0) return -1; if (git_oid__parse(&commit->tree_id, &buffer, buffer_end, "tree ") < 0) @@ -208,8 +231,8 @@ int git_commit__parse(void *_commit, git_odb_object *odb_obj) if (git_signature__parse(commit->committer, &buffer, buffer_end, "committer ", '\n') < 0) return -1; - /* Parse add'l header entries until blank line found */ - while (buffer < buffer_end && *buffer != '\n') { + /* Parse add'l header entries */ + while (buffer < buffer_end) { const char *eoln = buffer; while (eoln < buffer_end && *eoln != '\n') ++eoln; @@ -223,15 +246,18 @@ int git_commit__parse(void *_commit, git_odb_object *odb_obj) if (eoln < buffer_end && *eoln == '\n') ++eoln; - buffer = eoln; } - /* buffer is now at the end of the header, double-check and move forward into the message */ - if (buffer < buffer_end && *buffer == '\n') - buffer++; + /* point "buffer" to data after header */ + buffer = git_odb_object_data(odb_obj); + buffer_end = buffer + git_odb_object_size(odb_obj); - /* parse commit message */ + buffer += header_len; + if (*buffer == '\n') + ++buffer; + + /* extract commit message */ if (buffer <= buffer_end) { commit->message = git__strndup(buffer, buffer_end - buffer); GITERR_CHECK_ALLOC(commit->message); @@ -255,6 +281,7 @@ GIT_COMMIT_GETTER(const git_signature *, author, commit->author) GIT_COMMIT_GETTER(const git_signature *, committer, commit->committer) GIT_COMMIT_GETTER(const char *, message, commit->message) GIT_COMMIT_GETTER(const char *, message_encoding, commit->message_encoding) +GIT_COMMIT_GETTER(const char *, raw_header, commit->raw_header) GIT_COMMIT_GETTER(git_time_t, time, commit->committer->when.time) GIT_COMMIT_GETTER(int, time_offset, commit->committer->when.offset) GIT_COMMIT_GETTER(unsigned int, parentcount, (unsigned int)commit->parent_ids.length) diff --git a/src/commit.h b/src/commit.h index d0981b125..70d8fc690 100644 --- a/src/commit.h +++ b/src/commit.h @@ -25,6 +25,7 @@ struct git_commit { char *message_encoding; char *message; + char *raw_header; }; void git_commit__free(void *commit); From f44c4fa108ff7e326607812246c3c056c1b901cc Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Mon, 1 Jul 2013 15:41:32 -0700 Subject: [PATCH 07/17] Add basic commit formatting to log output --- examples/log.c | 58 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/examples/log.c b/examples/log.c index 5603d743d..12c1d5666 100644 --- a/examples/log.c +++ b/examples/log.c @@ -117,6 +117,32 @@ static int add_revision(struct log_state *s, const char *revstr) return 0; } +static void print_time(const git_time *intime, const char *prefix) +{ + char sign, out[32]; + struct tm intm; + int offset, hours, minutes; + time_t t; + + offset = intime->offset; + if (offset < 0) { + sign = '-'; + offset = -offset; + } else { + sign = '+'; + } + + hours = offset / 60; + minutes = offset % 60; + + t = (time_t)intime->time + (intime->offset * 60); + + gmtime_r(&t, &intm); + strftime(out, sizeof(out), "%a %b %d %T %Y", &intm); + + printf("%s%s %c%02d%02d\n", prefix, out, sign, hours, minutes); +} + struct log_options { int show_diff; int skip; @@ -125,7 +151,6 @@ struct log_options { git_time_t after; char *author; char *committer; - }; int main(int argc, char *argv[]) @@ -173,12 +198,39 @@ int main(int argc, char *argv[]) paths.count = argc - i; while (!git_revwalk_next(&oid, s.walker)) { + const git_signature *sig; + const char *scan, *eol; + check(git_commit_lookup(&commit, s.repo, &oid), "Failed to look up commit", NULL); - git_commit_free(commit); git_oid_tostr(buf, sizeof(buf), &oid); - printf("%s\n", buf); + printf("commit %s\n", buf); + + if ((count = (int)git_commit_parentcount(commit)) > 1) { + printf("Merge:"); + for (i = 0; i < count; ++i) { + git_oid_tostr(buf, 8, git_commit_parent_id(commit, i)); + printf(" %s", buf); + } + printf("\n"); + } + + if ((sig = git_commit_author(commit)) != NULL) { + printf("Author: %s <%s>\n", sig->name, sig->email); + print_time(&sig->when, "Date: "); + } + printf("\n"); + + for (scan = git_commit_message(commit); scan && *scan; ) { + for (eol = scan; *eol && *eol != '\n'; ++eol) /* find eol */; + + printf(" %.*s\n", (int)(eol - scan), scan); + scan = *eol ? eol + 1 : NULL; + } + printf("\n"); + + git_commit_free(commit); } git_revwalk_free(s.walker); From 2b3bd8ecd88a403f9d034aa3a4d1e14c5e904255 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Wed, 3 Jul 2013 14:53:39 -0700 Subject: [PATCH 08/17] Fix example/log.c minor diffs with git log --- examples/log.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/log.c b/examples/log.c index 12c1d5666..61b789263 100644 --- a/examples/log.c +++ b/examples/log.c @@ -56,9 +56,11 @@ static void push_rev(struct log_state *s, git_object *obj, int hide) { hide = s->hide ^ hide; - if (!s->walker) + if (!s->walker) { check(git_revwalk_new(&s->walker, s->repo), "Could not create revision walker", NULL); + git_revwalk_sorting(s->walker, s->sorting); + } if (!obj) check(git_revwalk_push_head(s->walker), @@ -138,7 +140,7 @@ static void print_time(const git_time *intime, const char *prefix) t = (time_t)intime->time + (intime->offset * 60); gmtime_r(&t, &intm); - strftime(out, sizeof(out), "%a %b %d %T %Y", &intm); + strftime(out, sizeof(out), "%a %b %e %T %Y", &intm); printf("%s%s %c%02d%02d\n", prefix, out, sign, hours, minutes); } @@ -166,6 +168,7 @@ int main(int argc, char *argv[]) git_threads_init(); memset(&s, 0, sizeof(s)); + s.sorting = GIT_SORT_TIME; for (i = 1; i < argc; ++i) { a = argv[i]; From 5a169711fa9875bc98c30c49b5e9ea06ebbbcfeb Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Wed, 3 Jul 2013 15:08:54 -0700 Subject: [PATCH 09/17] fix bug with order args and no revision --- examples/log.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/log.c b/examples/log.c index 61b789263..f7aabf0b4 100644 --- a/examples/log.c +++ b/examples/log.c @@ -86,9 +86,12 @@ static int add_revision(struct log_state *s, const char *revstr) "Could not open repository", s->repodir); } - if (!revstr) + if (!revstr) { push_rev(s, NULL, hide); - else if (*revstr == '^') { + return 0; + } + + if (*revstr == '^') { revs.flags = GIT_REVPARSE_SINGLE; hide = !hide; if (!git_revparse_single(&revs.from, s->repo, revstr + 1)) From 733c4f3aca212d90459fb21cfbc137f09ff6c951 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Wed, 3 Jul 2013 15:12:14 -0700 Subject: [PATCH 10/17] more examples/log.c bug fixing --- examples/log.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/log.c b/examples/log.c index f7aabf0b4..ba411d7a4 100644 --- a/examples/log.c +++ b/examples/log.c @@ -94,11 +94,11 @@ static int add_revision(struct log_state *s, const char *revstr) if (*revstr == '^') { revs.flags = GIT_REVPARSE_SINGLE; hide = !hide; - if (!git_revparse_single(&revs.from, s->repo, revstr + 1)) - return -1; - } else - if (!git_revparse(&revs, s->repo, revstr)) + + if (git_revparse_single(&revs.from, s->repo, revstr + 1) < 0) return -1; + } else if (git_revparse(&revs, s->repo, revstr) < 0) + return -1; if ((revs.flags & GIT_REVPARSE_SINGLE) != 0) push_rev(s, revs.from, hide); From a8b5f116bc39f884c8888adae2fd3f9b96d972c0 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Wed, 3 Jul 2013 17:00:50 -0700 Subject: [PATCH 11/17] Fix example/log.c pathspec handling of merges This fixes the way the example log program decides if a merge commit should be shown when a pathspec is given. Also makes it easier to use the pathspec API to just check "does a tree match anything in the pathspec" without allocating a match list. --- examples/log.c | 124 ++++++++++++++++++++++++++++++++++++------------- include/git2.h | 1 + src/pathspec.c | 21 +++++---- 3 files changed, 106 insertions(+), 40 deletions(-) diff --git a/examples/log.c b/examples/log.c index ba411d7a4..50e81efad 100644 --- a/examples/log.c +++ b/examples/log.c @@ -158,15 +158,73 @@ struct log_options { char *committer; }; +static void print_commit(git_commit *commit) +{ + char buf[GIT_OID_HEXSZ + 1]; + int i, count; + const git_signature *sig; + const char *scan, *eol; + + git_oid_tostr(buf, sizeof(buf), git_commit_id(commit)); + printf("commit %s\n", buf); + + if ((count = (int)git_commit_parentcount(commit)) > 1) { + printf("Merge:"); + for (i = 0; i < count; ++i) { + git_oid_tostr(buf, 8, git_commit_parent_id(commit, i)); + printf(" %s", buf); + } + printf("\n"); + } + + if ((sig = git_commit_author(commit)) != NULL) { + printf("Author: %s <%s>\n", sig->name, sig->email); + print_time(&sig->when, "Date: "); + } + printf("\n"); + + for (scan = git_commit_message(commit); scan && *scan; ) { + for (eol = scan; *eol && *eol != '\n'; ++eol) /* find eol */; + + printf(" %.*s\n", (int)(eol - scan), scan); + scan = *eol ? eol + 1 : NULL; + } + printf("\n"); +} + +static int match_with_parent( + git_commit *commit, int i, git_diff_options *opts) +{ + git_commit *parent; + git_tree *a, *b; + git_diff_list *diff; + int ndeltas; + + check(git_commit_parent(&parent, commit, (size_t)i), "Get parent", NULL); + check(git_commit_tree(&a, parent), "Tree for parent", NULL); + check(git_commit_tree(&b, commit), "Tree for commit", NULL); + check(git_diff_tree_to_tree(&diff, git_commit_owner(commit), a, b, opts), + "Checking diff between parent and commit", NULL); + + ndeltas = (int)git_diff_num_deltas(diff); + + git_diff_list_free(diff); + git_tree_free(a); + git_tree_free(b); + git_commit_free(parent); + + return ndeltas > 0; +} + int main(int argc, char *argv[]) { - int i, count = 0; + int i, count = 0, parents; char *a; struct log_state s; - git_strarray paths; + git_diff_options diffopts = GIT_DIFF_OPTIONS_INIT; git_oid oid; git_commit *commit; - char buf[GIT_OID_HEXSZ + 1]; + git_pathspec *ps = NULL; git_threads_init(); @@ -200,45 +258,47 @@ int main(int argc, char *argv[]) if (!count) add_revision(&s, NULL); - paths.strings = &argv[i]; - paths.count = argc - i; - - while (!git_revwalk_next(&oid, s.walker)) { - const git_signature *sig; - const char *scan, *eol; + diffopts.pathspec.strings = &argv[i]; + diffopts.pathspec.count = argc - i; + count = 0; + if (diffopts.pathspec.count > 0) + check(git_pathspec_new(&ps, &diffopts.pathspec), + "Building pathspec", NULL); + for (; !git_revwalk_next(&oid, s.walker); git_commit_free(commit)) { check(git_commit_lookup(&commit, s.repo, &oid), "Failed to look up commit", NULL); - git_oid_tostr(buf, sizeof(buf), &oid); - printf("commit %s\n", buf); + parents = (int)git_commit_parentcount(commit); - if ((count = (int)git_commit_parentcount(commit)) > 1) { - printf("Merge:"); - for (i = 0; i < count; ++i) { - git_oid_tostr(buf, 8, git_commit_parent_id(commit, i)); - printf(" %s", buf); + if (diffopts.pathspec.count > 0) { + int unmatched = parents; + + if (parents == 0) { + git_tree *tree; + check(git_commit_tree(&tree, commit), "Get tree", NULL); + if (git_pathspec_match_tree( + NULL, tree, GIT_PATHSPEC_NO_MATCH_ERROR, ps) != 0) + unmatched = 1; + git_tree_free(tree); + } else if (parents == 1) { + unmatched = match_with_parent(commit, 0, &diffopts) ? 0 : 1; + } else { + for (i = 0; i < parents; ++i) { + if (match_with_parent(commit, i, &diffopts)) + unmatched--; + } } - printf("\n"); + + if (unmatched > 0) + continue; } - if ((sig = git_commit_author(commit)) != NULL) { - printf("Author: %s <%s>\n", sig->name, sig->email); - print_time(&sig->when, "Date: "); - } - printf("\n"); - - for (scan = git_commit_message(commit); scan && *scan; ) { - for (eol = scan; *eol && *eol != '\n'; ++eol) /* find eol */; - - printf(" %.*s\n", (int)(eol - scan), scan); - scan = *eol ? eol + 1 : NULL; - } - printf("\n"); - - git_commit_free(commit); + print_commit(commit); + ++count; } + git_pathspec_free(ps); git_revwalk_free(s.walker); git_repository_free(s.repo); git_threads_shutdown(); diff --git a/include/git2.h b/include/git2.h index 5f9fc4824..e8638a830 100644 --- a/include/git2.h +++ b/include/git2.h @@ -56,5 +56,6 @@ #include "git2/message.h" #include "git2/pack.h" #include "git2/stash.h" +#include "git2/pathspec.h" #endif diff --git a/src/pathspec.c b/src/pathspec.c index 35421dbef..021f38f1c 100644 --- a/src/pathspec.c +++ b/src/pathspec.c @@ -311,7 +311,7 @@ static int pathspec_match_from_iterator( git_pathspec *ps) { int error = 0; - git_pathspec_match_list *m; + git_pathspec_match_list *m = NULL; const git_index_entry *entry = NULL; struct pathspec_match_context ctxt; git_vector *patterns = &ps->pathspec; @@ -322,8 +322,13 @@ static int pathspec_match_from_iterator( uint8_t *used_patterns = NULL; char **file; - *out = m = pathspec_match_alloc(ps); - GITERR_CHECK_ALLOC(m); + if (out) { + *out = m = pathspec_match_alloc(ps); + GITERR_CHECK_ALLOC(m); + } else { + failures_only = true; + find_failures = false; + } if ((error = git_iterator_reset(iter, ps->prefix, ps->prefix)) < 0) goto done; @@ -385,7 +390,7 @@ static int pathspec_match_from_iterator( } /* if only looking at failures, exit early or just continue */ - if (failures_only) { + if (failures_only || !out) { if (used_ct == patterns->length) break; continue; @@ -429,7 +434,7 @@ done: if (error < 0) { pathspec_match_free(m); - *out = NULL; + if (out) *out = NULL; } return error; @@ -456,7 +461,7 @@ int git_pathspec_match_workdir( int error = 0; git_iterator *iter; - assert(out && repo); + assert(repo); if (!(error = git_iterator_for_workdir( &iter, repo, pathspec_match_iter_flags(flags), NULL, NULL))) { @@ -478,7 +483,7 @@ int git_pathspec_match_index( int error = 0; git_iterator *iter; - assert(out && index); + assert(index); if (!(error = git_iterator_for_index( &iter, index, pathspec_match_iter_flags(flags), NULL, NULL))) { @@ -500,7 +505,7 @@ int git_pathspec_match_tree( int error = 0; git_iterator *iter; - assert(out && tree); + assert(tree); if (!(error = git_iterator_for_tree( &iter, tree, pathspec_match_iter_flags(flags), NULL, NULL))) { From bc6f0839ebd5794c99d5b488d431188a162a064d Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Fri, 5 Jul 2013 15:22:21 -0700 Subject: [PATCH 12/17] Add a bunch more features to log example --- examples/log.c | 126 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 112 insertions(+), 14 deletions(-) diff --git a/examples/log.c b/examples/log.c index 50e81efad..dbbc42914 100644 --- a/examples/log.c +++ b/examples/log.c @@ -148,16 +148,6 @@ static void print_time(const git_time *intime, const char *prefix) printf("%s%s %c%02d%02d\n", prefix, out, sign, hours, minutes); } -struct log_options { - int show_diff; - int skip; - int min_parents, max_parents; - git_time_t before; - git_time_t after; - char *author; - char *committer; -}; - static void print_commit(git_commit *commit) { char buf[GIT_OID_HEXSZ + 1]; @@ -192,6 +182,37 @@ static void print_commit(git_commit *commit) printf("\n"); } +static int print_diff( + const git_diff_delta *delta, + const git_diff_range *range, + char usage, + const char *line, + size_t line_len, + void *data) +{ + (void)delta; (void)range; (void)usage; (void)line_len; (void)data; + fputs(line, stdout); + return 0; +} + +static int match_int(int *value, const char *arg, int allow_negative) +{ + char *found; + *value = (int)strtol(arg, &found, 10); + return (found && *found == '\0' && (allow_negative || *value >= 0)); +} + +static int match_int_arg( + int *value, const char *arg, const char *pfx, int allow_negative) +{ + size_t pfxlen = strlen(pfx); + if (strncmp(arg, pfx, pfxlen) != 0) + return 0; + if (!match_int(value, arg + pfxlen, allow_negative)) + usage("Invalid value after argument", arg); + return 1; +} + static int match_with_parent( git_commit *commit, int i, git_diff_options *opts) { @@ -216,14 +237,25 @@ static int match_with_parent( return ndeltas > 0; } +struct log_options { + int show_diff; + int skip, limit; + int min_parents, max_parents; + git_time_t before; + git_time_t after; + char *author; + char *committer; +}; + int main(int argc, char *argv[]) { - int i, count = 0, parents; + int i, count = 0, printed = 0, parents; char *a; struct log_state s; + struct log_options opt; git_diff_options diffopts = GIT_DIFF_OPTIONS_INIT; git_oid oid; - git_commit *commit; + git_commit *commit = NULL; git_pathspec *ps = NULL; git_threads_init(); @@ -231,6 +263,9 @@ int main(int argc, char *argv[]) memset(&s, 0, sizeof(s)); s.sorting = GIT_SORT_TIME; + memset(&opt, 0, sizeof(opt)); + opt.max_parents = -1; + for (i = 1; i < argc; ++i) { a = argv[i]; @@ -251,6 +286,33 @@ int main(int argc, char *argv[]) set_sorting(&s, GIT_SORT_REVERSE); else if (!strncmp(a, "--git-dir=", strlen("--git-dir="))) s.repodir = a + strlen("--git-dir="); + else if (match_int_arg(&opt.skip, a, "--skip=", 0)) + /* found valid --skip */; + else if (match_int_arg(&opt.limit, a, "--max-count=", 0)) + /* found valid --max-count */; + else if (a[1] >= '0' && a[1] <= '9') { + if (!match_int(&opt.limit, a + 1, 0)) + usage("Invalid limit on number of commits", a); + } else if (!strcmp(a, "-n")) { + if (i + 1 == argc || !match_int(&opt.limit, argv[i], 0)) + usage("Argument -n not followed by valid count", argv[i]); + else + ++i; + } + else if (!strcmp(a, "--merges")) + opt.min_parents = 2; + else if (!strcmp(a, "--no-merges")) + opt.max_parents = 1; + else if (!strcmp(a, "--no-min-parents")) + opt.min_parents = 0; + else if (!strcmp(a, "--no-max-parents")) + opt.max_parents = -1; + else if (match_int_arg(&opt.max_parents, a, "--max-parents=", 1)) + /* found valid --max-parents */; + else if (match_int_arg(&opt.min_parents, a, "--min-parents=", 0)) + /* found valid --min_parents */; + else if (!strcmp(a, "-p") || !strcmp(a, "-u") || !strcmp(a, "--patch")) + opt.show_diff = 1; else usage("Unsupported argument", a); } @@ -260,16 +322,21 @@ int main(int argc, char *argv[]) diffopts.pathspec.strings = &argv[i]; diffopts.pathspec.count = argc - i; - count = 0; if (diffopts.pathspec.count > 0) check(git_pathspec_new(&ps, &diffopts.pathspec), "Building pathspec", NULL); + printed = count = 0; + for (; !git_revwalk_next(&oid, s.walker); git_commit_free(commit)) { check(git_commit_lookup(&commit, s.repo, &oid), "Failed to look up commit", NULL); parents = (int)git_commit_parentcount(commit); + if (parents < opt.min_parents) + continue; + if (opt.max_parents > 0 && parents > opt.max_parents) + continue; if (diffopts.pathspec.count > 0) { int unmatched = parents; @@ -294,8 +361,39 @@ int main(int argc, char *argv[]) continue; } + if (count++ < opt.skip) + continue; + if (printed++ >= opt.limit) { + git_commit_free(commit); + break; + } + print_commit(commit); - ++count; + + if (opt.show_diff) { + git_tree *a = NULL, *b = NULL; + git_diff_list *diff = NULL; + + if (parents > 1) + continue; + check(git_commit_tree(&b, commit), "Get tree", NULL); + if (parents == 1) { + git_commit *parent; + check(git_commit_parent(&parent, commit, 0), "Get parent", NULL); + check(git_commit_tree(&a, parent), "Tree for parent", NULL); + git_commit_free(parent); + } + + check(git_diff_tree_to_tree( + &diff, git_commit_owner(commit), a, b, &diffopts), + "Diff commit with parent", NULL); + check(git_diff_print_patch(diff, print_diff, NULL), + "Displaying diff", NULL); + + git_diff_list_free(diff); + git_tree_free(a); + git_tree_free(b); + } } git_pathspec_free(ps); From 9abc78ae6157167682f061b4f73eea51ab7d7342 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Sun, 7 Jul 2013 21:56:11 -0700 Subject: [PATCH 13/17] Convert commit->parent_ids to git_array_t This converts the array of parent SHAs from a git_vector where each SHA has to be separately allocated to a git_array_t where all the SHAs can be kept in one block. Since the two collections have almost identical APIs, there isn't much involved in making the change. I did add an API to git_array_t so that it could be allocated at a precise initial size. --- src/array.h | 3 +++ src/commit.c | 29 +++++++---------------------- src/commit.h | 4 ++-- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/array.h b/src/array.h index 2d77c71a0..707570624 100644 --- a/src/array.h +++ b/src/array.h @@ -30,6 +30,9 @@ #define git_array_init(a) \ do { (a).size = (a).asize = 0; (a).ptr = NULL; } while (0) +#define git_array_init_to_size(a, desired) \ + do { (a).size = 0; (a).asize = desired; (a).ptr = git__calloc(desired, sizeof(*(a).ptr)); } while (0) + #define git_array_clear(a) \ do { git__free((a).ptr); git_array_init(a); } while (0) diff --git a/src/commit.c b/src/commit.c index cf50c2d37..cc912a7be 100644 --- a/src/commit.c +++ b/src/commit.c @@ -19,24 +19,11 @@ #include -static void clear_parents(git_commit *commit) -{ - size_t i; - - for (i = 0; i < commit->parent_ids.length; ++i) { - git_oid *parent = git_vector_get(&commit->parent_ids, i); - git__free(parent); - } - - git_vector_clear(&commit->parent_ids); -} - void git_commit__free(void *_commit) { git_commit *commit = _commit; - clear_parents(commit); - git_vector_free(&commit->parent_ids); + git_array_clear(commit->parent_ids); git_signature_free(commit->author); git_signature_free(commit->committer); @@ -44,6 +31,7 @@ void git_commit__free(void *_commit) git__free(commit->raw_header); git__free(commit->message); git__free(commit->message_encoding); + git__free(commit); } @@ -198,8 +186,8 @@ int git_commit__parse(void *_commit, git_odb_object *odb_obj) if (parent_count < 1) parent_count = 1; - if (git_vector_init(&commit->parent_ids, parent_count, NULL) < 0) - return -1; + git_array_init_to_size(commit->parent_ids, parent_count); + GITERR_CHECK_ARRAY(commit->parent_ids); if (git_oid__parse(&commit->tree_id, &buffer, buffer_end, "tree ") < 0) goto bad_buffer; @@ -209,13 +197,10 @@ int git_commit__parse(void *_commit, git_odb_object *odb_obj) */ while (git_oid__parse(&parent_id, &buffer, buffer_end, "parent ") == 0) { - git_oid *new_id = git__malloc(sizeof(git_oid)); + git_oid *new_id = git_array_alloc(commit->parent_ids); GITERR_CHECK_ALLOC(new_id); git_oid_cpy(new_id, &parent_id); - - if (git_vector_insert(&commit->parent_ids, new_id) < 0) - return -1; } commit->author = git__malloc(sizeof(git_signature)); @@ -284,7 +269,7 @@ GIT_COMMIT_GETTER(const char *, message_encoding, commit->message_encoding) GIT_COMMIT_GETTER(const char *, raw_header, commit->raw_header) GIT_COMMIT_GETTER(git_time_t, time, commit->committer->when.time) GIT_COMMIT_GETTER(int, time_offset, commit->committer->when.offset) -GIT_COMMIT_GETTER(unsigned int, parentcount, (unsigned int)commit->parent_ids.length) +GIT_COMMIT_GETTER(unsigned int, parentcount, (unsigned int)git_array_size(commit->parent_ids)) GIT_COMMIT_GETTER(const git_oid *, tree_id, &commit->tree_id); int git_commit_tree(git_tree **tree_out, const git_commit *commit) @@ -298,7 +283,7 @@ const git_oid *git_commit_parent_id( { assert(commit); - return git_vector_get(&commit->parent_ids, n); + return git_array_get(commit->parent_ids, n); } int git_commit_parent( diff --git a/src/commit.h b/src/commit.h index 70d8fc690..22fc898a1 100644 --- a/src/commit.h +++ b/src/commit.h @@ -10,14 +10,14 @@ #include "git2/commit.h" #include "tree.h" #include "repository.h" -#include "vector.h" +#include "array.h" #include struct git_commit { git_object object; - git_vector parent_ids; + git_array_t(git_oid) parent_ids; git_oid tree_id; git_signature *author; From 3e96ecf219bd9b84c3a7faec61e818766f60e0d9 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Mon, 8 Jul 2013 09:53:24 -0700 Subject: [PATCH 14/17] Improve include/git2/pathspec.h docs --- include/git2/pathspec.h | 75 +++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/include/git2/pathspec.h b/include/git2/pathspec.h index 8122d9927..6d97bb326 100644 --- a/include/git2/pathspec.h +++ b/include/git2/pathspec.h @@ -25,20 +25,22 @@ typedef struct git_pathspec_match_list git_pathspec_match_list; * Options controlling how pathspec match should be executed * * - GIT_PATHSPEC_IGNORE_CASE forces match to ignore case; otherwise - * match will use native case sensitivity of platform + * match will use native case sensitivity of platform filesystem * - GIT_PATHSPEC_USE_CASE forces case sensitive match; otherwise - * match will use native case sensitivity of platform + * match will use native case sensitivity of platform filesystem * - GIT_PATHSPEC_NO_GLOB disables glob patterns and just uses simple * string comparison for matching - * - GIT_PATHSPEC_NO_MATCH_ERROR means the match function will return - * GIT_ENOTFOUND if no matches are found; otherwise it will return 0 - * for success and `git_pathspec_match_list_entrycount` will be 0. - * - GIT_PATHSPEC_FIND_FAILURES only applies to a git_pathspec_match_list; - * it means to check file names against all unmatched patterns so that - * at the end of a match we can identify patterns that did not match any - * files. - * - GIT_PATHSPEC_FAILURES_ONLY only applies to a git_pathspec_match_list; - * it means to only check for mismatches and not record matched paths. + * - GIT_PATHSPEC_NO_MATCH_ERROR means the match functions return error + * code GIT_ENOTFOUND if no matches are found; otherwise no matches is + * still success (return 0) but `git_pathspec_match_list_entrycount` + * will indicate 0 matches. + * - GIT_PATHSPEC_FIND_FAILURES means that the `git_pathspec_match_list` + * should track which patterns matched which files so that at the end of + * the match we can identify patterns that did not match any files. + * - GIT_PATHSPEC_FAILURES_ONLY means that the `git_pathspec_match_list` + * does not need to keep the actual matching filenames. Use this to + * just test if there were any matches at all or in combination with + * GIT_PATHSPEC_FIND_FAILURES to validate a pathspec. */ typedef enum { GIT_PATHSPEC_DEFAULT = 0, @@ -54,7 +56,6 @@ typedef enum { * Compile a pathspec * * @param out Output of the compiled pathspec - * @param flags Combination of git_pathspec_flag_t values * @param pathspec A git_strarray of the paths to match * @return 0 on success, <0 on failure */ @@ -77,7 +78,7 @@ GIT_EXTERN(void) git_pathspec_free(git_pathspec *ps); * fall back on being case sensitive. * * @param ps The compiled pathspec - * @param flags Match flags to influence matching behavior + * @param flags Combination of git_pathspec_flag_t options to control match * @param path The pathname to attempt to match * @return 1 is path matches spec, 0 if it does not */ @@ -87,18 +88,24 @@ GIT_EXTERN(int) git_pathspec_matches_path( /** * Match a pathspec against the working directory of a repository. * - * This returns a `git_patchspec_match` object that contains the list of - * all files matching the given pathspec in the working directory of the - * repository. This handles git ignores (i.e. ignored files will not be + * This matches the pathspec against the current files in the working + * directory of the repository. It is an error to invoke this on a bare + * repo. This handles git ignores (i.e. ignored files will not be * considered to match the `pathspec` unless the file is tracked in the * index). * - * @param out Object with list of matching items + * If `out` is not NULL, this returns a `git_patchspec_match_list`. That + * contains the list of all matched filenames (unless you pass the + * `GIT_PATHSPEC_FAILURES_ONLY` flag) and may also contain the list of + * pathspecs with no match (if you used the `GIT_PATHSPEC_FIND_FAILURES` + * flag). You must call `git_pathspec_match_list_free()` on this object. + * + * @param out Output list of matches; pass NULL to just get return value * @param repo The repository in which to match; bare repo is an error - * @param flags Options to control matching behavior + * @param flags Combination of git_pathspec_flag_t options to control match * @param ps Pathspec to be matched * @return 0 on success, -1 on error, GIT_ENOTFOUND if no matches and - * the GIT_PATHSPEC_NO_MATCH_ERROR flag is used + * the GIT_PATHSPEC_NO_MATCH_ERROR flag was given */ GIT_EXTERN(int) git_pathspec_match_workdir( git_pathspec_match_list **out, @@ -109,17 +116,22 @@ GIT_EXTERN(int) git_pathspec_match_workdir( /** * Match a pathspec against entries in an index. * - * This returns a `git_patchspec_match` object that contains the list of - * all files matching the given pathspec in the index. + * This matches the pathspec against the files in the repository index. * * NOTE: At the moment, the case sensitivity of this match is controlled * by the current case-sensitivity of the index object itself and the * USE_CASE and IGNORE_CASE flags will have no effect. This behavior will * be corrected in a future release. * - * @param out Object with list of matching items - * @param inex The index in which to match - * @param flags Options to control matching behavior + * If `out` is not NULL, this returns a `git_patchspec_match_list`. That + * contains the list of all matched filenames (unless you pass the + * `GIT_PATHSPEC_FAILURES_ONLY` flag) and may also contain the list of + * pathspecs with no match (if you used the `GIT_PATHSPEC_FIND_FAILURES` + * flag). You must call `git_pathspec_match_list_free()` on this object. + * + * @param out Output list of matches; pass NULL to just get return value + * @param index The index to match against + * @param flags Combination of git_pathspec_flag_t options to control match * @param ps Pathspec to be matched * @return 0 on success, -1 on error, GIT_ENOTFOUND if no matches and * the GIT_PATHSPEC_NO_MATCH_ERROR flag is used @@ -133,12 +145,17 @@ GIT_EXTERN(int) git_pathspec_match_index( /** * Match a pathspec against files in a tree. * - * This returns a `git_patchspec_match` object that contains the list of - * all files matching the given pathspec in the given tree. + * This matches the pathspec against the files in the given tree. * - * @param out Object with list of matching items - * @param inex The index in which to match - * @param flags Options to control matching behavior + * If `out` is not NULL, this returns a `git_patchspec_match_list`. That + * contains the list of all matched filenames (unless you pass the + * `GIT_PATHSPEC_FAILURES_ONLY` flag) and may also contain the list of + * pathspecs with no match (if you used the `GIT_PATHSPEC_FIND_FAILURES` + * flag). You must call `git_pathspec_match_list_free()` on this object. + * + * @param out Output list of matches; pass NULL to just get return value + * @param tree The root-level tree to match against + * @param flags Combination of git_pathspec_flag_t options to control match * @param ps Pathspec to be matched * @return 0 on success, -1 on error, GIT_ENOTFOUND if no matches and * the GIT_PATHSPEC_NO_MATCH_ERROR flag is used From 6fc5a58197bf04e1b5c6ca1bdb5765e90d3eb106 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Mon, 8 Jul 2013 22:42:02 -0700 Subject: [PATCH 15/17] Basic bit vector This is a simple bit vector object that is not resizable after the initial allocation but can be of arbitrary size. It will keep the bti vector entirely on the stack for vectors 64 bits or less, and will allocate the vector on the heap for larger sizes. The API is uniform regardless of storage location. This is very basic right now and all the APIs are inline functions, but it is useful for storing an array of boolean values. --- src/bitvec.h | 92 ++++++++++++++++++++++++++++++++++++++++ tests-clar/core/bitvec.c | 64 ++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 src/bitvec.h create mode 100644 tests-clar/core/bitvec.c diff --git a/src/bitvec.h b/src/bitvec.h new file mode 100644 index 000000000..a033f534f --- /dev/null +++ b/src/bitvec.h @@ -0,0 +1,92 @@ +/* + * 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. + */ +#ifndef INCLUDE_bitvec_h__ +#define INCLUDE_bitvec_h__ + +#include "util.h" + +/* + * This is a silly little fixed length bit vector type that will store + * vectors of 64 bits or less directly in the structure and allocate + * memory for vectors longer than 64 bits. You can use the two versions + * transparently through the API and avoid heap allocation completely when + * using a short bit vector as a result. + */ +typedef struct { + size_t length; + union { + uint8_t *ptr; + uint64_t bits; + } u; +} git_bitvec; + +GIT_INLINE(int) git_bitvec_init(git_bitvec *bv, size_t capacity) +{ + if (capacity < 64) { + bv->length = 0; + bv->u.bits = 0; + return 0; + } + + bv->length = (capacity + 7) / 8; + bv->u.ptr = git__calloc(bv->length, 1); + return bv->u.ptr ? 0 : -1; +} + +#define GIT_BITVEC_MASK_INLINE(BIT) (((uint64_t)1) << BIT) + +#define GIT_BITVEC_MASK_BYTE(BIT) (((uint8_t)1) << ((BIT) & 0x07)) +#define GIT_BITVEC_INDEX_BYTE(BIT) ((BIT) >> 3) + +GIT_INLINE(void) git_bitvec_set(git_bitvec *bv, size_t bit, bool on) +{ + if (!bv->length) { + assert(bit < 64); + + if (on) + bv->u.bits |= GIT_BITVEC_MASK_INLINE(bit); + else + bv->u.bits &= ~GIT_BITVEC_MASK_INLINE(bit); + } else { + assert(bit < bv->length * 8); + + if (on) + bv->u.ptr[GIT_BITVEC_INDEX_BYTE(bit)] |= GIT_BITVEC_MASK_BYTE(bit); + else + bv->u.ptr[GIT_BITVEC_INDEX_BYTE(bit)] &= ~GIT_BITVEC_MASK_BYTE(bit); + } +} + +GIT_INLINE(bool) git_bitvec_get(git_bitvec *bv, size_t bit) +{ + if (!bv->length) { + assert(bit < 64); + return (bv->u.bits & GIT_BITVEC_MASK_INLINE(bit)) != 0; + } else { + assert(bit < bv->length * 8); + return (bv->u.ptr[GIT_BITVEC_INDEX_BYTE(bit)] & + GIT_BITVEC_MASK_BYTE(bit)) != 0; + } +} + +GIT_INLINE(void) git_bitvec_clear(git_bitvec *bv) +{ + if (!bv->length) + bv->u.bits = 0; + else + memset(bv->u.ptr, 0, bv->length); +} + +GIT_INLINE(void) git_bitvec_free(git_bitvec *bv) +{ + if (bv->length) { + git__free(bv->u.ptr); + memset(bv, 0, sizeof(*bv)); + } +} + +#endif diff --git a/tests-clar/core/bitvec.c b/tests-clar/core/bitvec.c new file mode 100644 index 000000000..48d7b99f0 --- /dev/null +++ b/tests-clar/core/bitvec.c @@ -0,0 +1,64 @@ +#include "clar_libgit2.h" +#include "bitvec.h" + +#if 0 +static void print_bitvec(git_bitvec *bv) +{ + int b; + + if (!bv->length) { + for (b = 63; b >= 0; --b) + fprintf(stderr, "%d", (bv->u.bits & (1ul << b)) ? 1 : 0); + } else { + for (b = bv->length * 8; b >= 0; --b) + fprintf(stderr, "%d", (bv->u.ptr[b >> 3] & (b & 0x0ff)) ? 1 : 0); + } + fprintf(stderr, "\n"); +} +#endif + +static void set_some_bits(git_bitvec *bv, size_t length) +{ + size_t i; + + for (i = 0; i < length; ++i) { + if (i % 3 == 0 || i % 7 == 0) + git_bitvec_set(bv, i, true); + } +} + +static void check_some_bits(git_bitvec *bv, size_t length) +{ + size_t i; + + for (i = 0; i < length; ++i) + cl_assert_equal_b(i % 3 == 0 || i % 7 == 0, git_bitvec_get(bv, i)); +} + +void test_core_bitvec__0(void) +{ + git_bitvec bv; + + cl_git_pass(git_bitvec_init(&bv, 32)); + set_some_bits(&bv, 16); + check_some_bits(&bv, 16); + git_bitvec_clear(&bv); + set_some_bits(&bv, 32); + check_some_bits(&bv, 32); + git_bitvec_clear(&bv); + set_some_bits(&bv, 64); + check_some_bits(&bv, 64); + git_bitvec_free(&bv); + + cl_git_pass(git_bitvec_init(&bv, 128)); + set_some_bits(&bv, 32); + check_some_bits(&bv, 32); + set_some_bits(&bv, 128); + check_some_bits(&bv, 128); + git_bitvec_free(&bv); + + cl_git_pass(git_bitvec_init(&bv, 4000)); + set_some_bits(&bv, 4000); + check_some_bits(&bv, 4000); + git_bitvec_free(&bv); +} From 2b672d5b646edf94ae315a9f968611ff65508c90 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Mon, 8 Jul 2013 22:46:36 -0700 Subject: [PATCH 16/17] Add git_pathspec_match_diff API This adds an additional pathspec API that will match a pathspec against a diff object. This is convenient if you want to handle renames (so you need the whole diff and can't use the pathspec constraint built into the diff API) but still want to tell if the diff had any files that matched the pathspec. When the pathspec is matched against a diff, instead of keeping a list of filenames that matched, instead the API keeps the list of git_diff_deltas that matched and they can be retrieved via a new API git_pathspec_match_list_diff_entry. There are a couple of other minor API extensions here that were mostly for the sake of convenience and to reduce dependencies on knowing the internal data structure between files inside the library. --- include/git2/diff.h | 8 + include/git2/pathspec.h | 41 +++++ src/array.h | 2 + src/diff.c | 10 ++ src/diff.h | 2 + src/pathspec.c | 314 ++++++++++++++++++++++++++++--------- src/pathspec.h | 12 +- tests-clar/diff/pathspec.c | 92 +++++++++++ 8 files changed, 404 insertions(+), 77 deletions(-) create mode 100644 tests-clar/diff/pathspec.c diff --git a/include/git2/diff.h b/include/git2/diff.h index 43029c49c..121c9df5c 100644 --- a/include/git2/diff.h +++ b/include/git2/diff.h @@ -797,6 +797,14 @@ GIT_EXTERN(size_t) git_diff_num_deltas_of_type( git_diff_list *diff, git_delta_t type); +/** + * Check if deltas are sorted case sensitively or insensitively. + * + * @param diff Diff list to check + * @return 0 if case sensitive, 1 if case is ignored + */ +GIT_EXTERN(int) git_diff_is_sorted_icase(const git_diff_list *diff); + /** * Return the diff delta and patch for an entry in the diff list. * diff --git a/include/git2/pathspec.h b/include/git2/pathspec.h index 6d97bb326..a835f8e52 100644 --- a/include/git2/pathspec.h +++ b/include/git2/pathspec.h @@ -10,6 +10,7 @@ #include "common.h" #include "types.h" #include "strarray.h" +#include "diff.h" /** * Compiled pathspec @@ -166,6 +167,30 @@ GIT_EXTERN(int) git_pathspec_match_tree( uint32_t flags, git_pathspec *ps); +/** + * Match a pathspec against files in a diff list. + * + * This matches the pathspec against the files in the given diff list. + * + * If `out` is not NULL, this returns a `git_patchspec_match_list`. That + * contains the list of all matched filenames (unless you pass the + * `GIT_PATHSPEC_FAILURES_ONLY` flag) and may also contain the list of + * pathspecs with no match (if you used the `GIT_PATHSPEC_FIND_FAILURES` + * flag). You must call `git_pathspec_match_list_free()` on this object. + * + * @param out Output list of matches; pass NULL to just get return value + * @param diff A generated diff list + * @param flags Combination of git_pathspec_flag_t options to control match + * @param ps Pathspec to be matched + * @return 0 on success, -1 on error, GIT_ENOTFOUND if no matches and + * the GIT_PATHSPEC_NO_MATCH_ERROR flag is used + */ +GIT_EXTERN(int) git_pathspec_match_diff( + git_pathspec_match_list **out, + git_diff_list *diff, + uint32_t flags, + git_pathspec *ps); + /** * Free memory associates with a git_pathspec_match_list * @@ -185,6 +210,9 @@ GIT_EXTERN(size_t) git_pathspec_match_list_entrycount( /** * Get a matching filename by position. * + * This routine cannot be used if the match list was generated by + * `git_pathspec_match_diff`. If so, it will always return NULL. + * * @param m The git_pathspec_match_list object * @param pos The index into the list * @return The filename of the match @@ -192,6 +220,19 @@ GIT_EXTERN(size_t) git_pathspec_match_list_entrycount( GIT_EXTERN(const char *) git_pathspec_match_list_entry( const git_pathspec_match_list *m, size_t pos); +/** + * Get a matching diff delta by position. + * + * This routine can only be used if the match list was generated by + * `git_pathspec_match_diff`. Otherwise it will always return NULL. + * + * @param m The git_pathspec_match_list object + * @param pos The index into the list + * @return The filename of the match + */ +GIT_EXTERN(const git_diff_delta *) git_pathspec_match_list_diff_entry( + const git_pathspec_match_list *m, size_t pos); + /** * Get the number of pathspec items that did not match. * diff --git a/src/array.h b/src/array.h index 707570624..248010425 100644 --- a/src/array.h +++ b/src/array.h @@ -66,4 +66,6 @@ GIT_INLINE(void *) git_array_grow(git_array_generic_t *a, size_t item_size) #define git_array_size(a) (a).size +#define git_array_valid_index(a, i) ((i) < (a).size) + #endif diff --git a/src/diff.c b/src/diff.c index 56232ebf4..cc7be451f 100644 --- a/src/diff.c +++ b/src/diff.c @@ -247,6 +247,11 @@ GIT_INLINE(const char *) diff_delta__path(const git_diff_delta *delta) return str; } +const char *git_diff_delta__path(const git_diff_delta *delta) +{ + return diff_delta__path(delta); +} + int git_diff_delta__cmp(const void *a, const void *b) { const git_diff_delta *da = a, *db = b; @@ -1235,6 +1240,11 @@ size_t git_diff_num_deltas_of_type(git_diff_list *diff, git_delta_t type) return count; } +int git_diff_is_sorted_icase(const git_diff_list *diff) +{ + return (diff->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0; +} + int git_diff__paired_foreach( git_diff_list *head2idx, git_diff_list *idx2wd, diff --git a/src/diff.h b/src/diff.h index 6ef03ee7c..d09a130bc 100644 --- a/src/diff.h +++ b/src/diff.h @@ -76,6 +76,8 @@ extern void git_diff_list_addref(git_diff_list *diff); extern int git_diff_delta__cmp(const void *a, const void *b); extern int git_diff_delta__casecmp(const void *a, const void *b); +extern const char *git_diff_delta__path(const git_diff_delta *delta); + extern bool git_diff_delta__should_skip( const git_diff_options *opts, const git_diff_delta *delta); diff --git a/src/pathspec.c b/src/pathspec.c index 021f38f1c..625726e0b 100644 --- a/src/pathspec.c +++ b/src/pathspec.c @@ -6,12 +6,15 @@ */ #include "git2/pathspec.h" +#include "git2/diff.h" #include "pathspec.h" #include "buf_text.h" #include "attr_file.h" #include "iterator.h" #include "repository.h" #include "index.h" +#include "bitvec.h" +#include "diff.h" /* what is the common non-wildcard prefix for all items in the pathspec */ char *git_pathspec_prefix(const git_strarray *pathspec) @@ -162,6 +165,28 @@ static int pathspec_match_one( return -1; } +static int git_pathspec__match_at( + size_t *matched_at, + const git_vector *vspec, + struct pathspec_match_context *ctxt, + const char *path0, + const char *path1) +{ + int result = GIT_ENOTFOUND; + size_t i = 0; + const git_attr_fnmatch *match; + + git_vector_foreach(vspec, i, match) { + if (path0 && (result = pathspec_match_one(match, ctxt, path0)) >= 0) + break; + if (path1 && (result = pathspec_match_one(match, ctxt, path1)) >= 0) + break; + } + + *matched_at = i; + return result; +} + /* match a path against the vectorized pathspec */ bool git_pathspec__match( const git_vector *vspec, @@ -171,8 +196,8 @@ bool git_pathspec__match( const char **matched_pathspec, size_t *matched_at) { - size_t i; - const git_attr_fnmatch *match; + int result; + size_t pos; struct pathspec_match_context ctxt; if (matched_pathspec) @@ -185,20 +210,18 @@ bool git_pathspec__match( pathspec_match_context_init(&ctxt, disable_fnmatch, casefold); - git_vector_foreach(vspec, i, match) { - int result = pathspec_match_one(match, &ctxt, path); - - if (result >= 0) { - if (matched_pathspec) - *matched_pathspec = match->pattern; - if (matched_at) - *matched_at = i; - - return (result != 0); + result = git_pathspec__match_at(&pos, vspec, &ctxt, path, NULL); + if (result >= 0) { + if (matched_pathspec) { + const git_attr_fnmatch *match = git_vector_get(vspec, pos); + *matched_pathspec = match->pattern; } + + if (matched_at) + *matched_at = pos; } - return false; + return (result > 0); } @@ -277,7 +300,8 @@ static void pathspec_match_free(git_pathspec_match_list *m) git__free(m); } -static git_pathspec_match_list *pathspec_match_alloc(git_pathspec *ps) +static git_pathspec_match_list *pathspec_match_alloc( + git_pathspec *ps, int datatype) { git_pathspec_match_list *m = git__calloc(1, sizeof(git_pathspec_match_list)); @@ -292,16 +316,73 @@ static git_pathspec_match_list *pathspec_match_alloc(git_pathspec *ps) */ GIT_REFCOUNT_INC(ps); m->pathspec = ps; + m->datatype = datatype; return m; } -GIT_INLINE(void) pathspec_mark_pattern(uint8_t *used, size_t pos, size_t *ct) +GIT_INLINE(size_t) pathspec_mark_pattern(git_bitvec *used, size_t pos) { - if (!used[pos]) { - used[pos] = 1; - (*ct)++; + if (!git_bitvec_get(used, pos)) { + git_bitvec_set(used, pos, true); + return 1; } + + return 0; +} + +static size_t pathspec_mark_remaining( + git_bitvec *used, + git_vector *patterns, + struct pathspec_match_context *ctxt, + size_t start, + const char *path0, + const char *path1) +{ + size_t count = 0; + + if (path1 == path0) + path1 = NULL; + + for (; start < patterns->length; ++start) { + const git_attr_fnmatch *pat = git_vector_get(patterns, start); + + if (git_bitvec_get(used, start)) + continue; + + if (path0 && pathspec_match_one(pat, ctxt, path0) > 0) + count += pathspec_mark_pattern(used, start); + else if (path1 && pathspec_match_one(pat, ctxt, path1) > 0) + count += pathspec_mark_pattern(used, start); + } + + return count; +} + +static int pathspec_build_failure_array( + git_pathspec_string_array_t *failures, + git_vector *patterns, + git_bitvec *used, + git_pool *pool) +{ + size_t pos; + char **failed; + const git_attr_fnmatch *pat; + + for (pos = 0; pos < patterns->length; ++pos) { + if (git_bitvec_get(used, pos)) + continue; + + if ((failed = git_array_alloc(*failures)) == NULL) + return -1; + + pat = git_vector_get(patterns, pos); + + if ((*failed = git_pool_strdup(pool, pat->pattern)) == NULL) + return -1; + } + + return 0; } static int pathspec_match_from_iterator( @@ -315,47 +396,37 @@ static int pathspec_match_from_iterator( const git_index_entry *entry = NULL; struct pathspec_match_context ctxt; git_vector *patterns = &ps->pathspec; - bool find_failures = (flags & GIT_PATHSPEC_FIND_FAILURES) != 0; - bool failures_only = (flags & GIT_PATHSPEC_FAILURES_ONLY) != 0; + bool find_failures = out && (flags & GIT_PATHSPEC_FIND_FAILURES) != 0; + bool failures_only = !out || (flags & GIT_PATHSPEC_FAILURES_ONLY) != 0; size_t pos, used_ct = 0, found_files = 0; git_index *index = NULL; - uint8_t *used_patterns = NULL; + git_bitvec used_patterns; char **file; + if (git_bitvec_init(&used_patterns, patterns->length) < 0) + return -1; + if (out) { - *out = m = pathspec_match_alloc(ps); + *out = m = pathspec_match_alloc(ps, PATHSPEC_DATATYPE_STRINGS); GITERR_CHECK_ALLOC(m); - } else { - failures_only = true; - find_failures = false; } if ((error = git_iterator_reset(iter, ps->prefix, ps->prefix)) < 0) goto done; - if (patterns->length > 0) { - used_patterns = git__calloc(patterns->length, sizeof(uint8_t)); - GITERR_CHECK_ALLOC(used_patterns); - } - if (git_iterator_type(iter) == GIT_ITERATOR_TYPE_WORKDIR && (error = git_repository_index__weakptr( &index, git_iterator_owner(iter))) < 0) goto done; - pathspec_match_context_init(&ctxt, - (flags & GIT_PATHSPEC_NO_GLOB) != 0, git_iterator_ignore_case(iter)); + pathspec_match_context_init( + &ctxt, (flags & GIT_PATHSPEC_NO_GLOB) != 0, + git_iterator_ignore_case(iter)); while (!(error = git_iterator_advance(&entry, iter))) { - int result = -1; - - for (pos = 0; pos < patterns->length; ++pos) { - const git_attr_fnmatch *pat = git_vector_get(patterns, pos); - - result = pathspec_match_one(pat, &ctxt, entry->path); - if (result >= 0) - break; - } + /* search for match with entry->path */ + int result = git_pathspec__match_at( + &pos, patterns, &ctxt, entry->path, NULL); /* no matches for this path */ if (result < 0) @@ -363,31 +434,24 @@ static int pathspec_match_from_iterator( /* if result was a negative pattern match, then don't list file */ if (!result) { - pathspec_mark_pattern(used_patterns, pos, &used_ct); + used_ct += pathspec_mark_pattern(&used_patterns, pos); continue; } - /* check if path is untracked and ignored */ + /* check if path is ignored and untracked */ if (index != NULL && git_iterator_current_is_ignored(iter) && git_index__find(NULL, index, entry->path, GIT_INDEX_STAGE_ANY) < 0) continue; /* mark the matched pattern as used */ - pathspec_mark_pattern(used_patterns, pos, &used_ct); + used_ct += pathspec_mark_pattern(&used_patterns, pos); ++found_files; /* if find_failures is on, check if any later patterns also match */ - if (find_failures && used_ct < patterns->length) { - for (++pos; pos < patterns->length; ++pos) { - const git_attr_fnmatch *pat = git_vector_get(patterns, pos); - if (used_patterns[pos]) - continue; - - if (pathspec_match_one(pat, &ctxt, entry->path) > 0) - pathspec_mark_pattern(used_patterns, pos, &used_ct); - } - } + if (find_failures && used_ct < patterns->length) + used_ct += pathspec_mark_remaining( + &used_patterns, patterns, &ctxt, pos + 1, entry->path, NULL); /* if only looking at failures, exit early or just continue */ if (failures_only || !out) { @@ -397,7 +461,7 @@ static int pathspec_match_from_iterator( } /* insert matched path into matches array */ - if ((file = git_array_alloc(m->matches)) == NULL || + if ((file = (char **)git_array_alloc(m->matches)) == NULL || (*file = git_pool_strdup(&m->pool, entry->path)) == NULL) { error = -1; goto done; @@ -409,19 +473,10 @@ static int pathspec_match_from_iterator( error = 0; /* insert patterns that had no matches into failures array */ - if (find_failures && used_ct < patterns->length) { - for (pos = 0; pos < patterns->length; ++pos) { - const git_attr_fnmatch *pat = git_vector_get(patterns, pos); - if (used_patterns[pos]) - continue; - - if ((file = git_array_alloc(m->failures)) == NULL || - (*file = git_pool_strdup(&m->pool, pat->pattern)) == NULL) { - error = -1; - goto done; - } - } - } + if (find_failures && used_ct < patterns->length && + (error = pathspec_build_failure_array( + &m->failures, patterns, &used_patterns, &m->pool)) < 0) + goto done; /* if every pattern failed to match, then we have failed */ if ((flags & GIT_PATHSPEC_NO_MATCH_ERROR) != 0 && !found_files) { @@ -430,7 +485,7 @@ static int pathspec_match_from_iterator( } done: - git__free(used_patterns); + git_bitvec_free(&used_patterns); if (error < 0) { pathspec_match_free(m); @@ -518,33 +573,142 @@ int git_pathspec_match_tree( return error; } +int git_pathspec_match_diff( + git_pathspec_match_list **out, + git_diff_list *diff, + uint32_t flags, + git_pathspec *ps) +{ + int error = 0; + git_pathspec_match_list *m = NULL; + struct pathspec_match_context ctxt; + git_vector *patterns = &ps->pathspec; + bool find_failures = out && (flags & GIT_PATHSPEC_FIND_FAILURES) != 0; + bool failures_only = !out || (flags & GIT_PATHSPEC_FAILURES_ONLY) != 0; + size_t i, pos, used_ct = 0, found_deltas = 0; + const git_diff_delta *delta, **match; + git_bitvec used_patterns; + + assert(diff); + + if (git_bitvec_init(&used_patterns, patterns->length) < 0) + return -1; + + if (out) { + *out = m = pathspec_match_alloc(ps, PATHSPEC_DATATYPE_DIFF); + GITERR_CHECK_ALLOC(m); + } + + pathspec_match_context_init( + &ctxt, (flags & GIT_PATHSPEC_NO_GLOB) != 0, + git_diff_is_sorted_icase(diff)); + + git_vector_foreach(&diff->deltas, i, delta) { + /* search for match with delta */ + int result = git_pathspec__match_at( + &pos, patterns, &ctxt, delta->old_file.path, delta->new_file.path); + + /* no matches for this path */ + if (result < 0) + continue; + + /* mark the matched pattern as used */ + used_ct += pathspec_mark_pattern(&used_patterns, pos); + + /* if result was a negative pattern match, then don't list file */ + if (!result) + continue; + + ++found_deltas; + + /* if find_failures is on, check if any later patterns also match */ + if (find_failures && used_ct < patterns->length) + used_ct += pathspec_mark_remaining( + &used_patterns, patterns, &ctxt, pos + 1, + delta->old_file.path, delta->new_file.path); + + /* if only looking at failures, exit early or just continue */ + if (failures_only || !out) { + if (used_ct == patterns->length) + break; + continue; + } + + /* insert matched delta into matches array */ + if (!(match = (const git_diff_delta **)git_array_alloc(m->matches))) { + error = -1; + goto done; + } else { + *match = delta; + } + } + + /* insert patterns that had no matches into failures array */ + if (find_failures && used_ct < patterns->length && + (error = pathspec_build_failure_array( + &m->failures, patterns, &used_patterns, &m->pool)) < 0) + goto done; + + /* if every pattern failed to match, then we have failed */ + if ((flags & GIT_PATHSPEC_NO_MATCH_ERROR) != 0 && !found_deltas) { + giterr_set(GITERR_INVALID, "No matching deltas were found"); + error = GIT_ENOTFOUND; + } + +done: + git_bitvec_free(&used_patterns); + + if (error < 0) { + pathspec_match_free(m); + if (out) *out = NULL; + } + + return error; +} + void git_pathspec_match_list_free(git_pathspec_match_list *m) { - pathspec_match_free(m); + if (m) + pathspec_match_free(m); } size_t git_pathspec_match_list_entrycount( const git_pathspec_match_list *m) { - return git_array_size(m->matches); + return m ? git_array_size(m->matches) : 0; } const char *git_pathspec_match_list_entry( const git_pathspec_match_list *m, size_t pos) { - char **entry = git_array_get(m->matches, pos); - return entry ? *entry : NULL; + if (!m || m->datatype != PATHSPEC_DATATYPE_STRINGS || + !git_array_valid_index(m->matches, pos)) + return NULL; + + return *((const char **)git_array_get(m->matches, pos)); +} + +const git_diff_delta *git_pathspec_match_list_diff_entry( + const git_pathspec_match_list *m, size_t pos) +{ + if (!m || m->datatype != PATHSPEC_DATATYPE_DIFF || + !git_array_valid_index(m->matches, pos)) + return NULL; + + return *((const git_diff_delta **)git_array_get(m->matches, pos)); } size_t git_pathspec_match_list_failed_entrycount( const git_pathspec_match_list *m) { - return git_array_size(m->failures); + return m ? git_array_size(m->failures) : 0; } const char * git_pathspec_match_list_failed_entry( const git_pathspec_match_list *m, size_t pos) { - char **entry = git_array_get(m->failures, pos); + char **entry = m ? git_array_get(m->failures, pos) : NULL; + return entry ? *entry : NULL; } + diff --git a/src/pathspec.h b/src/pathspec.h index e7edfea38..40cd21c3f 100644 --- a/src/pathspec.h +++ b/src/pathspec.h @@ -22,12 +22,20 @@ struct git_pathspec { git_pool pool; }; +enum { + PATHSPEC_DATATYPE_STRINGS = 0, + PATHSPEC_DATATYPE_DIFF = 1, +}; + +typedef git_array_t(char *) git_pathspec_string_array_t; + /* public interface to pathspec matching */ struct git_pathspec_match_list { git_pathspec *pathspec; - git_array_t(char *) matches; - git_array_t(char *) failures; + git_array_t(void *) matches; + git_pathspec_string_array_t failures; git_pool pool; + int datatype; }; /* what is the common non-wildcard prefix for all items in the pathspec */ diff --git a/tests-clar/diff/pathspec.c b/tests-clar/diff/pathspec.c new file mode 100644 index 000000000..332b513b3 --- /dev/null +++ b/tests-clar/diff/pathspec.c @@ -0,0 +1,92 @@ +#include "clar_libgit2.h" +#include "diff_helpers.h" + +static git_repository *g_repo = NULL; + +void test_diff_pathspec__initialize(void) +{ + g_repo = cl_git_sandbox_init("status"); +} + +void test_diff_pathspec__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_diff_pathspec__0(void) +{ + const char *a_commit = "26a125ee"; /* the current HEAD */ + const char *b_commit = "0017bd4a"; /* the start */ + git_tree *a = resolve_commit_oid_to_tree(g_repo, a_commit); + git_tree *b = resolve_commit_oid_to_tree(g_repo, b_commit); + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + git_diff_list *diff = NULL; + git_strarray paths = { NULL, 1 }; + char *path; + git_pathspec *ps; + git_pathspec_match_list *matches; + + cl_assert(a); + cl_assert(b); + + path = "*_file"; + paths.strings = &path; + cl_git_pass(git_pathspec_new(&ps, &paths)); + + cl_git_pass(git_pathspec_match_tree(&matches, a, GIT_PATHSPEC_DEFAULT, ps)); + cl_assert_equal_i(7, git_pathspec_match_list_entrycount(matches)); + cl_assert_equal_s("current_file", git_pathspec_match_list_entry(matches,0)); + cl_assert(git_pathspec_match_list_diff_entry(matches,0) == NULL); + git_pathspec_match_list_free(matches); + + cl_git_pass(git_diff_tree_to_tree(&diff, g_repo, NULL, a, &opts)); + + cl_git_pass(git_pathspec_match_diff( + &matches, diff, GIT_PATHSPEC_DEFAULT, ps)); + cl_assert_equal_i(7, git_pathspec_match_list_entrycount(matches)); + cl_assert(git_pathspec_match_list_diff_entry(matches, 0) != NULL); + cl_assert(git_pathspec_match_list_entry(matches, 0) == NULL); + cl_assert_equal_s("current_file", + git_pathspec_match_list_diff_entry(matches,0)->new_file.path); + cl_assert_equal_i(GIT_DELTA_ADDED, + git_pathspec_match_list_diff_entry(matches,0)->status); + git_pathspec_match_list_free(matches); + + git_diff_list_free(diff); + diff = NULL; + + cl_git_pass(git_diff_tree_to_tree(&diff, g_repo, a, b, &opts)); + + cl_git_pass(git_pathspec_match_diff( + &matches, diff, GIT_PATHSPEC_DEFAULT, ps)); + cl_assert_equal_i(3, git_pathspec_match_list_entrycount(matches)); + cl_assert(git_pathspec_match_list_diff_entry(matches, 0) != NULL); + cl_assert(git_pathspec_match_list_entry(matches, 0) == NULL); + cl_assert_equal_s("subdir/current_file", + git_pathspec_match_list_diff_entry(matches,0)->new_file.path); + cl_assert_equal_i(GIT_DELTA_DELETED, + git_pathspec_match_list_diff_entry(matches,0)->status); + git_pathspec_match_list_free(matches); + + git_diff_list_free(diff); + diff = NULL; + + cl_git_pass(git_diff_tree_to_workdir(&diff, g_repo, a, &opts)); + + cl_git_pass(git_pathspec_match_diff( + &matches, diff, GIT_PATHSPEC_DEFAULT, ps)); + cl_assert_equal_i(4, git_pathspec_match_list_entrycount(matches)); + cl_assert(git_pathspec_match_list_diff_entry(matches, 0) != NULL); + cl_assert(git_pathspec_match_list_entry(matches, 0) == NULL); + cl_assert_equal_s("modified_file", + git_pathspec_match_list_diff_entry(matches,0)->new_file.path); + cl_assert_equal_i(GIT_DELTA_MODIFIED, + git_pathspec_match_list_diff_entry(matches,0)->status); + git_pathspec_match_list_free(matches); + + git_diff_list_free(diff); + diff = NULL; + + git_tree_free(a); + git_tree_free(b); +} From 406dd556e20117b3cc2e5c53410d65314fd14056 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Wed, 10 Jul 2013 21:05:47 +0200 Subject: [PATCH 17/17] bitvec: Simplify the bit vector code --- src/bitvec.h | 61 +++++++++++++++++++--------------------------------- 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/src/bitvec.h b/src/bitvec.h index a033f534f..fd6f0ccf8 100644 --- a/src/bitvec.h +++ b/src/bitvec.h @@ -19,58 +19,43 @@ typedef struct { size_t length; union { - uint8_t *ptr; + uint64_t *words; uint64_t bits; } u; } git_bitvec; GIT_INLINE(int) git_bitvec_init(git_bitvec *bv, size_t capacity) { - if (capacity < 64) { - bv->length = 0; - bv->u.bits = 0; - return 0; + memset(bv, 0x0, sizeof(*bv)); + + if (capacity >= 64) { + bv->length = (capacity / 64) + 1; + bv->u.words = git__calloc(bv->length, sizeof(uint64_t)); + if (!bv->u.words) + return -1; } - bv->length = (capacity + 7) / 8; - bv->u.ptr = git__calloc(bv->length, 1); - return bv->u.ptr ? 0 : -1; + return 0; } -#define GIT_BITVEC_MASK_INLINE(BIT) (((uint64_t)1) << BIT) - -#define GIT_BITVEC_MASK_BYTE(BIT) (((uint8_t)1) << ((BIT) & 0x07)) -#define GIT_BITVEC_INDEX_BYTE(BIT) ((BIT) >> 3) +#define GIT_BITVEC_MASK(BIT) ((uint64_t)1 << (BIT % 64)) +#define GIT_BITVEC_WORD(BV, BIT) (BV->length ? &BV->u.words[BIT / 64] : &BV->u.bits) GIT_INLINE(void) git_bitvec_set(git_bitvec *bv, size_t bit, bool on) { - if (!bv->length) { - assert(bit < 64); + uint64_t *word = GIT_BITVEC_WORD(bv, bit); + uint64_t mask = GIT_BITVEC_MASK(bit); - if (on) - bv->u.bits |= GIT_BITVEC_MASK_INLINE(bit); - else - bv->u.bits &= ~GIT_BITVEC_MASK_INLINE(bit); - } else { - assert(bit < bv->length * 8); - - if (on) - bv->u.ptr[GIT_BITVEC_INDEX_BYTE(bit)] |= GIT_BITVEC_MASK_BYTE(bit); - else - bv->u.ptr[GIT_BITVEC_INDEX_BYTE(bit)] &= ~GIT_BITVEC_MASK_BYTE(bit); - } + if (on) + *word |= mask; + else + *word &= ~mask; } GIT_INLINE(bool) git_bitvec_get(git_bitvec *bv, size_t bit) { - if (!bv->length) { - assert(bit < 64); - return (bv->u.bits & GIT_BITVEC_MASK_INLINE(bit)) != 0; - } else { - assert(bit < bv->length * 8); - return (bv->u.ptr[GIT_BITVEC_INDEX_BYTE(bit)] & - GIT_BITVEC_MASK_BYTE(bit)) != 0; - } + uint64_t *word = GIT_BITVEC_WORD(bv, bit); + return (*word & GIT_BITVEC_MASK(bit)) != 0; } GIT_INLINE(void) git_bitvec_clear(git_bitvec *bv) @@ -78,15 +63,13 @@ GIT_INLINE(void) git_bitvec_clear(git_bitvec *bv) if (!bv->length) bv->u.bits = 0; else - memset(bv->u.ptr, 0, bv->length); + memset(bv->u.words, 0x0, bv->length * sizeof(uint64_t)); } GIT_INLINE(void) git_bitvec_free(git_bitvec *bv) { - if (bv->length) { - git__free(bv->u.ptr); - memset(bv, 0, sizeof(*bv)); - } + if (bv->length) + git__free(bv->u.words); } #endif