diff options
Diffstat (limited to 'src')
130 files changed, 14216 insertions, 5870 deletions
diff --git a/src/array.h b/src/array.h new file mode 100644 index 000000000..c25a1b29e --- /dev/null +++ b/src/array.h @@ -0,0 +1,72 @@ +/* + * 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_array_h__ +#define INCLUDE_array_h__ + +#include "util.h" + +/* + * Use this to declare a typesafe resizable array of items, a la: + * + * git_array_t(int) my_ints = GIT_ARRAY_INIT; + * ... + * int *i = git_array_alloc(my_ints); + * GITERR_CHECK_ALLOC(i); + * ... + * git_array_clear(my_ints); + * + * You may also want to do things like: + * + * typedef git_array_t(my_struct) my_struct_array_t; + */ +#define git_array_t(type) struct { type *ptr; uint32_t size, asize; } + +#define GIT_ARRAY_INIT { NULL, 0, 0 } + +#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) + +#define GITERR_CHECK_ARRAY(a) GITERR_CHECK_ALLOC((a).ptr) + + +typedef git_array_t(char) git_array_generic_t; + +/* use a generic array for growth so this can return the new item */ +GIT_INLINE(void *) git_array_grow(void *_a, size_t item_size) +{ + git_array_generic_t *a = _a; + uint32_t new_size = (a->size < 8) ? 8 : a->asize * 3 / 2; + char *new_array = git__realloc(a->ptr, new_size * item_size); + if (!new_array) { + git_array_clear(*a); + return NULL; + } else { + a->ptr = new_array; a->asize = new_size; a->size++; + return a->ptr + (a->size - 1) * item_size; + } +} + +#define git_array_alloc(a) \ + ((a).size >= (a).asize) ? \ + git_array_grow(&(a), sizeof(*(a).ptr)) : \ + (a).ptr ? &(a).ptr[(a).size++] : NULL + +#define git_array_last(a) ((a).size ? &(a).ptr[(a).size - 1] : NULL) + +#define git_array_get(a, i) (((i) < (a).size) ? &(a).ptr[(i)] : NULL) + +#define git_array_size(a) (a).size + +#define git_array_valid_index(a, i) ((i) < (a).size) + +#endif diff --git a/src/attr.c b/src/attr.c index 979fecc14..6cdff29f9 100644 --- a/src/attr.c +++ b/src/attr.c @@ -36,7 +36,7 @@ static int collect_attr_files( int git_attr_get( const char **value, - git_repository *repo, + git_repository *repo, uint32_t flags, const char *pathname, const char *name) @@ -88,10 +88,10 @@ typedef struct { int git_attr_get_many( const char **values, - git_repository *repo, + git_repository *repo, uint32_t flags, const char *pathname, - size_t num_attr, + size_t num_attr, const char **names) { int error; @@ -151,7 +151,7 @@ cleanup: int git_attr_foreach( - git_repository *repo, + git_repository *repo, uint32_t flags, const char *pathname, int (*callback)(const char *name, const char *value, void *payload), @@ -312,7 +312,7 @@ static int load_attr_blob_from_index( entry = git_index_get_byindex(index, pos); - if (old_oid && git_oid_cmp(old_oid, &entry->oid) == 0) + if (old_oid && git_oid__cmp(old_oid, &entry->oid) == 0) return GIT_ENOTFOUND; if ((error = git_blob_lookup(blob, repo, &entry->oid)) < 0) @@ -596,26 +596,33 @@ static int collect_attr_files( } static int attr_cache__lookup_path( - const char **out, git_config *cfg, const char *key, const char *fallback) + char **out, git_config *cfg, const char *key, const char *fallback) { git_buf buf = GIT_BUF_INIT; int error; + const char *cfgval = NULL; - if (!(error = git_config_get_string(out, cfg, key))) - return 0; + *out = NULL; + + if (!(error = git_config_get_string(&cfgval, cfg, key))) { - if (error == GIT_ENOTFOUND) { + /* expand leading ~/ as needed */ + if (cfgval && cfgval[0] == '~' && cfgval[1] == '/' && + !git_futils_find_global_file(&buf, &cfgval[2])) + *out = git_buf_detach(&buf); + else if (cfgval) + *out = git__strdup(cfgval); + + } else if (error == GIT_ENOTFOUND) { giterr_clear(); error = 0; if (!git_futils_find_xdg_file(&buf, fallback)) *out = git_buf_detach(&buf); - else - *out = NULL; - - git_buf_free(&buf); } + git_buf_free(&buf); + return error; } @@ -696,6 +703,12 @@ void git_attr_cache_flush( git_pool_clear(&cache->pool); + git__free(cache->cfg_attr_file); + cache->cfg_attr_file = NULL; + + git__free(cache->cfg_excl_file); + cache->cfg_excl_file = NULL; + cache->initialized = 0; } diff --git a/src/attr_file.c b/src/attr_file.c index 93f6df1d9..ca5f2137c 100644 --- a/src/attr_file.c +++ b/src/attr_file.c @@ -423,7 +423,8 @@ int git_attr_fnmatch__parse_shellglob_format( *base = scan; - spec->length = scan - pattern; + if ((spec->length = scan - pattern) == 0) + return GIT_ENOTFOUND; if (pattern[spec->length - 1] == '/') { spec->length--; @@ -523,7 +524,7 @@ int git_attr_assignment__parse( assert(assigns && !assigns->length); - assigns->_cmp = sort_by_hash_and_name; + git_vector_set_cmp(assigns, sort_by_hash_and_name); while (*scan && *scan != '\n') { const char *name_start, *value_start; diff --git a/src/attr_file.h b/src/attr_file.h index 8ca7e4eb7..afea1e115 100644 --- a/src/attr_file.h +++ b/src/attr_file.h @@ -47,14 +47,14 @@ typedef struct { typedef struct { git_refcount unused; const char *name; - uint32_t name_hash; + uint32_t name_hash; } git_attr_name; typedef struct { git_refcount rc; /* for macros */ char *name; - uint32_t name_hash; - const char *value; + uint32_t name_hash; + const char *value; } git_attr_assignment; typedef struct { diff --git a/src/attrcache.h b/src/attrcache.h index 12cec4bfb..077633b87 100644 --- a/src/attrcache.h +++ b/src/attrcache.h @@ -13,10 +13,10 @@ typedef struct { int initialized; git_pool pool; - git_strmap *files; /* hash path to git_attr_file of rules */ - git_strmap *macros; /* hash name to vector<git_attr_assignment> */ - const char *cfg_attr_file; /* cached value of core.attributesfile */ - const char *cfg_excl_file; /* cached value of core.excludesfile */ + git_strmap *files; /* hash path to git_attr_file of rules */ + git_strmap *macros; /* hash name to vector<git_attr_assignment> */ + char *cfg_attr_file; /* cached value of core.attributesfile */ + char *cfg_excl_file; /* cached value of core.excludesfile */ } git_attr_cache; extern int git_attr_cache__init(git_repository *repo); diff --git a/src/bitvec.h b/src/bitvec.h new file mode 100644 index 000000000..fd6f0ccf8 --- /dev/null +++ b/src/bitvec.h @@ -0,0 +1,75 @@ +/* + * 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 { + uint64_t *words; + uint64_t bits; + } u; +} git_bitvec; + +GIT_INLINE(int) git_bitvec_init(git_bitvec *bv, size_t capacity) +{ + 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; + } + + return 0; +} + +#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) +{ + uint64_t *word = GIT_BITVEC_WORD(bv, bit); + uint64_t mask = GIT_BITVEC_MASK(bit); + + if (on) + *word |= mask; + else + *word &= ~mask; +} + +GIT_INLINE(bool) git_bitvec_get(git_bitvec *bv, size_t bit) +{ + uint64_t *word = GIT_BITVEC_WORD(bv, bit); + return (*word & GIT_BITVEC_MASK(bit)) != 0; +} + +GIT_INLINE(void) git_bitvec_clear(git_bitvec *bv) +{ + if (!bv->length) + bv->u.bits = 0; + else + 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.words); +} + +#endif diff --git a/src/blob.c b/src/blob.c index c0514fc13..5bb51f7cf 100644 --- a/src/blob.c +++ b/src/blob.c @@ -8,8 +8,10 @@ #include "git2/common.h" #include "git2/object.h" #include "git2/repository.h" +#include "git2/odb_backend.h" #include "common.h" +#include "filebuf.h" #include "blob.h" #include "filter.h" #include "buf_text.h" @@ -17,32 +19,34 @@ const void *git_blob_rawcontent(const git_blob *blob) { assert(blob); - return blob->odb_object->raw.data; + return git_odb_object_data(blob->odb_object); } git_off_t git_blob_rawsize(const git_blob *blob) { assert(blob); - return (git_off_t)blob->odb_object->raw.len; + return (git_off_t)git_odb_object_size(blob->odb_object); } int git_blob__getbuf(git_buf *buffer, git_blob *blob) { return git_buf_set( - buffer, blob->odb_object->raw.data, blob->odb_object->raw.len); + buffer, + git_odb_object_data(blob->odb_object), + git_odb_object_size(blob->odb_object)); } -void git_blob__free(git_blob *blob) +void git_blob__free(void *blob) { - git_odb_object_free(blob->odb_object); + git_odb_object_free(((git_blob *)blob)->odb_object); git__free(blob); } -int git_blob__parse(git_blob *blob, git_odb_object *odb_obj) +int git_blob__parse(void *blob, git_odb_object *odb_obj) { assert(blob); git_cached_obj_incref((git_cached_obj *)odb_obj); - blob->odb_object = odb_obj; + ((git_blob *)blob)->odb_object = odb_obj; return 0; } @@ -101,6 +105,7 @@ static int write_file_stream( static int write_file_filtered( git_oid *oid, + git_off_t *size, git_odb *odb, const char *full_path, git_vector *filters) @@ -119,8 +124,11 @@ static int write_file_filtered( git_buf_free(&source); /* Write the file to disk if it was properly filtered */ - if (!error) + if (!error) { + *size = dest.size; + error = git_odb_write(oid, odb, dest.ptr, dest.size, GIT_OBJ_BLOB); + } git_buf_free(&dest); return error; @@ -148,21 +156,46 @@ static int write_symlink( return error; } -static int blob_create_internal(git_oid *oid, git_repository *repo, const char *content_path, const char *hint_path, bool try_load_filters) +int git_blob__create_from_paths( + git_oid *oid, + struct stat *out_st, + git_repository *repo, + const char *content_path, + const char *hint_path, + mode_t hint_mode, + bool try_load_filters) { int error; struct stat st; git_odb *odb = NULL; git_off_t size; + mode_t mode; + git_buf path = GIT_BUF_INIT; assert(hint_path || !try_load_filters); - if ((error = git_path_lstat(content_path, &st)) < 0 || (error = git_repository_odb__weakptr(&odb, repo)) < 0) - return error; + if (!content_path) { + if (git_repository__ensure_not_bare(repo, "create blob from file") < 0) + return GIT_EBAREREPO; + + if (git_buf_joinpath( + &path, git_repository_workdir(repo), hint_path) < 0) + return -1; + + content_path = path.ptr; + } + + if ((error = git_path_lstat(content_path, &st)) < 0 || + (error = git_repository_odb(&odb, repo)) < 0) + goto done; + + if (out_st) + memcpy(out_st, &st, sizeof(st)); size = st.st_size; + mode = hint_mode ? hint_mode : st.st_mode; - if (S_ISLNK(st.st_mode)) { + if (S_ISLNK(mode)) { error = write_symlink(oid, odb, content_path, (size_t)size); } else { git_vector write_filters = GIT_VECTOR_INIT; @@ -183,7 +216,8 @@ static int blob_create_internal(git_oid *oid, git_repository *repo, const char * error = write_file_stream(oid, odb, content_path, size); } else { /* We need to apply one or more filters */ - error = write_file_filtered(oid, odb, content_path, &write_filters); + error = write_file_filtered( + oid, &size, odb, content_path, &write_filters); } git_filters_free(&write_filters); @@ -203,34 +237,21 @@ static int blob_create_internal(git_oid *oid, git_repository *repo, const char * */ } +done: + git_odb_free(odb); + git_buf_free(&path); + return error; } -int git_blob_create_fromworkdir(git_oid *oid, git_repository *repo, const char *path) +int git_blob_create_fromworkdir( + git_oid *oid, git_repository *repo, const char *path) { - git_buf full_path = GIT_BUF_INIT; - const char *workdir; - int error; - - if ((error = git_repository__ensure_not_bare(repo, "create blob from file")) < 0) - return error; - - workdir = git_repository_workdir(repo); - - if (git_buf_joinpath(&full_path, workdir, path) < 0) { - git_buf_free(&full_path); - return -1; - } - - error = blob_create_internal( - oid, repo, git_buf_cstr(&full_path), - git_buf_cstr(&full_path) + strlen(workdir), true); - - git_buf_free(&full_path); - return error; + return git_blob__create_from_paths(oid, NULL, repo, NULL, path, 0, true); } -int git_blob_create_fromdisk(git_oid *oid, git_repository *repo, const char *path) +int git_blob_create_fromdisk( + git_oid *oid, git_repository *repo, const char *path) { int error; git_buf full_path = GIT_BUF_INIT; @@ -247,8 +268,8 @@ int git_blob_create_fromdisk(git_oid *oid, git_repository *repo, const char *pat if (workdir && !git__prefixcmp(hintpath, workdir)) hintpath += strlen(workdir); - error = blob_create_internal( - oid, repo, git_buf_cstr(&full_path), hintpath, true); + error = git_blob__create_from_paths( + oid, NULL, repo, git_buf_cstr(&full_path), hintpath, 0, true); git_buf_free(&full_path); return error; @@ -268,12 +289,9 @@ int git_blob_create_fromchunks( git_filebuf file = GIT_FILEBUF_INIT; git_buf path = GIT_BUF_INIT; - if (git_buf_join_n( - &path, '/', 3, - git_repository_path(repo), - GIT_OBJECTS_DIR, - "streamed") < 0) - goto cleanup; + if (git_buf_joinpath( + &path, git_repository_path(repo), GIT_OBJECTS_DIR "streamed") < 0) + goto cleanup; content = git__malloc(BUFFER_SIZE); GITERR_CHECK_ALLOC(content); @@ -299,7 +317,8 @@ int git_blob_create_fromchunks( if (git_filebuf_flush(&file) < 0) goto cleanup; - error = blob_create_internal(oid, repo, file.path_lock, hintpath, hintpath != NULL); + error = git_blob__create_from_paths( + oid, NULL, repo, file.path_lock, hintpath, 0, hintpath != NULL); cleanup: git_buf_free(&path); @@ -314,8 +333,8 @@ int git_blob_is_binary(git_blob *blob) assert(blob); - content.ptr = blob->odb_object->raw.data; - content.size = min(blob->odb_object->raw.len, 4000); + content.ptr = blob->odb_object->buffer; + content.size = min(blob->odb_object->cached.size, 4000); return git_buf_text_is_binary(&content); } diff --git a/src/blob.h b/src/blob.h index 524734b1f..4cd9f1e0c 100644 --- a/src/blob.h +++ b/src/blob.h @@ -17,8 +17,17 @@ struct git_blob { git_odb_object *odb_object; }; -void git_blob__free(git_blob *blob); -int git_blob__parse(git_blob *blob, git_odb_object *obj); +void git_blob__free(void *blob); +int git_blob__parse(void *blob, git_odb_object *obj); int git_blob__getbuf(git_buf *buffer, git_blob *blob); +extern int git_blob__create_from_paths( + git_oid *out_oid, + struct stat *out_st, + git_repository *repo, + const char *full_path, + const char *hint_path, + mode_t hint_mode, + bool apply_filters); + #endif diff --git a/src/branch.c b/src/branch.c index e7088790e..7064fa7fc 100644 --- a/src/branch.c +++ b/src/branch.c @@ -11,6 +11,7 @@ #include "config.h" #include "refspec.h" #include "refs.h" +#include "remote.h" #include "git2/branch.h" @@ -123,40 +124,48 @@ on_error: return error; } -typedef struct { - git_branch_foreach_cb branch_cb; - void *callback_payload; - unsigned int branch_type; -} branch_foreach_filter; - -static int branch_foreach_cb(const char *branch_name, void *payload) -{ - branch_foreach_filter *filter = (branch_foreach_filter *)payload; - - if (filter->branch_type & GIT_BRANCH_LOCAL && - git__prefixcmp(branch_name, GIT_REFS_HEADS_DIR) == 0) - return filter->branch_cb(branch_name + strlen(GIT_REFS_HEADS_DIR), GIT_BRANCH_LOCAL, filter->callback_payload); - - if (filter->branch_type & GIT_BRANCH_REMOTE && - git__prefixcmp(branch_name, GIT_REFS_REMOTES_DIR) == 0) - return filter->branch_cb(branch_name + strlen(GIT_REFS_REMOTES_DIR), GIT_BRANCH_REMOTE, filter->callback_payload); - - return 0; -} - int git_branch_foreach( git_repository *repo, unsigned int list_flags, - git_branch_foreach_cb branch_cb, + git_branch_foreach_cb callback, void *payload) { - branch_foreach_filter filter; + git_reference_iterator *iter; + git_reference *ref; + int error = 0; + + if (git_reference_iterator_new(&iter, repo) < 0) + return -1; + + while ((error = git_reference_next(&ref, iter)) == 0) { + if (list_flags & GIT_BRANCH_LOCAL && + git__prefixcmp(ref->name, GIT_REFS_HEADS_DIR) == 0) { + if (callback(ref->name + strlen(GIT_REFS_HEADS_DIR), + GIT_BRANCH_LOCAL, payload)) { + error = GIT_EUSER; + } + } + + if (list_flags & GIT_BRANCH_REMOTE && + git__prefixcmp(ref->name, GIT_REFS_REMOTES_DIR) == 0) { + if (callback(ref->name + strlen(GIT_REFS_REMOTES_DIR), + GIT_BRANCH_REMOTE, payload)) { + error = GIT_EUSER; + } + } + + git_reference_free(ref); - filter.branch_cb = branch_cb; - filter.branch_type = list_flags; - filter.callback_payload = payload; + /* check if the callback has cancelled iteration */ + if (error == GIT_EUSER) + break; + } + + if (error == GIT_ITEROVER) + error = 0; - return git_reference_foreach(repo, GIT_REF_LISTALL, &branch_foreach_cb, (void *)&filter); + git_reference_iterator_free(iter); + return error; } int git_branch_move( @@ -175,18 +184,21 @@ int git_branch_move( if (!git_reference_is_branch(branch)) return not_a_local_branch(git_reference_name(branch)); - if ((error = git_buf_joinpath(&new_reference_name, GIT_REFS_HEADS_DIR, new_branch_name)) < 0 || - (error = git_buf_printf(&old_config_section, "branch.%s", git_reference_name(branch) + strlen(GIT_REFS_HEADS_DIR))) < 0 || - (error = git_buf_printf(&new_config_section, "branch.%s", new_branch_name)) < 0) + error = git_buf_joinpath(&new_reference_name, GIT_REFS_HEADS_DIR, new_branch_name); + if (error < 0) goto done; + git_buf_printf(&old_config_section, + "branch.%s", git_reference_name(branch) + strlen(GIT_REFS_HEADS_DIR)); + + git_buf_printf(&new_config_section, "branch.%s", new_branch_name); + if ((error = git_config_rename_section(git_reference_owner(branch), git_buf_cstr(&old_config_section), git_buf_cstr(&new_config_section))) < 0) goto done; - - if ((error = git_reference_rename(out, branch, git_buf_cstr(&new_reference_name), force)) < 0) - goto done; + + error = git_reference_rename(out, branch, git_buf_cstr(&new_reference_name), force); done: git_buf_free(&new_reference_name); @@ -275,6 +287,8 @@ int git_branch_upstream__name( goto cleanup; if (!*remote_name || !*merge_name) { + giterr_set(GITERR_REFERENCE, + "branch '%s' does not have an upstream", canonical_branch_name); error = GIT_ENOTFOUND; goto cleanup; } @@ -283,12 +297,10 @@ int git_branch_upstream__name( if ((error = git_remote_load(&remote, repo, remote_name)) < 0) goto cleanup; - refspec = git_remote_fetchspec(remote); - if (refspec == NULL - || refspec->src == NULL - || refspec->dst == NULL) { - error = GIT_ENOTFOUND; - goto cleanup; + refspec = git_remote__matching_refspec(remote, merge_name); + if (!refspec) { + error = GIT_ENOTFOUND; + goto cleanup; } if (git_refspec_transform_r(&buf, refspec, merge_name) < 0) @@ -333,11 +345,8 @@ static int remote_name(git_buf *buf, git_repository *repo, const char *canonical if ((error = git_remote_load(&remote, repo, remote_list.strings[i])) < 0) continue; - fetchspec = git_remote_fetchspec(remote); - - /* Defensivly check that we have a fetchspec */ - if (fetchspec && - git_refspec_dst_matches(fetchspec, canonical_branch_name)) { + fetchspec = git_remote__matching_dst_refspec(remote, canonical_branch_name); + if (fetchspec) { /* If we have not already set out yet, then set * it to the matching remote name. Otherwise * multiple remotes match this reference, and it @@ -346,6 +355,9 @@ static int remote_name(git_buf *buf, git_repository *repo, const char *canonical remote_name = remote_list.strings[i]; } else { git_remote_free(remote); + + giterr_set(GITERR_REFERENCE, + "Reference '%s' is ambiguous", canonical_branch_name); error = GIT_EAMBIGUOUS; goto cleanup; } @@ -358,6 +370,8 @@ static int remote_name(git_buf *buf, git_repository *repo, const char *canonical git_buf_clear(buf); error = git_buf_puts(buf, remote_name); } else { + giterr_set(GITERR_REFERENCE, + "Could not determine remote for '%s'", canonical_branch_name); error = GIT_ENOTFOUND; } @@ -377,7 +391,7 @@ int git_branch_remote_name(char *buffer, size_t buffer_len, git_repository *repo if (buffer) git_buf_copy_cstr(buffer, buffer_len, &buf); - ret = git_buf_len(&buf) + 1; + ret = (int)git_buf_len(&buf) + 1; git_buf_free(&buf); return ret; @@ -494,8 +508,11 @@ int git_branch_set_upstream(git_reference *branch, const char *upstream_name) local = 1; else if (git_branch_lookup(&upstream, repo, upstream_name, GIT_BRANCH_REMOTE) == 0) local = 0; - else + else { + giterr_set(GITERR_REFERENCE, + "Cannot set upstream for branch '%s'", shortname); return GIT_ENOTFOUND; + } /* * If it's local, the remote is "." and the branch name is @@ -515,16 +532,17 @@ int git_branch_set_upstream(git_reference *branch, const char *upstream_name) goto on_error; if (local) { - if (git_buf_puts(&value, git_reference_name(branch)) < 0) + git_buf_clear(&value); + if (git_buf_puts(&value, git_reference_name(upstream)) < 0) goto on_error; } else { /* Get the remoe-tracking branch's refname in its repo */ if (git_remote_load(&remote, repo, git_buf_cstr(&value)) < 0) goto on_error; - fetchspec = git_remote_fetchspec(remote); + fetchspec = git_remote__matching_dst_refspec(remote, git_reference_name(upstream)); git_buf_clear(&value); - if (git_refspec_transform_l(&value, fetchspec, git_reference_name(upstream)) < 0) + if (!fetchspec || git_refspec_transform_l(&value, fetchspec, git_reference_name(upstream)) < 0) goto on_error; git_remote_free(remote); diff --git a/src/buf_text.c b/src/buf_text.c index 443454b5f..472339def 100644 --- a/src/buf_text.c +++ b/src/buf_text.c @@ -262,7 +262,7 @@ bool git_buf_text_gather_stats( while (scan < end) { unsigned char c = *scan++; - if ((c > 0x1F && c < 0x7F) || c > 0x9f) + if (c > 0x1F && c != 0x7F) stats->printable++; else switch (c) { case '\0': diff --git a/src/buffer.c b/src/buffer.c index 6e3ffe560..b5b2fd678 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -259,6 +259,15 @@ void git_buf_truncate(git_buf *buf, size_t len) } } +void git_buf_shorten(git_buf *buf, size_t amount) +{ + if (amount > buf->size) + amount = buf->size; + + buf->size = buf->size - amount; + buf->ptr[buf->size] = '\0'; +} + void git_buf_rtruncate_at_char(git_buf *buf, char separator) { ssize_t idx = git_buf_rfind_next(buf, separator); diff --git a/src/buffer.h b/src/buffer.h index 5402f3827..f3e1d506f 100644 --- a/src/buffer.h +++ b/src/buffer.h @@ -91,6 +91,7 @@ int git_buf_vprintf(git_buf *buf, const char *format, va_list ap); void git_buf_clear(git_buf *buf); void git_buf_consume(git_buf *buf, const char *end); void git_buf_truncate(git_buf *buf, size_t len); +void git_buf_shorten(git_buf *buf, size_t amount); void git_buf_rtruncate_at_char(git_buf *path, char separator); int git_buf_join_n(git_buf *buf, char separator, int nbuf, ...); diff --git a/src/cache.c b/src/cache.c index e7f333577..36ce66570 100644 --- a/src/cache.c +++ b/src/cache.c @@ -11,100 +11,270 @@ #include "thread-utils.h" #include "util.h" #include "cache.h" +#include "odb.h" +#include "object.h" #include "git2/oid.h" -int git_cache_init(git_cache *cache, size_t size, git_cached_obj_freeptr free_ptr) +GIT__USE_OIDMAP + +bool git_cache__enabled = true; +ssize_t git_cache__max_storage = (256 * 1024 * 1024); +git_atomic_ssize git_cache__current_storage = {0}; + +static size_t git_cache__max_object_size[8] = { + 0, /* GIT_OBJ__EXT1 */ + 4096, /* GIT_OBJ_COMMIT */ + 4096, /* GIT_OBJ_TREE */ + 0, /* GIT_OBJ_BLOB */ + 4096, /* GIT_OBJ_TAG */ + 0, /* GIT_OBJ__EXT2 */ + 0, /* GIT_OBJ_OFS_DELTA */ + 0 /* GIT_OBJ_REF_DELTA */ +}; + +int git_cache_set_max_object_size(git_otype type, size_t size) +{ + if (type < 0 || (size_t)type >= ARRAY_SIZE(git_cache__max_object_size)) { + giterr_set(GITERR_INVALID, "type out of range"); + return -1; + } + + git_cache__max_object_size[type] = size; + return 0; +} + +void git_cache_dump_stats(git_cache *cache) { - if (size < 8) - size = 8; - size = git__size_t_powerof2(size); + git_cached_obj *object; - cache->size_mask = size - 1; - cache->lru_count = 0; - cache->free_obj = free_ptr; + if (kh_size(cache->map) == 0) + return; - git_mutex_init(&cache->lock); + printf("Cache %p: %d items cached, %d bytes\n", + cache, kh_size(cache->map), (int)cache->used_memory); - cache->nodes = git__malloc(size * sizeof(git_cached_obj *)); - GITERR_CHECK_ALLOC(cache->nodes); + kh_foreach_value(cache->map, object, { + char oid_str[9]; + printf(" %s%c %s (%d)\n", + git_object_type2string(object->type), + object->flags == GIT_CACHE_STORE_PARSED ? '*' : ' ', + git_oid_tostr(oid_str, sizeof(oid_str), &object->oid), + (int)object->size + ); + }); +} - memset(cache->nodes, 0x0, size * sizeof(git_cached_obj *)); +int git_cache_init(git_cache *cache) +{ + memset(cache, 0, sizeof(*cache)); + cache->map = git_oidmap_alloc(); + if (git_mutex_init(&cache->lock)) { + giterr_set(GITERR_OS, "Failed to initialize cache mutex"); + return -1; + } return 0; } +/* called with lock */ +static void clear_cache(git_cache *cache) +{ + git_cached_obj *evict = NULL; + + if (kh_size(cache->map) == 0) + return; + + kh_foreach_value(cache->map, evict, { + git_cached_obj_decref(evict); + }); + + kh_clear(oid, cache->map); + git_atomic_ssize_add(&git_cache__current_storage, -cache->used_memory); + cache->used_memory = 0; +} + +void git_cache_clear(git_cache *cache) +{ + if (git_mutex_lock(&cache->lock) < 0) + return; + + clear_cache(cache); + + git_mutex_unlock(&cache->lock); +} + void git_cache_free(git_cache *cache) { - size_t i; + git_cache_clear(cache); + git_oidmap_free(cache->map); + git_mutex_free(&cache->lock); + git__memzero(cache, sizeof(*cache)); +} + +/* Called with lock */ +static void cache_evict_entries(git_cache *cache) +{ + uint32_t seed = rand(); + size_t evict_count = 8; + ssize_t evicted_memory = 0; - for (i = 0; i < (cache->size_mask + 1); ++i) { - if (cache->nodes[i] != NULL) - git_cached_obj_decref(cache->nodes[i], cache->free_obj); + /* do not infinite loop if there's not enough entries to evict */ + if (evict_count > kh_size(cache->map)) { + clear_cache(cache); + return; } - git_mutex_free(&cache->lock); - git__free(cache->nodes); + while (evict_count > 0) { + khiter_t pos = seed++ % kh_end(cache->map); + + if (kh_exist(cache->map, pos)) { + git_cached_obj *evict = kh_val(cache->map, pos); + + evict_count--; + evicted_memory += evict->size; + git_cached_obj_decref(evict); + + kh_del(oid, cache->map, pos); + } + } + + cache->used_memory -= evicted_memory; + git_atomic_ssize_add(&git_cache__current_storage, -evicted_memory); } -void *git_cache_get(git_cache *cache, const git_oid *oid) +static bool cache_should_store(git_otype object_type, size_t object_size) { - uint32_t hash; - git_cached_obj *node = NULL, *result = NULL; + size_t max_size = git_cache__max_object_size[object_type]; + return git_cache__enabled && object_size < max_size; +} - memcpy(&hash, oid->id, sizeof(hash)); +static void *cache_get(git_cache *cache, const git_oid *oid, unsigned int flags) +{ + khiter_t pos; + git_cached_obj *entry = NULL; - if (git_mutex_lock(&cache->lock)) { - giterr_set(GITERR_THREAD, "unable to lock cache mutex"); + if (!git_cache__enabled || git_mutex_lock(&cache->lock) < 0) return NULL; - } - { - node = cache->nodes[hash & cache->size_mask]; + pos = kh_get(oid, cache->map, oid); + if (pos != kh_end(cache->map)) { + entry = kh_val(cache->map, pos); - if (node != NULL && git_oid_cmp(&node->oid, oid) == 0) { - git_cached_obj_incref(node); - result = node; + if (flags && entry->flags != flags) { + entry = NULL; + } else { + git_cached_obj_incref(entry); } } + git_mutex_unlock(&cache->lock); - return result; + return entry; } -void *git_cache_try_store(git_cache *cache, void *_entry) +static void *cache_store(git_cache *cache, git_cached_obj *entry) { - git_cached_obj *entry = _entry; - uint32_t hash; + khiter_t pos; - memcpy(&hash, &entry->oid, sizeof(uint32_t)); + git_cached_obj_incref(entry); - if (git_mutex_lock(&cache->lock)) { - giterr_set(GITERR_THREAD, "unable to lock cache mutex"); - return NULL; + if (!git_cache__enabled && cache->used_memory > 0) { + git_cache_clear(cache); + return entry; } - { - git_cached_obj *node = cache->nodes[hash & cache->size_mask]; + if (!cache_should_store(entry->type, entry->size)) + return entry; - /* increase the refcount on this object, because - * the cache now owns it */ - git_cached_obj_incref(entry); + if (git_mutex_lock(&cache->lock) < 0) + return entry; - if (node == NULL) { - cache->nodes[hash & cache->size_mask] = entry; - } else if (git_oid_cmp(&node->oid, &entry->oid) == 0) { - git_cached_obj_decref(entry, cache->free_obj); - entry = node; - } else { - git_cached_obj_decref(node, cache->free_obj); - cache->nodes[hash & cache->size_mask] = entry; + /* soften the load on the cache */ + if (git_cache__current_storage.val > git_cache__max_storage) + cache_evict_entries(cache); + + pos = kh_get(oid, cache->map, &entry->oid); + + /* not found */ + if (pos == kh_end(cache->map)) { + int rval; + + pos = kh_put(oid, cache->map, &entry->oid, &rval); + if (rval >= 0) { + kh_key(cache->map, pos) = &entry->oid; + kh_val(cache->map, pos) = entry; + git_cached_obj_incref(entry); + cache->used_memory += entry->size; + git_atomic_ssize_add(&git_cache__current_storage, (ssize_t)entry->size); } + } + /* found */ + else { + git_cached_obj *stored_entry = kh_val(cache->map, pos); - /* increase the refcount again, because we are - * returning it to the user */ - git_cached_obj_incref(entry); + if (stored_entry->flags == entry->flags) { + git_cached_obj_decref(entry); + git_cached_obj_incref(stored_entry); + entry = stored_entry; + } else if (stored_entry->flags == GIT_CACHE_STORE_RAW && + entry->flags == GIT_CACHE_STORE_PARSED) { + git_cached_obj_decref(stored_entry); + git_cached_obj_incref(entry); + kh_key(cache->map, pos) = &entry->oid; + kh_val(cache->map, pos) = entry; + } else { + /* NO OP */ + } } - git_mutex_unlock(&cache->lock); + git_mutex_unlock(&cache->lock); return entry; } + +void *git_cache_store_raw(git_cache *cache, git_odb_object *entry) +{ + entry->cached.flags = GIT_CACHE_STORE_RAW; + return cache_store(cache, (git_cached_obj *)entry); +} + +void *git_cache_store_parsed(git_cache *cache, git_object *entry) +{ + entry->cached.flags = GIT_CACHE_STORE_PARSED; + return cache_store(cache, (git_cached_obj *)entry); +} + +git_odb_object *git_cache_get_raw(git_cache *cache, const git_oid *oid) +{ + return cache_get(cache, oid, GIT_CACHE_STORE_RAW); +} + +git_object *git_cache_get_parsed(git_cache *cache, const git_oid *oid) +{ + return cache_get(cache, oid, GIT_CACHE_STORE_PARSED); +} + +void *git_cache_get_any(git_cache *cache, const git_oid *oid) +{ + return cache_get(cache, oid, GIT_CACHE_STORE_ANY); +} + +void git_cached_obj_decref(void *_obj) +{ + git_cached_obj *obj = _obj; + + if (git_atomic_dec(&obj->refcount) == 0) { + switch (obj->flags) { + case GIT_CACHE_STORE_RAW: + git_odb_object__free(_obj); + break; + + case GIT_CACHE_STORE_PARSED: + git_object__free(_obj); + break; + + default: + git__free(_obj); + break; + } + } +} diff --git a/src/cache.h b/src/cache.h index 7034ec268..53fbcf4e9 100644 --- a/src/cache.h +++ b/src/cache.h @@ -12,43 +12,56 @@ #include "git2/odb.h" #include "thread-utils.h" +#include "oidmap.h" -#define GIT_DEFAULT_CACHE_SIZE 128 - -typedef void (*git_cached_obj_freeptr)(void *); +enum { + GIT_CACHE_STORE_ANY = 0, + GIT_CACHE_STORE_RAW = 1, + GIT_CACHE_STORE_PARSED = 2 +}; typedef struct { - git_oid oid; + git_oid oid; + int16_t type; /* git_otype value */ + uint16_t flags; /* GIT_CACHE_STORE value */ + size_t size; git_atomic refcount; } git_cached_obj; typedef struct { - git_cached_obj **nodes; - git_mutex lock; - - unsigned int lru_count; - size_t size_mask; - git_cached_obj_freeptr free_obj; + git_oidmap *map; + git_mutex lock; + ssize_t used_memory; } git_cache; -int git_cache_init(git_cache *cache, size_t size, git_cached_obj_freeptr free_ptr); +extern bool git_cache__enabled; +extern ssize_t git_cache__max_storage; +extern git_atomic_ssize git_cache__current_storage; + +int git_cache_set_max_object_size(git_otype type, size_t size); + +int git_cache_init(git_cache *cache); void git_cache_free(git_cache *cache); +void git_cache_clear(git_cache *cache); -void *git_cache_try_store(git_cache *cache, void *entry); -void *git_cache_get(git_cache *cache, const git_oid *oid); +void *git_cache_store_raw(git_cache *cache, git_odb_object *entry); +void *git_cache_store_parsed(git_cache *cache, git_object *entry); -GIT_INLINE(void) git_cached_obj_incref(void *_obj) +git_odb_object *git_cache_get_raw(git_cache *cache, const git_oid *oid); +git_object *git_cache_get_parsed(git_cache *cache, const git_oid *oid); +void *git_cache_get_any(git_cache *cache, const git_oid *oid); + +GIT_INLINE(size_t) git_cache_size(git_cache *cache) { - git_cached_obj *obj = _obj; - git_atomic_inc(&obj->refcount); + return (size_t)kh_size(cache->map); } -GIT_INLINE(void) git_cached_obj_decref(void *_obj, git_cached_obj_freeptr free_obj) +GIT_INLINE(void) git_cached_obj_incref(void *_obj) { git_cached_obj *obj = _obj; - - if (git_atomic_dec(&obj->refcount) == 0) - free_obj(obj); + git_atomic_inc(&obj->refcount); } +void git_cached_obj_decref(void *_obj); + #endif diff --git a/src/checkout.c b/src/checkout.c index 24fa21024..ec9da7e2e 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -16,9 +16,11 @@ #include "git2/config.h" #include "git2/diff.h" #include "git2/submodule.h" +#include "git2/sys/index.h" #include "refs.h" #include "repository.h" +#include "index.h" #include "filter.h" #include "blob.h" #include "diff.h" @@ -119,6 +121,7 @@ static bool checkout_is_workdir_modified( const git_index_entry *wditem) { git_oid oid; + const git_index_entry *ie; /* handle "modified" submodule */ if (wditem->mode == GIT_FILEMODE_COMMIT) { @@ -137,7 +140,18 @@ static bool checkout_is_workdir_modified( if (!sm_oid) return false; - return (git_oid_cmp(&baseitem->oid, sm_oid) != 0); + return (git_oid__cmp(&baseitem->oid, sm_oid) != 0); + } + + /* Look at the cache to decide if the workdir is modified. If not, + * we can simply compare the oid in the cache to the baseitem instead + * of hashing the file. + */ + if ((ie = git_index_get_bypath(data->index, wditem->path, 0)) != NULL) { + if (wditem->mtime.seconds == ie->mtime.seconds && + wditem->mtime.nanoseconds == ie->mtime.nanoseconds && + wditem->file_size == ie->file_size) + return (git_oid__cmp(&baseitem->oid, &ie->oid) != 0); } /* depending on where base is coming from, we may or may not know @@ -151,7 +165,7 @@ static bool checkout_is_workdir_modified( wditem->file_size, &oid) < 0) return false; - return (git_oid_cmp(&baseitem->oid, &oid) != 0); + return (git_oid__cmp(&baseitem->oid, &oid) != 0); } #define CHECKOUT_ACTION_IF(FLAG,YES,NO) \ @@ -176,6 +190,10 @@ static int checkout_action_common( action = (action & ~CHECKOUT_ACTION__UPDATE_BLOB) | CHECKOUT_ACTION__UPDATE_SUBMODULE; + /* to "update" a symlink, we must remove the old one first */ + if (delta->new_file.mode == GIT_FILEMODE_LINK && wd != NULL) + action |= CHECKOUT_ACTION__REMOVE; + notify = GIT_CHECKOUT_NOTIFY_UPDATED; } @@ -202,9 +220,11 @@ static int checkout_action_no_wd( action = CHECKOUT_ACTION_IF(SAFE_CREATE, UPDATE_BLOB, NONE); break; case GIT_DELTA_ADDED: /* case 2 or 28 (and 5 but not really) */ - case GIT_DELTA_MODIFIED: /* case 13 (and 35 but not really) */ action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); break; + case GIT_DELTA_MODIFIED: /* case 13 (and 35 but not really) */ + action = CHECKOUT_ACTION_IF(SAFE_CREATE, UPDATE_BLOB, CONFLICT); + break; case GIT_DELTA_TYPECHANGE: /* case 21 (B->T) and 28 (T->B)*/ if (delta->new_file.mode == GIT_FILEMODE_TREE) action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); @@ -226,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 */ @@ -454,6 +474,7 @@ static int checkout_action( int cmp = -1, act; int (*strcomp)(const char *, const char *) = data->diff->strcomp; int (*pfxcomp)(const char *str, const char *pfx) = data->diff->pfxcomp; + int error; /* move workdir iterator to follow along with deltas */ @@ -477,9 +498,9 @@ static int checkout_action( if (cmp == 0) { if (wd->mode == GIT_FILEMODE_TREE) { /* case 2 - entry prefixed by workdir tree */ - if (git_iterator_advance_into(&wd, workdir) < 0) + error = git_iterator_advance_into_or_over(&wd, workdir); + if (error && error != GIT_ITEROVER) goto fail; - *wditem_ptr = wd; continue; } @@ -494,8 +515,10 @@ static int checkout_action( } /* case 1 - handle wd item (if it matches pathspec) */ - if (checkout_action_wd_only(data, workdir, wd, pathspec) < 0 || - git_iterator_advance(&wd, workdir) < 0) + if (checkout_action_wd_only(data, workdir, wd, pathspec) < 0) + goto fail; + if ((error = git_iterator_advance(&wd, workdir)) < 0 && + error != GIT_ITEROVER) goto fail; *wditem_ptr = wd; @@ -518,8 +541,9 @@ static int checkout_action( if (delta->status == GIT_DELTA_TYPECHANGE) { if (delta->old_file.mode == GIT_FILEMODE_TREE) { act = checkout_action_with_wd(data, delta, wd); - if (git_iterator_advance_into(&wd, workdir) < 0) - wd = NULL; + if ((error = git_iterator_advance_into(&wd, workdir)) < 0 && + error != GIT_ENOTFOUND) + goto fail; *wditem_ptr = wd; return act; } @@ -529,8 +553,9 @@ static int checkout_action( delta->old_file.mode == GIT_FILEMODE_COMMIT) { act = checkout_action_with_wd(data, delta, wd); - if (git_iterator_advance(&wd, workdir) < 0) - wd = NULL; + if ((error = git_iterator_advance(&wd, workdir)) < 0 && + error != GIT_ITEROVER) + goto fail; *wditem_ptr = wd; return act; } @@ -561,6 +586,9 @@ static int checkout_remaining_wd_items( error = git_iterator_advance(&wd, workdir); } + if (error == GIT_ITEROVER) + error = 0; + return error; } @@ -579,10 +607,11 @@ 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) + if ((error = git_iterator_current(&wditem, workdir)) < 0 && + error != GIT_ITEROVER) goto fail; deltas = &data->diff->deltas; @@ -630,7 +659,7 @@ static int checkout_get_actions( goto fail; } - git_pathspec_free(&pathspec); + git_pathspec__vfree(&pathspec); git_pool_clear(&pathpool); return 0; @@ -641,7 +670,7 @@ fail: *actions_ptr = NULL; git__free(actions); - git_pathspec_free(&pathspec); + git_pathspec__vfree(&pathspec); git_pool_clear(&pathpool); return error; @@ -655,33 +684,26 @@ static int buffer_to_file( int file_open_flags, mode_t file_mode) { - int fd, error; + int error; if ((error = git_futils_mkpath2file(path, dir_mode)) < 0) return error; - if ((fd = p_open(path, file_open_flags, file_mode)) < 0) { - giterr_set(GITERR_OS, "Could not open '%s' for writing", path); - return fd; - } - - if ((error = p_write(fd, git_buf_cstr(buffer), git_buf_len(buffer))) < 0) { - giterr_set(GITERR_OS, "Could not write to '%s'", path); - (void)p_close(fd); - } else { - if ((error = p_close(fd)) < 0) - giterr_set(GITERR_OS, "Error while closing '%s'", path); + if ((error = git_futils_writebuffer( + buffer, path, file_open_flags, file_mode)) < 0) + return error; - if ((error = p_stat(path, st)) < 0) - giterr_set(GITERR_OS, "Error while statting '%s'", path); + if (st != NULL && (error = p_stat(path, st)) < 0) { + giterr_set(GITERR_OS, "Error while statting '%s'", path); + return error; } - if (!error && - (file_mode & 0100) != 0 && - (error = p_chmod(path, file_mode)) < 0) + if ((file_mode & 0100) != 0 && (error = p_chmod(path, file_mode)) < 0) { giterr_set(GITERR_OS, "Failed to set permissions on '%s'", path); + return error; + } - return error; + return 0; } static int blob_content_to_file( @@ -698,8 +720,8 @@ static int blob_content_to_file( git_vector filters = GIT_VECTOR_INIT; /* Create a fake git_buf from the blob raw data... */ - filtered.ptr = blob->odb_object->raw.data; - filtered.size = blob->odb_object->raw.len; + filtered.ptr = (void *)git_blob_rawcontent(blob); + filtered.size = (size_t)git_blob_rawsize(blob); /* ... and make sure it doesn't get unexpectedly freed */ dont_free_filtered = true; @@ -747,17 +769,24 @@ cleanup: } static int blob_content_to_link( - struct stat *st, git_blob *blob, const char *path, int can_symlink) + struct stat *st, + git_blob *blob, + const char *path, + mode_t dir_mode, + int can_symlink) { git_buf linktarget = GIT_BUF_INIT; int error; + if ((error = git_futils_mkpath2file(path, dir_mode)) < 0) + return error; + if ((error = git_blob__getbuf(&linktarget, blob)) < 0) return error; if (can_symlink) { if ((error = p_symlink(git_buf_cstr(&linktarget), path)) < 0) - giterr_set(GITERR_CHECKOUT, "Could not create symlink %s\n", path); + giterr_set(GITERR_OS, "Could not create symlink %s\n", path); } else { error = git_futils_fake_symlink(git_buf_cstr(&linktarget), path); } @@ -792,6 +821,31 @@ static int checkout_update_index( return git_index_add(data->index, &entry); } +static int checkout_submodule_update_index( + checkout_data *data, + const git_diff_file *file) +{ + struct stat st; + + /* update the index unless prevented */ + if ((data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) != 0) + return 0; + + git_buf_truncate(&data->path, data->workdir_len); + if (git_buf_puts(&data->path, file->path) < 0) + return -1; + + if (p_stat(git_buf_cstr(&data->path), &st) < 0) { + giterr_set( + GITERR_CHECKOUT, "Could not stat submodule %s\n", file->path); + return GIT_ENOTFOUND; + } + + st.st_mode = GIT_FILEMODE_COMMIT; + + return checkout_update_index(data, file, &st); +} + static int checkout_submodule( checkout_data *data, const git_diff_file *file) @@ -804,12 +858,21 @@ static int checkout_submodule( return 0; if ((error = git_futils_mkdir( - file->path, git_repository_workdir(data->repo), + file->path, data->opts.target_directory, data->opts.dir_mode, GIT_MKDIR_PATH)) < 0) return error; - if ((error = git_submodule_lookup(&sm, data->repo, file->path)) < 0) + if ((error = git_submodule_lookup(&sm, data->repo, file->path)) < 0) { + /* I've observed repos with submodules in the tree that do not + * have a .gitmodules - core Git just makes an empty directory + */ + if (error == GIT_ENOTFOUND) { + giterr_clear(); + return checkout_submodule_update_index(data, file); + } + return error; + } /* TODO: Support checkout_strategy options. Two circumstances: * 1 - submodule already checked out, but we need to move the HEAD @@ -820,26 +883,7 @@ static int checkout_submodule( * command should probably be able to. Do we need a submodule callback? */ - /* update the index unless prevented */ - if ((data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0) { - struct stat st; - - git_buf_truncate(&data->path, data->workdir_len); - if (git_buf_puts(&data->path, file->path) < 0) - return -1; - - if ((error = p_stat(git_buf_cstr(&data->path), &st)) < 0) { - giterr_set( - GITERR_CHECKOUT, "Could not stat submodule %s\n", file->path); - return error; - } - - st.st_mode = GIT_FILEMODE_COMMIT; - - error = checkout_update_index(data, file, &st); - } - - return error; + return checkout_submodule_update_index(data, file); } static void report_progress( @@ -897,7 +941,7 @@ static int checkout_blob( if (S_ISLNK(file->mode)) error = blob_content_to_link( - &st, blob, git_buf_cstr(&data->path), data->can_symlink); + &st, blob, git_buf_cstr(&data->path), data->opts.dir_mode, data->can_symlink); else error = blob_content_to_file( &st, blob, git_buf_cstr(&data->path), file->mode, &data->opts); @@ -938,6 +982,9 @@ static int checkout_remove_the_old( uint32_t flg = GIT_RMDIR_EMPTY_PARENTS | GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_REMOVE_BLOCKERS; + if (data->opts.checkout_strategy & GIT_CHECKOUT_SKIP_LOCKED_DIRECTORIES) + flg |= GIT_RMDIR_SKIP_NONEMPTY; + git_buf_truncate(&data->path, data->workdir_len); git_vector_foreach(&data->diff->deltas, i, delta) { @@ -983,7 +1030,7 @@ static int checkout_deferred_remove(git_repository *repo, const char *path) { #if 0 int error = git_futils_rmdir_r( - path, git_repository_workdir(repo), GIT_RMDIR_EMPTY_PARENTS); + path, data->opts.target_directory, GIT_RMDIR_EMPTY_PARENTS); if (error == GIT_ENOTFOUND) { error = 0; @@ -1107,7 +1154,6 @@ static int checkout_data_init( git_checkout_opts *proposed) { int error = 0; - git_config *cfg; git_repository *repo = git_iterator_owner(target); memset(data, 0, sizeof(*data)); @@ -1117,10 +1163,8 @@ static int checkout_data_init( return -1; } - if ((error = git_repository__ensure_not_bare(repo, "checkout")) < 0) - return error; - - if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + if ((!proposed || !proposed->target_directory) && + (error = git_repository__ensure_not_bare(repo, "checkout")) < 0) return error; data->repo = repo; @@ -1133,9 +1177,19 @@ static int checkout_data_init( else memmove(&data->opts, proposed, sizeof(git_checkout_opts)); + if (!data->opts.target_directory) + data->opts.target_directory = git_repository_workdir(repo); + else if (!git_path_isdir(data->opts.target_directory) && + (error = git_futils_mkdir(data->opts.target_directory, NULL, + GIT_DIR_MODE, GIT_MKDIR_VERIFY_DIR)) < 0) + goto cleanup; + /* refresh config and index content unless NO_REFRESH is given */ if ((data->opts.checkout_strategy & GIT_CHECKOUT_NO_REFRESH) == 0) { - if ((error = git_config_refresh(cfg)) < 0) + git_config *cfg; + + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0 || + (error = git_config_refresh(cfg)) < 0) goto cleanup; /* if we are checking out the index, don't reload, @@ -1172,19 +1226,13 @@ static int checkout_data_init( data->pfx = git_pathspec_prefix(&data->opts.paths); - error = git_config_get_bool(&data->can_symlink, cfg, "core.symlinks"); - if (error < 0) { - if (error != GIT_ENOTFOUND) - goto cleanup; - - /* If "core.symlinks" is not found anywhere, default to true. */ - data->can_symlink = true; - giterr_clear(); - error = 0; - } + if ((error = git_repository__cvar( + &data->can_symlink, repo, GIT_CVAR_SYMLINKS)) < 0) + goto cleanup; if (!data->opts.baseline) { data->opts_free_baseline = true; + error = checkout_lookup_head_tree(&data->opts.baseline, repo); if (error == GIT_EORPHANEDHEAD) { @@ -1198,7 +1246,8 @@ static int checkout_data_init( if ((error = git_vector_init(&data->removes, 0, git__strcmp_cb)) < 0 || (error = git_pool_init(&data->pool, 1, 0)) < 0 || - (error = git_buf_puts(&data->path, git_repository_workdir(repo))) < 0) + (error = git_buf_puts(&data->path, data->opts.target_directory)) < 0 || + (error = git_path_to_dir(&data->path)) < 0) goto cleanup; data->workdir_len = git_buf_len(&data->path); @@ -1246,11 +1295,13 @@ int git_checkout_iterator( GIT_ITERATOR_IGNORE_CASE : GIT_ITERATOR_DONT_IGNORE_CASE; if ((error = git_iterator_reset(target, data.pfx, data.pfx)) < 0 || - (error = git_iterator_for_workdir( - &workdir, data.repo, iterflags | GIT_ITERATOR_DONT_AUTOEXPAND, + (error = git_iterator_for_workdir_ext( + &workdir, data.repo, data.opts.target_directory, + iterflags | GIT_ITERATOR_DONT_AUTOEXPAND, data.pfx, data.pfx)) < 0 || (error = git_iterator_for_tree( - &baseline, data.opts.baseline, iterflags, data.pfx, data.pfx)) < 0) + &baseline, data.opts.baseline, + iterflags, data.pfx, data.pfx)) < 0) goto cleanup; /* Should not have case insensitivity mismatch */ @@ -1318,8 +1369,19 @@ int git_checkout_index( int error; git_iterator *index_i; - if ((error = git_repository__ensure_not_bare(repo, "checkout index")) < 0) - return error; + if (!index && !repo) { + giterr_set(GITERR_CHECKOUT, + "Must provide either repository or index to checkout"); + return -1; + } + if (index && repo && git_index_owner(index) != repo) { + giterr_set(GITERR_CHECKOUT, + "Index to checkout does not match repository"); + return -1; + } + + if (!repo) + repo = git_index_owner(index); if (!index && (error = git_repository_index__weakptr(&index, repo)) < 0) return error; @@ -1343,8 +1405,19 @@ int git_checkout_tree( git_tree *tree = NULL; git_iterator *tree_i = NULL; - if ((error = git_repository__ensure_not_bare(repo, "checkout tree")) < 0) - return error; + if (!treeish && !repo) { + giterr_set(GITERR_CHECKOUT, + "Must provide either repository or tree to checkout"); + return -1; + } + if (treeish && repo && git_object_owner(treeish) != repo) { + giterr_set(GITERR_CHECKOUT, + "Object to checkout does not match repository"); + return -1; + } + + if (!repo) + repo = git_object_owner(treeish); if (git_object_peel((git_object **)&tree, treeish, GIT_OBJ_TREE) < 0) { giterr_set( @@ -1369,8 +1442,7 @@ int git_checkout_head( git_tree *head = NULL; git_iterator *head_i = NULL; - if ((error = git_repository__ensure_not_bare(repo, "checkout head")) < 0) - return error; + assert(repo); if (!(error = checkout_lookup_head_tree(&head, repo)) && !(error = git_iterator_for_tree(&head_i, head, 0, NULL, NULL))) diff --git a/src/clone.c b/src/clone.c index 0bbccd44b..5c11872cc 100644 --- a/src/clone.c +++ b/src/clone.c @@ -21,6 +21,7 @@ #include "fileops.h" #include "refs.h" #include "path.h" +#include "repository.h" static int create_branch( git_reference **branch, @@ -132,14 +133,14 @@ static int reference_matches_remote_head( return 0; } - if (git_oid_cmp(&head_info->remote_head_oid, &oid) == 0) { + if (git_oid__cmp(&head_info->remote_head_oid, &oid) == 0) { /* Determine the local reference name from the remote tracking one */ if (git_refspec_transform_l( - &head_info->branchname, + &head_info->branchname, head_info->refspec, reference_name) < 0) return -1; - + if (git_buf_len(&head_info->branchname) > 0) { if (git_buf_sets( &head_info->branchname, @@ -187,6 +188,7 @@ static int get_head_callback(git_remote_head *head, void *payload) static int update_head_to_remote(git_repository *repo, git_remote *remote) { int retcode = -1; + git_refspec dummy_spec; git_remote_head *remote_head; struct head_info head_info; git_buf remote_master_name = GIT_BUF_INIT; @@ -202,7 +204,7 @@ static int update_head_to_remote(git_repository *repo, git_remote *remote) /* Get the remote's HEAD. This is always the first ref in remote->refs. */ remote_head = NULL; - + if (!remote->transport->ls(remote->transport, get_head_callback, &remote_head)) return -1; @@ -211,9 +213,14 @@ static int update_head_to_remote(git_repository *repo, git_remote *remote) git_oid_cpy(&head_info.remote_head_oid, &remote_head->oid); git_buf_init(&head_info.branchname, 16); head_info.repo = repo; - head_info.refspec = git_remote_fetchspec(remote); + head_info.refspec = git_remote__matching_refspec(remote, GIT_REFS_HEADS_MASTER_FILE); head_info.found = 0; - + + if (head_info.refspec == NULL) { + memset(&dummy_spec, 0, sizeof(git_refspec)); + head_info.refspec = &dummy_spec; + } + /* Determine the remote tracking reference name from the local master */ if (git_refspec_transform_r( &remote_master_name, @@ -235,9 +242,8 @@ static int update_head_to_remote(git_repository *repo, git_remote *remote) } /* Not master. Check all the other refs. */ - if (git_reference_foreach( + if (git_reference_foreach_name( repo, - GIT_REF_LISTALL, reference_matches_remote_head, &head_info) < 0) goto cleanup; @@ -269,7 +275,7 @@ static int update_head_to_branch( int retcode; git_buf remote_branch_name = GIT_BUF_INIT; git_reference* remote_ref = NULL; - + assert(options->checkout_branch); if ((retcode = git_buf_printf(&remote_branch_name, GIT_REFS_REMOTES_DIR "%s/%s", @@ -317,18 +323,24 @@ static int create_and_configure_origin( (error = git_remote_set_callbacks(origin, options->remote_callbacks)) < 0) goto on_error; - if (options->fetch_spec && - (error = git_remote_set_fetchspec(origin, options->fetch_spec)) < 0) - goto on_error; + if (options->fetch_spec) { + git_remote_clear_refspecs(origin); + if ((error = git_remote_add_fetch(origin, options->fetch_spec)) < 0) + goto on_error; + } if (options->push_spec && - (error = git_remote_set_pushspec(origin, options->push_spec)) < 0) + (error = git_remote_add_push(origin, options->push_spec)) < 0) goto on_error; if (options->pushurl && (error = git_remote_set_pushurl(origin, options->pushurl)) < 0) goto on_error; + if (options->transport_flags == GIT_TRANSPORTFLAGS_NO_CHECK_CERT) { + git_remote_check_cert(origin, 0); + } + if ((error = git_remote_save(origin)) < 0) goto on_error; @@ -347,50 +359,48 @@ static int setup_remotes_and_fetch( const git_clone_options *options) { int retcode = GIT_ERROR; - git_remote *origin; + git_remote *origin = NULL; /* Construct an origin remote */ - if (!create_and_configure_origin(&origin, repo, url, options)) { - git_remote_set_update_fetchhead(origin, 0); - - /* Connect and download everything */ - if (!git_remote_connect(origin, GIT_DIRECTION_FETCH)) { - if (!(retcode = git_remote_download(origin, options->fetch_progress_cb, - options->fetch_progress_payload))) { - /* Create "origin/foo" branches for all remote branches */ - if (!git_remote_update_tips(origin)) { - /* Point HEAD to the requested branch */ - if (options->checkout_branch) { - if (!update_head_to_branch(repo, options)) - retcode = 0; - } - /* Point HEAD to the same ref as the remote's head */ - else if (!update_head_to_remote(repo, origin)) { - retcode = 0; - } - } - } - git_remote_disconnect(origin); - } - git_remote_free(origin); - } + if ((retcode = create_and_configure_origin(&origin, repo, url, options)) < 0) + goto on_error; - return retcode; -} + git_remote_set_update_fetchhead(origin, 0); + + /* If the download_tags value has not been specified, then make sure to + * download tags as well. It is set here because we want to download tags + * on the initial clone, but do not want to persist the value in the + * configuration file. + */ + if (origin->download_tags == GIT_REMOTE_DOWNLOAD_TAGS_AUTO && + ((retcode = git_remote_add_fetch(origin, "refs/tags/*:refs/tags/*")) < 0)) + goto on_error; + /* Connect and download everything */ + if ((retcode = git_remote_connect(origin, GIT_DIRECTION_FETCH)) < 0) + goto on_error; -static bool path_is_okay(const char *path) -{ - /* The path must either not exist, or be an empty directory */ - if (!git_path_exists(path)) return true; - if (!git_path_is_empty_dir(path)) { - giterr_set(GITERR_INVALID, - "'%s' exists and is not an empty directory", path); - return false; - } - return true; + if ((retcode = git_remote_download(origin, options->fetch_progress_cb, + options->fetch_progress_payload)) < 0) + goto on_error; + + /* Create "origin/foo" branches for all remote branches */ + if ((retcode = git_remote_update_tips(origin)) < 0) + goto on_error; + + /* Point HEAD to the requested branch */ + if (options->checkout_branch) + retcode = update_head_to_branch(repo, options); + /* Point HEAD to the same ref as the remote's head */ + else + retcode = update_head_to_remote(repo, origin); + +on_error: + git_remote_free(origin); + return retcode; } + static bool should_checkout( git_repository *repo, bool is_bare, @@ -417,7 +427,6 @@ static void normalize_options(git_clone_options *dst, const git_clone_options *s /* Provide defaults for null pointers */ if (!dst->remote_name) dst->remote_name = "origin"; - if (!dst->remote_autotag) dst->remote_autotag = GIT_REMOTE_DOWNLOAD_TAGS_ALL; } int git_clone( @@ -436,7 +445,10 @@ int git_clone( normalize_options(&normOptions, options); GITERR_CHECK_VERSION(&normOptions, GIT_CLONE_OPTIONS_VERSION, "git_clone_options"); - if (!path_is_okay(local_path)) { + /* Only clone to a new directory or an empty directory */ + if (git_path_exists(local_path) && !git_path_is_empty_dir(local_path)) { + giterr_set(GITERR_INVALID, + "'%s' exists and is not an empty directory", local_path); return GIT_ERROR; } diff --git a/src/commit.c b/src/commit.c index c7b83ed43..15a195fe5 100644 --- a/src/commit.c +++ b/src/commit.c @@ -9,6 +9,7 @@ #include "git2/object.h" #include "git2/repository.h" #include "git2/signature.h" +#include "git2/sys/commit.h" #include "common.h" #include "odb.h" @@ -18,42 +19,33 @@ #include <stdarg.h> -static void clear_parents(git_commit *commit) +void git_commit__free(void *_commit) { - size_t i; + git_commit *commit = _commit; - 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(git_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); + git__free(commit->raw_header); git__free(commit->message); git__free(commit->message_encoding); + git__free(commit); } int git_commit_create_v( - git_oid *oid, - git_repository *repo, - const char *update_ref, - const git_signature *author, - const git_signature *committer, - const char *message_encoding, - const char *message, - const git_tree *tree, - int parent_count, - ...) + git_oid *oid, + git_repository *repo, + const char *update_ref, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + const git_tree *tree, + int parent_count, + ...) { va_list ap; int i, res; @@ -76,30 +68,28 @@ int git_commit_create_v( return res; } -int git_commit_create( - git_oid *oid, - git_repository *repo, - const char *update_ref, - const git_signature *author, - const git_signature *committer, - const char *message_encoding, - const char *message, - const git_tree *tree, - int parent_count, - const git_commit *parents[]) +int git_commit_create_from_oids( + git_oid *oid, + git_repository *repo, + const char *update_ref, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + const git_oid *tree, + int parent_count, + const git_oid *parents[]) { git_buf commit = GIT_BUF_INIT; int i; git_odb *odb; - assert(git_object_owner((const git_object *)tree) == repo); + assert(oid && repo && tree && parent_count >= 0); - git_oid__writebuf(&commit, "tree ", git_object_id((const git_object *)tree)); + git_oid__writebuf(&commit, "tree ", tree); - for (i = 0; i < parent_count; ++i) { - assert(git_object_owner((const git_object *)parents[i]) == repo); - git_oid__writebuf(&commit, "parent ", git_object_id((const git_object *)parents[i])); - } + for (i = 0; i < parent_count; ++i) + git_oid__writebuf(&commit, "parent ", parents[i]); git_signature__writebuf(&commit, "author ", author); git_signature__writebuf(&commit, "committer ", committer); @@ -131,14 +121,74 @@ on_error: return -1; } -int git_commit__parse_buffer(git_commit *commit, const void *data, size_t len) +int git_commit_create( + git_oid *oid, + git_repository *repo, + const char *update_ref, + const git_signature *author, + const git_signature *committer, + const char *message_encoding, + const char *message, + const git_tree *tree, + int parent_count, + const git_commit *parents[]) { - const char *buffer = data; - const char *buffer_end = (const char *)data + len; + int retval, i; + const git_oid **parent_oids; + + assert(parent_count >= 0); + assert(git_object_owner((const git_object *)tree) == repo); + + parent_oids = git__malloc(parent_count * sizeof(git_oid *)); + GITERR_CHECK_ALLOC(parent_oids); + + for (i = 0; i < parent_count; ++i) { + assert(git_object_owner((const git_object *)parents[i]) == repo); + parent_oids[i] = git_object_id((const git_object *)parents[i]); + } + + retval = git_commit_create_from_oids( + oid, repo, update_ref, author, committer, + message_encoding, message, + git_object_id((const git_object *)tree), parent_count, parent_oids); + + git__free((void *)parent_oids); + + return retval; +} + +int git_commit__parse(void *_commit, git_odb_object *odb_obj) +{ + git_commit *commit = _commit; + 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; + uint32_t parent_count = 0; + size_t header_len; + + /* 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; + } - if (git_vector_init(&commit->parent_ids, 4, NULL) < 0) - return -1; + 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; + + 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; @@ -148,13 +198,10 @@ int git_commit__parse_buffer(git_commit *commit, const void *data, size_t len) */ 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)); @@ -170,8 +217,8 @@ int git_commit__parse_buffer(git_commit *commit, const void *data, size_t len) 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; @@ -185,15 +232,18 @@ int git_commit__parse_buffer(git_commit *commit, const void *data, size_t len) 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); @@ -206,12 +256,6 @@ bad_buffer: return -1; } -int git_commit__parse(git_commit *commit, git_odb_object *obj) -{ - assert(commit); - return git_commit__parse_buffer(commit, obj->raw.data, obj->raw.len); -} - #define GIT_COMMIT_GETTER(_rvalue, _name, _return) \ _rvalue git_commit_##_name(const git_commit *commit) \ {\ @@ -223,9 +267,10 @@ 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) +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) @@ -234,14 +279,16 @@ int git_commit_tree(git_tree **tree_out, const git_commit *commit) return git_tree_lookup(tree_out, commit->object.repo, &commit->tree_id); } -const git_oid *git_commit_parent_id(git_commit *commit, unsigned int n) +const git_oid *git_commit_parent_id( + const git_commit *commit, unsigned int n) { assert(commit); - return git_vector_get(&commit->parent_ids, n); + return git_array_get(commit->parent_ids, n); } -int git_commit_parent(git_commit **parent, git_commit *commit, unsigned int n) +int git_commit_parent( + git_commit **parent, const git_commit *commit, unsigned int n) { const git_oid *parent_id; assert(commit); @@ -260,7 +307,7 @@ int git_commit_nth_gen_ancestor( const git_commit *commit, unsigned int n) { - git_commit *current, *parent; + git_commit *current, *parent = NULL; int error; assert(ancestor && commit); diff --git a/src/commit.h b/src/commit.h index 1ab164c0b..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 <time.h> struct git_commit { git_object object; - git_vector parent_ids; + git_array_t(git_oid) parent_ids; git_oid tree_id; git_signature *author; @@ -25,10 +25,10 @@ struct git_commit { char *message_encoding; char *message; + char *raw_header; }; -void git_commit__free(git_commit *c); -int git_commit__parse(git_commit *commit, git_odb_object *obj); +void git_commit__free(void *commit); +int git_commit__parse(void *commit, git_odb_object *obj); -int git_commit__parse_buffer(git_commit *commit, const void *data, size_t len); #endif diff --git a/src/commit_list.c b/src/commit_list.c index 603dd754a..64416e54d 100644 --- a/src/commit_list.c +++ b/src/commit_list.c @@ -36,7 +36,7 @@ git_commit_list *git_commit_list_insert_by_date(git_commit_list_node *item, git_ git_commit_list *p; while ((p = *pp) != NULL) { - if (git_commit_list_time_cmp(p->item, item) < 0) + if (git_commit_list_time_cmp(p->item, item) > 0) break; pp = &p->next; @@ -100,12 +100,15 @@ git_commit_list_node *git_commit_list_pop(git_commit_list **stack) return item; } -static int commit_quick_parse(git_revwalk *walk, git_commit_list_node *commit, git_rawobj *raw) +static int commit_quick_parse( + git_revwalk *walk, + git_commit_list_node *commit, + const uint8_t *buffer, + size_t buffer_len) { const size_t parent_len = strlen("parent ") + GIT_OID_HEXSZ + 1; - unsigned char *buffer = raw->data; - unsigned char *buffer_end = buffer + raw->len; - unsigned char *parents_start, *committer_start; + const uint8_t *buffer_end = buffer + buffer_len; + const uint8_t *parents_start, *committer_start; int i, parents = 0; int commit_time; @@ -124,7 +127,7 @@ static int commit_quick_parse(git_revwalk *walk, git_commit_list_node *commit, g for (i = 0; i < parents; ++i) { git_oid oid; - if (git_oid_fromstr(&oid, (char *)buffer + strlen("parent ")) < 0) + if (git_oid_fromstr(&oid, (const char *)buffer + strlen("parent ")) < 0) return -1; commit->parents[i] = git_revwalk__commit_lookup(walk, &oid); @@ -182,11 +185,14 @@ int git_commit_list_parse(git_revwalk *walk, git_commit_list_node *commit) if ((error = git_odb_read(&obj, walk->odb, &commit->oid)) < 0) return error; - if (obj->raw.type != GIT_OBJ_COMMIT) { + if (obj->cached.type != GIT_OBJ_COMMIT) { giterr_set(GITERR_INVALID, "Object is no commit object"); error = -1; } else - error = commit_quick_parse(walk, commit, &obj->raw); + error = commit_quick_parse( + walk, commit, + (const uint8_t *)git_odb_object_data(obj), + git_odb_object_size(obj)); git_odb_object_free(obj); return error; diff --git a/src/config.c b/src/config.c index 5379b0ec5..2a058549f 100644 --- a/src/config.c +++ b/src/config.c @@ -9,6 +9,7 @@ #include "fileops.h" #include "config.h" #include "git2/config.h" +#include "git2/sys/config.h" #include "vector.h" #include "buf_text.h" #include "config_file.h" @@ -22,7 +23,7 @@ typedef struct { git_refcount rc; git_config_backend *file; - unsigned int level; + git_config_level_t level; } file_internal; static void file_internal_free(file_internal *internal) @@ -39,12 +40,14 @@ static void config_free(git_config *cfg) size_t i; file_internal *internal; - for(i = 0; i < cfg->files.length; ++i){ + for (i = 0; i < cfg->files.length; ++i) { internal = git_vector_get(&cfg->files, i); GIT_REFCOUNT_DEC(internal, file_internal_free); } git_vector_free(&cfg->files); + + git__memzero(cfg, sizeof(*cfg)); git__free(cfg); } @@ -86,17 +89,19 @@ int git_config_new(git_config **out) int git_config_add_file_ondisk( git_config *cfg, const char *path, - unsigned int level, + git_config_level_t level, int force) { git_config_backend *file = NULL; + struct stat st; int res; assert(cfg && path); - if (!git_path_isfile(path)) { - giterr_set(GITERR_CONFIG, "Cannot find config file '%s'", path); - return GIT_ENOTFOUND; + res = p_stat(path, &st); + if (res < 0 && errno != ENOENT) { + giterr_set(GITERR_CONFIG, "Error stat'ing config file '%s'", path); + return -1; } if (git_config_file__ondisk(&file, path) < 0) @@ -135,11 +140,11 @@ int git_config_open_ondisk(git_config **out, const char *path) static int find_internal_file_by_level( file_internal **internal_out, const git_config *cfg, - int level) + git_config_level_t level) { int pos = -1; file_internal *internal; - unsigned int i; + size_t i; /* when passing GIT_CONFIG_HIGHEST_LEVEL, the idea is to get the config file * which has the highest level. As config files are stored in a vector @@ -150,14 +155,14 @@ static int find_internal_file_by_level( pos = 0; } else { git_vector_foreach(&cfg->files, i, internal) { - if (internal->level == (unsigned int)level) - pos = i; + if (internal->level == level) + pos = (int)i; } } if (pos == -1) { giterr_set(GITERR_CONFIG, - "No config file exists for the given level '%i'", level); + "No config file exists for the given level '%i'", (int)level); return GIT_ENOTFOUND; } @@ -172,21 +177,21 @@ static int duplicate_level(void **old_raw, void *new_raw) GIT_UNUSED(new_raw); - giterr_set(GITERR_CONFIG, "A file with the same level (%i) has already been added to the config", (*old)->level); + giterr_set(GITERR_CONFIG, "A file with the same level (%i) has already been added to the config", (int)(*old)->level); return GIT_EEXISTS; } static void try_remove_existing_file_internal( git_config *cfg, - unsigned int level) + git_config_level_t level) { int pos = -1; file_internal *internal; - unsigned int i; + size_t i; git_vector_foreach(&cfg->files, i, internal) { if (internal->level == level) - pos = i; + pos = (int)i; } if (pos == -1) @@ -203,7 +208,7 @@ static void try_remove_existing_file_internal( static int git_config__add_internal( git_config *cfg, file_internal *internal, - unsigned int level, + git_config_level_t level, int force) { int result; @@ -224,10 +229,18 @@ static int git_config__add_internal( return 0; } +int git_config_open_global(git_config **cfg_out, git_config *cfg) +{ + if (!git_config_open_level(cfg_out, cfg, GIT_CONFIG_LEVEL_XDG)) + return 0; + + return git_config_open_level(cfg_out, cfg, GIT_CONFIG_LEVEL_GLOBAL); +} + int git_config_open_level( - git_config **cfg_out, - const git_config *cfg_parent, - unsigned int level) + git_config **cfg_out, + const git_config *cfg_parent, + git_config_level_t level) { git_config *cfg; file_internal *internal; @@ -252,7 +265,7 @@ int git_config_open_level( int git_config_add_backend( git_config *cfg, git_config_backend *file, - unsigned int level, + git_config_level_t level, int force) { file_internal *internal; @@ -292,6 +305,9 @@ int git_config_refresh(git_config *cfg) error = file->refresh(file); } + if (!error && GIT_REFCOUNT_OWNER(cfg) != NULL) + git_repository__cvar_cache_clear(GIT_REFCOUNT_OWNER(cfg)); + return error; } @@ -325,21 +341,30 @@ int git_config_foreach_match( return ret; } +/************** + * Setters + **************/ + +static int config_error_nofiles(const char *name) +{ + giterr_set(GITERR_CONFIG, + "Cannot set value for '%s' when no config files exist", name); + return GIT_ENOTFOUND; +} + int git_config_delete_entry(git_config *cfg, const char *name) { git_config_backend *file; file_internal *internal; internal = git_vector_get(&cfg->files, 0); + if (!internal || !internal->file) + return config_error_nofiles(name); file = internal->file; return file->del(file, name); } -/************** - * Setters - **************/ - int git_config_set_int64(git_config *cfg, const char *name, int64_t value) { char str_value[32]; /* All numbers should fit in here */ @@ -359,6 +384,7 @@ int git_config_set_bool(git_config *cfg, const char *name, int value) int git_config_set_string(git_config *cfg, const char *name, const char *value) { + int error; git_config_backend *file; file_internal *internal; @@ -368,9 +394,16 @@ int git_config_set_string(git_config *cfg, const char *name, const char *value) } internal = git_vector_get(&cfg->files, 0); + if (!internal || !internal->file) + return config_error_nofiles(name); file = internal->file; - return file->set(file, name, value); + error = file->set(file, name, value); + + if (!error && GIT_REFCOUNT_OWNER(cfg) != NULL) + git_repository__cvar_cache_clear(GIT_REFCOUNT_OWNER(cfg)); + + return error; } /*********** @@ -426,19 +459,28 @@ static int get_string_at_file(const char **out, const git_config_backend *file, return res; } +static int config_error_notfound(const char *name) +{ + giterr_set(GITERR_CONFIG, "Config value '%s' was not found", name); + return GIT_ENOTFOUND; +} + static int get_string(const char **out, const git_config *cfg, const char *name) { file_internal *internal; unsigned int i; + int res; git_vector_foreach(&cfg->files, i, internal) { - int res = get_string_at_file(out, internal->file, name); + if (!internal || !internal->file) + continue; + res = get_string_at_file(out, internal->file, name); if (res != GIT_ENOTFOUND) return res; } - return GIT_ENOTFOUND; + return config_error_notfound(name); } int git_config_get_bool(int *out, const git_config *cfg, const char *name) @@ -468,25 +510,31 @@ int git_config_get_entry(const git_config_entry **out, const git_config *cfg, co { file_internal *internal; unsigned int i; + git_config_backend *file; + int ret; *out = NULL; git_vector_foreach(&cfg->files, i, internal) { - git_config_backend *file = internal->file; - int ret = file->get(file, name, out); + if (!internal || !internal->file) + continue; + file = internal->file; + + ret = file->get(file, name, out); if (ret != GIT_ENOTFOUND) return ret; } - return GIT_ENOTFOUND; + return config_error_notfound(name); } -int git_config_get_multivar(const git_config *cfg, const char *name, const char *regexp, - git_config_foreach_cb cb, void *payload) +int git_config_get_multivar( + const git_config *cfg, const char *name, const char *regexp, + git_config_foreach_cb cb, void *payload) { file_internal *internal; git_config_backend *file; - int ret = GIT_ENOTFOUND; + int ret = GIT_ENOTFOUND, err; size_t i; /* @@ -495,13 +543,17 @@ int git_config_get_multivar(const git_config *cfg, const char *name, const char */ for (i = cfg->files.length; i > 0; --i) { internal = git_vector_get(&cfg->files, i - 1); + if (!internal || !internal->file) + continue; file = internal->file; - ret = file->get_multivar(file, name, regexp, cb, payload); - if (ret < 0 && ret != GIT_ENOTFOUND) - return ret; + + if (!(err = file->get_multivar(file, name, regexp, cb, payload))) + ret = 0; + else if (err != GIT_ENOTFOUND) + return err; } - return 0; + return (ret == GIT_ENOTFOUND) ? config_error_notfound(name) : 0; } int git_config_set_multivar(git_config *cfg, const char *name, const char *regexp, const char *value) @@ -510,6 +562,8 @@ int git_config_set_multivar(git_config *cfg, const char *name, const char *regex file_internal *internal; internal = git_vector_get(&cfg->files, 0); + if (!internal || !internal->file) + return config_error_nofiles(name); file = internal->file; return file->set_multivar(file, name, regexp, value); @@ -570,17 +624,46 @@ int git_config_find_system(char *system_config_path, size_t length) system_config_path, length, git_config_find_system_r); } +int git_config__global_location(git_buf *buf) +{ + const git_buf *paths; + const char *sep, *start; + size_t len; + + if (git_futils_dirs_get(&paths, GIT_FUTILS_DIR_GLOBAL) < 0) + return -1; + + /* no paths, so give up */ + if (git_buf_len(paths) == 0) + return -1; + + start = git_buf_cstr(paths); + sep = strchr(start, GIT_PATH_LIST_SEPARATOR); + + if (sep) + len = sep - start; + else + len = paths->size; + + if (git_buf_set(buf, start, len) < 0) + return -1; + + return git_buf_joinpath(buf, buf->ptr, GIT_CONFIG_FILENAME_GLOBAL); +} + int git_config_open_default(git_config **out) { int error; git_config *cfg = NULL; git_buf buf = GIT_BUF_INIT; - error = git_config_new(&cfg); + if ((error = git_config_new(&cfg)) < 0) + return error; - if (!error && !git_config_find_global_r(&buf)) + if (!git_config_find_global_r(&buf) || !git_config__global_location(&buf)) { error = git_config_add_file_ondisk(cfg, buf.ptr, GIT_CONFIG_LEVEL_GLOBAL, 0); + } if (!error && !git_config_find_xdg_r(&buf)) error = git_config_add_file_ondisk(cfg, buf.ptr, @@ -592,7 +675,7 @@ int git_config_open_default(git_config **out) git_buf_free(&buf); - if (error && cfg) { + if (error) { git_config_free(cfg); cfg = NULL; } @@ -605,6 +688,7 @@ int git_config_open_default(git_config **out) /*********** * Parsers ***********/ + int git_config_lookup_map_value( int *out, const git_cvar_map *maps, diff --git a/src/config.h b/src/config.h index c43e47e82..c5c11ae14 100644 --- a/src/config.h +++ b/src/config.h @@ -28,6 +28,9 @@ extern int git_config_find_global_r(git_buf *global_config_path); extern int git_config_find_xdg_r(git_buf *system_config_path); extern int git_config_find_system_r(git_buf *system_config_path); + +extern int git_config__global_location(git_buf *buf); + extern int git_config_rename_section( git_repository *repo, const char *old_section_name, /* eg "branch.dummy" */ diff --git a/src/config_cache.c b/src/config_cache.c index 2f36df7d1..84de3a5ed 100644 --- a/src/config_cache.c +++ b/src/config_cache.c @@ -26,7 +26,7 @@ struct map_data { * files that have the text property set. Alternatives are lf, crlf * and native, which uses the platform's native line ending. The default * value is native. See gitattributes(5) for more information on - * end-of-line conversion. + * end-of-line conversion. */ static git_cvar_map _cvar_map_eol[] = { {GIT_CVAR_FALSE, NULL, GIT_EOL_UNSET}, @@ -37,7 +37,7 @@ static git_cvar_map _cvar_map_eol[] = { /* * core.autocrlf - * Setting this variable to "true" is almost the same as setting + * Setting this variable to "true" is almost the same as setting * the text attribute to "auto" on all files except that text files are * not guaranteed to be normalized: files that contain CRLF in the * repository will not be touched. Use this setting if you want to have @@ -51,9 +51,22 @@ static git_cvar_map _cvar_map_autocrlf[] = { {GIT_CVAR_STRING, "input", GIT_AUTO_CRLF_INPUT} }; +/* + * Generic map for integer values + */ +static git_cvar_map _cvar_map_int[] = { + {GIT_CVAR_INT32, NULL, 0}, +}; + static struct map_data _cvar_maps[] = { {"core.autocrlf", _cvar_map_autocrlf, ARRAY_SIZE(_cvar_map_autocrlf), GIT_AUTO_CRLF_DEFAULT}, - {"core.eol", _cvar_map_eol, ARRAY_SIZE(_cvar_map_eol), GIT_EOL_DEFAULT} + {"core.eol", _cvar_map_eol, ARRAY_SIZE(_cvar_map_eol), GIT_EOL_DEFAULT}, + {"core.symlinks", NULL, 0, GIT_SYMLINKS_DEFAULT }, + {"core.ignorecase", NULL, 0, GIT_IGNORECASE_DEFAULT }, + {"core.filemode", NULL, 0, GIT_FILEMODE_DEFAULT }, + {"core.ignorestat", NULL, 0, GIT_IGNORESTAT_DEFAULT }, + {"core.trustctime", NULL, 0, GIT_TRUSTCTIME_DEFAULT }, + {"core.abbrev", _cvar_map_int, 1, GIT_ABBREV_DEFAULT }, }; int git_repository__cvar(int *out, git_repository *repo, git_cvar_cached cvar) @@ -69,12 +82,16 @@ int git_repository__cvar(int *out, git_repository *repo, git_cvar_cached cvar) if (error < 0) return error; - error = git_config_get_mapped(out, - config, data->cvar_name, data->maps, data->map_count); + if (data->maps) + error = git_config_get_mapped( + out, config, data->cvar_name, data->maps, data->map_count); + else + error = git_config_get_bool(out, config, data->cvar_name); - if (error == GIT_ENOTFOUND) + if (error == GIT_ENOTFOUND) { + giterr_clear(); *out = data->default_value; - + } else if (error < 0) return error; diff --git a/src/config_file.c b/src/config_file.c index 8b51ab21b..1d7b4fb38 100644 --- a/src/config_file.c +++ b/src/config_file.c @@ -12,6 +12,7 @@ #include "buffer.h" #include "buf_text.h" #include "git2/config.h" +#include "git2/sys/config.h" #include "git2/types.h" #include "strmap.h" @@ -80,10 +81,10 @@ typedef struct { time_t file_mtime; size_t file_size; - unsigned int level; + git_config_level_t level; } diskfile_backend; -static int config_parse(diskfile_backend *cfg_file, unsigned int level); +static int config_parse(diskfile_backend *cfg_file, git_config_level_t level); static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_value); static int config_write(diskfile_backend *cfg, const char *key, const regex_t *preg, const char *value); static char *escape_value(const char *ptr); @@ -180,7 +181,7 @@ static void free_vars(git_strmap *values) git_strmap_free(values); } -static int config_open(git_config_backend *cfg, unsigned int level) +static int config_open(git_config_backend *cfg, git_config_level_t level) { int res; diskfile_backend *b = (diskfile_backend *)cfg; @@ -295,7 +296,7 @@ cleanup: static int config_set(git_config_backend *cfg, const char *name, const char *value) { - cvar_t *var = NULL, *old_var; + cvar_t *var = NULL, *old_var = NULL; diskfile_backend *b = (diskfile_backend *)cfg; char *key, *esc_value = NULL; khiter_t pos; @@ -481,8 +482,10 @@ static int config_set_multivar( pos = git_strmap_lookup_index(b->values, key); if (!git_strmap_valid_index(b->values, pos)) { + /* If we don't have it, behave like a normal set */ + result = config_set(cfg, name, value); git__free(key); - return GIT_ENOTFOUND; + return result; } var = git_strmap_value_at(b->values, pos); @@ -789,6 +792,11 @@ static int parse_section_header_ext(diskfile_backend *cfg, const char *line, con } switch (c) { + case 0: + set_parse_error(cfg, 0, "Unexpected end-of-line in section header"); + git_buf_free(&buf); + return -1; + case '"': ++quote_marks; continue; @@ -798,6 +806,12 @@ static int parse_section_header_ext(diskfile_backend *cfg, const char *line, con switch (c) { case '"': + if (&line[rpos-1] == last_quote) { + set_parse_error(cfg, 0, "Missing closing quotation mark in section header"); + git_buf_free(&buf); + return -1; + } + case '\\': break; @@ -962,7 +976,7 @@ static int strip_comments(char *line, int in_quotes) return quote_count; } -static int config_parse(diskfile_backend *cfg_file, unsigned int level) +static int config_parse(diskfile_backend *cfg_file, git_config_level_t level) { int c; char *current_section = NULL; @@ -1290,6 +1304,9 @@ static char *escape_value(const char *ptr) assert(ptr); len = strlen(ptr); + if (!len) + return git__calloc(1, sizeof(char)); + git_buf_grow(&buf, len); while (*ptr != '\0') { @@ -1392,7 +1409,7 @@ static int parse_multiline_variable(diskfile_backend *cfg, git_buf *value, int i * standard, this character **has** to be last one in the buf, with * no whitespace after it */ assert(is_multiline_var(value->ptr)); - git_buf_truncate(value, git_buf_len(value) - 1); + git_buf_shorten(value, 1); proc_line = fixup_line(line, in_quotes); if (proc_line == NULL) { diff --git a/src/crlf.c b/src/crlf.c index 81268da83..65039f9cc 100644 --- a/src/crlf.c +++ b/src/crlf.c @@ -5,14 +5,16 @@ * a Linking Exception. For full terms see the included COPYING file. */ +#include "git2/attr.h" +#include "git2/blob.h" +#include "git2/index.h" + #include "common.h" #include "fileops.h" #include "hash.h" #include "filter.h" #include "buf_text.h" #include "repository.h" -#include "git2/attr.h" -#include "git2/blob.h" struct crlf_attrs { int crlf_action; diff --git a/src/date.c b/src/date.c index ce1721a0b..48841e4f9 100644 --- a/src/date.c +++ b/src/date.c @@ -823,8 +823,8 @@ static void pending_number(struct tm *tm, int *num) } static git_time_t approxidate_str(const char *date, - const struct timeval *tv, - int *error_ret) + const struct timeval *tv, + int *error_ret) { int number = 0; int touched = 0; @@ -866,7 +866,7 @@ int git__date_parse(git_time_t *out, const char *date) int offset, error_ret=0; if (!parse_date_basic(date, ×tamp, &offset)) { - *out = timestamp; + *out = timestamp; return 0; } diff --git a/src/delta.c b/src/delta.c index 3252dbf14..b3435ba87 100644 --- a/src/delta.c +++ b/src/delta.c @@ -168,7 +168,7 @@ git_delta_create_index(const void *buf, unsigned long bufsize) memset(hash, 0, hsize * sizeof(*hash)); /* allocate an array to count hash entries */ - hash_count = calloc(hsize, sizeof(*hash_count)); + hash_count = git__calloc(hsize, sizeof(*hash_count)); if (!hash_count) { git__free(index); return NULL; diff --git a/src/diff.c b/src/diff.c index 37c89f3f1..e875d09b3 100644 --- a/src/diff.c +++ b/src/diff.c @@ -11,9 +11,14 @@ #include "attr_file.h" #include "filter.h" #include "pathspec.h" +#include "index.h" +#include "odb.h" +#include "submodule.h" #define DIFF_FLAG_IS_SET(DIFF,FLAG) (((DIFF)->opts.flags & (FLAG)) != 0) #define DIFF_FLAG_ISNT_SET(DIFF,FLAG) (((DIFF)->opts.flags & (FLAG)) == 0) +#define DIFF_FLAG_SET(DIFF,FLAG,VAL) (DIFF)->opts.flags = \ + (VAL) ? ((DIFF)->opts.flags | (FLAG)) : ((DIFF)->opts.flags & ~(VAL)) static git_diff_delta *diff_delta__alloc( git_diff_list *diff, @@ -73,15 +78,11 @@ static int diff_delta__from_one( DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNTRACKED)) return 0; - if (entry->mode == GIT_FILEMODE_COMMIT && - DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) - return 0; - - if (!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); @@ -130,16 +131,12 @@ static int diff_delta__from_two( { git_diff_delta *delta; int notify_res; + const char *canonical_path = old_entry->path; if (status == GIT_DELTA_UNMODIFIED && DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNMODIFIED)) return 0; - if (old_entry->mode == GIT_FILEMODE_COMMIT && - new_entry->mode == GIT_FILEMODE_COMMIT && - DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) - return 0; - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { uint32_t temp_mode = old_mode; const git_index_entry *temp_entry = old_entry; @@ -149,7 +146,7 @@ static int diff_delta__from_two( new_mode = temp_mode; } - delta = diff_delta__alloc(diff, status, old_entry->path); + delta = diff_delta__alloc(diff, status, canonical_path); GITERR_CHECK_ALLOC(delta); git_oid_cpy(&delta->old_file.oid, &old_entry->oid); @@ -194,21 +191,21 @@ static git_diff_delta *diff_delta__last_for_item( switch (delta->status) { case GIT_DELTA_UNMODIFIED: case GIT_DELTA_DELETED: - if (git_oid_cmp(&delta->old_file.oid, &item->oid) == 0) + if (git_oid__cmp(&delta->old_file.oid, &item->oid) == 0) return delta; break; case GIT_DELTA_ADDED: - if (git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) + if (git_oid__cmp(&delta->new_file.oid, &item->oid) == 0) return delta; break; case GIT_DELTA_UNTRACKED: if (diff->strcomp(delta->new_file.path, item->path) == 0 && - git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) + git_oid__cmp(&delta->new_file.oid, &item->oid) == 0) return delta; break; case GIT_DELTA_MODIFIED: - if (git_oid_cmp(&delta->old_file.oid, &item->oid) == 0 || - git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) + if (git_oid__cmp(&delta->old_file.oid, &item->oid) == 0 || + git_oid__cmp(&delta->new_file.oid, &item->oid) == 0) return delta; break; default: @@ -229,10 +226,35 @@ static char *diff_strdup_prefix(git_pool *pool, const char *prefix) return git_pool_strndup(pool, prefix, len + 1); } +GIT_INLINE(const char *) diff_delta__path(const git_diff_delta *delta) +{ + const char *str = delta->old_file.path; + + if (!str || + delta->status == GIT_DELTA_ADDED || + delta->status == GIT_DELTA_RENAMED || + delta->status == GIT_DELTA_COPIED) + str = delta->new_file.path; + + 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; - int val = strcmp(da->old_file.path, db->old_file.path); + int val = strcmp(diff_delta__path(da), diff_delta__path(db)); + return val ? val : ((int)da->status - (int)db->status); +} + +int git_diff_delta__casecmp(const void *a, const void *b) +{ + const git_diff_delta *da = a, *db = b; + int val = strcasecmp(diff_delta__path(da), diff_delta__path(db)); return val ? val : ((int)da->status - (int)db->status); } @@ -267,67 +289,176 @@ static int config_bool(git_config *cfg, const char *name, int defvalue) return val; } -static git_diff_list *git_diff_list_alloc( - git_repository *repo, const git_diff_options *opts) +static int config_int(git_config *cfg, const char *name, int defvalue) { - git_config *cfg; + int val = defvalue; + + if (git_config_get_int32(&val, cfg, name) < 0) + giterr_clear(); + + return val; +} + +static const char *diff_mnemonic_prefix( + git_iterator_type_t type, bool left_side) +{ + const char *pfx = ""; + + switch (type) { + case GIT_ITERATOR_TYPE_EMPTY: pfx = "c"; break; + case GIT_ITERATOR_TYPE_TREE: pfx = "c"; break; + case GIT_ITERATOR_TYPE_INDEX: pfx = "i"; break; + case GIT_ITERATOR_TYPE_WORKDIR: pfx = "w"; break; + case GIT_ITERATOR_TYPE_FS: pfx = left_side ? "1" : "2"; break; + default: break; + } + + /* note: without a deeper look at pathspecs, there is no easy way + * to get the (o)bject / (w)ork tree mnemonics working... + */ + + return pfx; +} + +static git_diff_list *diff_list_alloc( + git_repository *repo, + git_iterator *old_iter, + git_iterator *new_iter) +{ + git_diff_options dflt = GIT_DIFF_OPTIONS_INIT; git_diff_list *diff = git__calloc(1, sizeof(git_diff_list)); - if (diff == NULL) + if (!diff) return NULL; + assert(repo && old_iter && new_iter); + GIT_REFCOUNT_INC(diff); diff->repo = repo; + diff->old_src = old_iter->type; + diff->new_src = new_iter->type; + memcpy(&diff->opts, &dflt, sizeof(diff->opts)); if (git_vector_init(&diff->deltas, 0, git_diff_delta__cmp) < 0 || - git_pool_init(&diff->pool, 1, 0) < 0) - goto fail; + git_pool_init(&diff->pool, 1, 0) < 0) { + git_diff_list_free(diff); + return NULL; + } + + /* Use case-insensitive compare if either iterator has + * the ignore_case bit set */ + if (!git_iterator_ignore_case(old_iter) && + !git_iterator_ignore_case(new_iter)) { + diff->opts.flags &= ~GIT_DIFF_DELTAS_ARE_ICASE; + + diff->strcomp = git__strcmp; + diff->strncomp = git__strncmp; + diff->pfxcomp = git__prefixcmp; + diff->entrycomp = git_index_entry__cmp; + } else { + diff->opts.flags |= GIT_DIFF_DELTAS_ARE_ICASE; + + diff->strcomp = git__strcasecmp; + diff->strncomp = git__strncasecmp; + diff->pfxcomp = git__prefixcmp_icase; + diff->entrycomp = git_index_entry__cmp_icase; + + git_vector_set_cmp(&diff->deltas, git_diff_delta__casecmp); + } + + return diff; +} + +static int diff_list_apply_options( + git_diff_list *diff, + const git_diff_options *opts) +{ + git_config *cfg; + git_repository *repo = diff->repo; + git_pool *pool = &diff->pool; + int val; + + if (opts) { + /* copy user options (except case sensitivity info from iterators) */ + bool icase = DIFF_FLAG_IS_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE); + memcpy(&diff->opts, opts, sizeof(diff->opts)); + DIFF_FLAG_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE, icase); + + /* initialize pathspec from options */ + if (git_pathspec__vinit(&diff->pathspec, &opts->pathspec, pool) < 0) + return -1; + } + + /* flag INCLUDE_TYPECHANGE_TREES implies INCLUDE_TYPECHANGE */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES)) + diff->opts.flags |= GIT_DIFF_INCLUDE_TYPECHANGE; + + /* flag INCLUDE_UNTRACKED_CONTENT implies INCLUDE_UNTRACKED */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_UNTRACKED_CONTENT)) + diff->opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED; /* load config values that affect diff behavior */ if (git_repository_config__weakptr(&cfg, repo) < 0) - goto fail; - if (config_bool(cfg, "core.symlinks", 1)) + return -1; + + if (!git_repository__cvar(&val, repo, GIT_CVAR_SYMLINKS) && val) diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_HAS_SYMLINKS; - if (config_bool(cfg, "core.ignorestat", 0)) + + if (!git_repository__cvar(&val, repo, GIT_CVAR_IGNORESTAT) && val) diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_ASSUME_UNCHANGED; - if (config_bool(cfg, "core.filemode", 1)) + + if ((diff->opts.flags & GIT_DIFF_IGNORE_FILEMODE) == 0 && + !git_repository__cvar(&val, repo, GIT_CVAR_FILEMODE) && val) diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_TRUST_MODE_BITS; - if (config_bool(cfg, "core.trustctime", 1)) + + if (!git_repository__cvar(&val, repo, GIT_CVAR_TRUSTCTIME) && val) diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_TRUST_CTIME; + /* Don't set GIT_DIFFCAPS_USE_DEV - compile time option in core git */ - /* TODO: there are certain config settings where even if we were - * not given an options structure, we need the diff list to have one - * so that we can store the altered default values. - * - * - diff.ignoreSubmodules - * - diff.mnemonicprefix - * - diff.noprefix - */ + /* Set GIT_DIFFCAPS_TRUST_NANOSECS on a platform basis */ + diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_TRUST_NANOSECS; + + /* If not given explicit `opts`, check `diff.xyz` configs */ + if (!opts) { + diff->opts.context_lines = config_int(cfg, "diff.context", 3); - if (opts == NULL) { - /* Make sure we default to 3 lines */ - diff->opts.context_lines = 3; - return diff; + /* add other defaults here */ } - memcpy(&diff->opts, opts, sizeof(git_diff_options)); + /* if ignore_submodules not explicitly set, check diff config */ + if (diff->opts.ignore_submodules <= 0) { + const char *str; - if(opts->flags & GIT_DIFF_IGNORE_FILEMODE) - diff->diffcaps = diff->diffcaps & ~GIT_DIFFCAPS_TRUST_MODE_BITS; + if (git_config_get_string(&str , cfg, "diff.ignoreSubmodules") < 0) + giterr_clear(); + else if (str != NULL && + git_submodule_parse_ignore(&diff->opts.ignore_submodules, str) < 0) + giterr_clear(); + } - /* pathspec init will do nothing for empty pathspec */ - if (git_pathspec_init(&diff->pathspec, &opts->pathspec, &diff->pool) < 0) - goto fail; + /* if either prefix is not set, figure out appropriate value */ + if (!diff->opts.old_prefix || !diff->opts.new_prefix) { + const char *use_old = DIFF_OLD_PREFIX_DEFAULT; + const char *use_new = DIFF_NEW_PREFIX_DEFAULT; - /* TODO: handle config diff.mnemonicprefix, diff.noprefix */ + if (config_bool(cfg, "diff.noprefix", 0)) { + use_old = use_new = ""; + } else if (config_bool(cfg, "diff.mnemonicprefix", 0)) { + use_old = diff_mnemonic_prefix(diff->old_src, true); + use_new = diff_mnemonic_prefix(diff->new_src, false); + } - diff->opts.old_prefix = diff_strdup_prefix(&diff->pool, - opts->old_prefix ? opts->old_prefix : DIFF_OLD_PREFIX_DEFAULT); - diff->opts.new_prefix = diff_strdup_prefix(&diff->pool, - opts->new_prefix ? opts->new_prefix : DIFF_NEW_PREFIX_DEFAULT); + if (!diff->opts.old_prefix) + diff->opts.old_prefix = use_old; + if (!diff->opts.new_prefix) + diff->opts.new_prefix = use_new; + } + /* strdup prefix from pool so we're not dependent on external data */ + diff->opts.old_prefix = diff_strdup_prefix(pool, diff->opts.old_prefix); + diff->opts.new_prefix = diff_strdup_prefix(pool, diff->opts.new_prefix); if (!diff->opts.old_prefix || !diff->opts.new_prefix) - goto fail; + return -1; if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { const char *swap = diff->opts.old_prefix; @@ -335,15 +466,7 @@ static git_diff_list *git_diff_list_alloc( diff->opts.new_prefix = swap; } - /* INCLUDE_TYPECHANGE_TREES implies INCLUDE_TYPECHANGE */ - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES)) - diff->opts.flags |= GIT_DIFF_INCLUDE_TYPECHANGE; - - return diff; - -fail: - git_diff_list_free(diff); - return NULL; + return 0; } static void diff_list_free(git_diff_list *diff) @@ -357,8 +480,10 @@ 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)); git__free(diff); } @@ -445,31 +570,95 @@ cleanup: return result; } +static bool diff_time_eq( + const git_index_time *a, const git_index_time *b, bool use_nanos) +{ + return a->seconds == b->seconds && + (!use_nanos || a->nanoseconds == b->nanoseconds); +} + +typedef struct { + git_repository *repo; + git_iterator *old_iter; + git_iterator *new_iter; + const git_index_entry *oitem; + const git_index_entry *nitem; + git_buf ignore_prefix; +} diff_in_progress; + #define MODE_BITS_MASK 0000777 +static int maybe_modified_submodule( + git_delta_t *status, + git_oid *found_oid, + git_diff_list *diff, + diff_in_progress *info) +{ + int error = 0; + git_submodule *sub; + unsigned int sm_status = 0; + git_submodule_ignore_t ign = diff->opts.ignore_submodules; + + *status = GIT_DELTA_UNMODIFIED; + + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES) || + ign == GIT_SUBMODULE_IGNORE_ALL) + return 0; + + if ((error = git_submodule_lookup( + &sub, diff->repo, info->nitem->path)) < 0) { + + /* GIT_EEXISTS means dir with .git in it was found - ignore it */ + if (error == GIT_EEXISTS) { + giterr_clear(); + error = 0; + } + return error; + } + + if (ign <= 0 && git_submodule_ignore(sub) == GIT_SUBMODULE_IGNORE_ALL) + return 0; + + if ((error = git_submodule__status( + &sm_status, NULL, NULL, found_oid, sub, ign)) < 0) + return error; + + /* check IS_WD_UNMODIFIED because this case is only used + * when the new side of the diff is the working directory + */ + if (!GIT_SUBMODULE_STATUS_IS_WD_UNMODIFIED(sm_status)) + *status = GIT_DELTA_MODIFIED; + + /* now that we have a HEAD OID, check if HEAD moved */ + if ((sm_status & GIT_SUBMODULE_STATUS_IN_WD) != 0 && + !git_oid_equal(&info->oitem->oid, found_oid)) + *status = GIT_DELTA_MODIFIED; + + return 0; +} + static int maybe_modified( - git_iterator *old_iter, - const git_index_entry *oitem, - git_iterator *new_iter, - const git_index_entry *nitem, - git_diff_list *diff) + git_diff_list *diff, + diff_in_progress *info) { - git_oid noid, *use_noid = NULL; + git_oid noid; git_delta_t status = GIT_DELTA_MODIFIED; + const git_index_entry *oitem = info->oitem; + const git_index_entry *nitem = info->nitem; unsigned int omode = oitem->mode; unsigned int nmode = nitem->mode; - bool new_is_workdir = (new_iter->type == GIT_ITERATOR_TYPE_WORKDIR); + bool new_is_workdir = (info->new_iter->type == GIT_ITERATOR_TYPE_WORKDIR); const char *matched_pathspec; - GIT_UNUSED(old_iter); - - 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)); + /* on platforms with no symlinks, preserve mode of existing symlinks */ if (S_ISLNK(omode) && S_ISREG(nmode) && new_is_workdir && !(diff->diffcaps & GIT_DIFFCAPS_HAS_SYMLINKS)) @@ -502,63 +691,40 @@ static int maybe_modified( } } - /* if oids and modes match, then file is unmodified */ - else if (git_oid_equal(&oitem->oid, &nitem->oid) && omode == nmode) + /* if oids and modes match (and are valid), then file is unmodified */ + else if (git_oid_equal(&oitem->oid, &nitem->oid) && + omode == nmode && + !git_oid_iszero(&oitem->oid)) status = GIT_DELTA_UNMODIFIED; /* if we have an unknown OID and a workdir iterator, then check some * circumstances that can accelerate things or need special handling */ else if (git_oid_iszero(&nitem->oid) && new_is_workdir) { - /* TODO: add check against index file st_mtime to avoid racy-git */ + bool use_ctime = ((diff->diffcaps & GIT_DIFFCAPS_TRUST_CTIME) != 0); + bool use_nanos = ((diff->diffcaps & GIT_DIFFCAPS_TRUST_NANOSECS) != 0); - /* if the stat data looks exactly alike, then assume the same */ - if (omode == nmode && - oitem->file_size == nitem->file_size && - (!(diff->diffcaps & GIT_DIFFCAPS_TRUST_CTIME) || - (oitem->ctime.seconds == nitem->ctime.seconds)) && - oitem->mtime.seconds == nitem->mtime.seconds && - (!(diff->diffcaps & GIT_DIFFCAPS_USE_DEV) || - (oitem->dev == nitem->dev)) && - oitem->ino == nitem->ino && - oitem->uid == nitem->uid && - oitem->gid == nitem->gid) - status = GIT_DELTA_UNMODIFIED; + status = GIT_DELTA_UNMODIFIED; - else if (S_ISGITLINK(nmode)) { - int err; - git_submodule *sub; - - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) - status = GIT_DELTA_UNMODIFIED; - else if ((err = git_submodule_lookup(&sub, diff->repo, nitem->path)) < 0) { - if (err == GIT_EEXISTS) - status = GIT_DELTA_UNMODIFIED; - else - return err; - } else if (git_submodule_ignore(sub) == GIT_SUBMODULE_IGNORE_ALL) - status = GIT_DELTA_UNMODIFIED; - else { - unsigned int sm_status = 0; - if (git_submodule_status(&sm_status, sub) < 0) - return -1; - - /* check IS_WD_UNMODIFIED because this case is only used - * when the new side of the diff is the working directory - */ - status = GIT_SUBMODULE_STATUS_IS_WD_UNMODIFIED(sm_status) - ? GIT_DELTA_UNMODIFIED : GIT_DELTA_MODIFIED; - - /* grab OID while we are here */ - if (git_oid_iszero(&nitem->oid)) { - const git_oid *sm_oid = git_submodule_wd_id(sub); - if (sm_oid != NULL) { - git_oid_cpy(&noid, sm_oid); - use_noid = &noid; - } - } - } + /* TODO: add check against index file st_mtime to avoid racy-git */ + + if (S_ISGITLINK(nmode)) { + if (maybe_modified_submodule(&status, &noid, diff, info) < 0) + return -1; } + + /* if the stat data looks different, then mark modified - this just + * means that the OID will be recalculated below to confirm change + */ + else if (omode != nmode || + oitem->file_size != nitem->file_size || + !diff_time_eq(&oitem->mtime, &nitem->mtime, use_nanos) || + (use_ctime && + !diff_time_eq(&oitem->ctime, &nitem->ctime, use_nanos)) || + oitem->ino != nitem->ino || + oitem->uid != nitem->uid || + oitem->gid != nitem->gid) + status = GIT_DELTA_MODIFIED; } /* if mode is GITLINK and submodules are ignored, then skip */ @@ -569,12 +735,11 @@ static int maybe_modified( /* if we got here and decided that the files are modified, but we * haven't calculated the OID of the new item, then calculate it now */ - if (status != GIT_DELTA_UNMODIFIED && git_oid_iszero(&nitem->oid)) { - if (!use_noid) { + if (status == GIT_DELTA_MODIFIED && git_oid_iszero(&nitem->oid)) { + if (git_oid_iszero(&noid)) { if (git_diff__oid_for_file(diff->repo, nitem->path, nitem->mode, nitem->file_size, &noid) < 0) return -1; - use_noid = &noid; } /* if oid matches, then mark unmodified (except submodules, where @@ -582,12 +747,13 @@ static int maybe_modified( * matches between the index and the workdir HEAD) */ if (omode == nmode && !S_ISGITLINK(omode) && - git_oid_equal(&oitem->oid, use_noid)) + git_oid_equal(&oitem->oid, &noid)) status = GIT_DELTA_UNMODIFIED; } return diff_delta__from_two( - diff, status, oitem, omode, nitem, nmode, use_noid, matched_pathspec); + diff, status, oitem, omode, nitem, nmode, + git_oid_iszero(&noid) ? NULL : &noid, matched_pathspec); } static bool entry_is_prefixed( @@ -607,237 +773,347 @@ static bool entry_is_prefixed( item->path[pathlen] == '/'); } -static int diff_list_init_from_iterators( - git_diff_list *diff, - git_iterator *old_iter, - git_iterator *new_iter) +static int diff_scan_inside_untracked_dir( + git_diff_list *diff, diff_in_progress *info, git_delta_t *delta_type) { - diff->old_src = old_iter->type; - diff->new_src = new_iter->type; + int error = 0; + git_buf base = GIT_BUF_INIT; + bool is_ignored; - /* Use case-insensitive compare if either iterator has - * the ignore_case bit set */ - if (!git_iterator_ignore_case(old_iter) && - !git_iterator_ignore_case(new_iter)) - { - diff->opts.flags &= ~GIT_DIFF_DELTAS_ARE_ICASE; + *delta_type = GIT_DELTA_IGNORED; + git_buf_sets(&base, info->nitem->path); - diff->strcomp = git__strcmp; - diff->strncomp = git__strncmp; - diff->pfxcomp = git__prefixcmp; - diff->entrycomp = git_index_entry__cmp; - } else { - diff->opts.flags |= GIT_DIFF_DELTAS_ARE_ICASE; + /* advance into untracked directory */ + if ((error = git_iterator_advance_into(&info->nitem, info->new_iter)) < 0) { - diff->strcomp = git__strcasecmp; - diff->strncomp = git__strncasecmp; - diff->pfxcomp = git__prefixcmp_icase; - diff->entrycomp = git_index_entry__cmp_icase; + /* skip ahead if empty */ + if (error == GIT_ENOTFOUND) { + giterr_clear(); + error = git_iterator_advance(&info->nitem, info->new_iter); + } + + goto done; } - return 0; + /* look for actual untracked file */ + while (info->nitem != NULL && + !diff->pfxcomp(info->nitem->path, git_buf_cstr(&base))) { + is_ignored = git_iterator_current_is_ignored(info->new_iter); + + /* need to recurse into non-ignored directories */ + if (!is_ignored && S_ISDIR(info->nitem->mode)) { + error = git_iterator_advance_into(&info->nitem, info->new_iter); + + if (!error) + continue; + else if (error == GIT_ENOTFOUND) { + error = 0; + is_ignored = true; /* treat empty as ignored */ + } else + break; /* real error, must stop */ + } + + /* found a non-ignored item - treat parent dir as untracked */ + if (!is_ignored) { + *delta_type = GIT_DELTA_UNTRACKED; + break; + } + + if ((error = git_iterator_advance(&info->nitem, info->new_iter)) < 0) + break; + } + + /* finish off scan */ + while (info->nitem != NULL && + !diff->pfxcomp(info->nitem->path, git_buf_cstr(&base))) { + if ((error = git_iterator_advance(&info->nitem, info->new_iter)) < 0) + break; + } + +done: + git_buf_free(&base); + + if (error == GIT_ITEROVER) + error = 0; + + return error; } -int git_diff__from_iterators( - git_diff_list **diff_ptr, - git_repository *repo, - git_iterator *old_iter, - git_iterator *new_iter, - const git_diff_options *opts) +static int handle_unmatched_new_item( + git_diff_list *diff, diff_in_progress *info) { int error = 0; - const git_index_entry *oitem, *nitem; - git_buf ignore_prefix = GIT_BUF_INIT; - git_diff_list *diff = git_diff_list_alloc(repo, opts); - - *diff_ptr = NULL; + const git_index_entry *nitem = info->nitem; + git_delta_t delta_type = GIT_DELTA_UNTRACKED; + bool contains_oitem; - if (!diff || diff_list_init_from_iterators(diff, old_iter, new_iter) < 0) - goto fail; + /* check if this is a prefix of the other side */ + contains_oitem = entry_is_prefixed(diff, info->oitem, nitem); - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE)) { - if (git_iterator_set_ignore_case(old_iter, true) < 0 || - git_iterator_set_ignore_case(new_iter, true) < 0) - goto fail; + /* check if this is contained in an ignored parent directory */ + if (git_buf_len(&info->ignore_prefix)) { + if (diff->pfxcomp(nitem->path, git_buf_cstr(&info->ignore_prefix)) == 0) + delta_type = GIT_DELTA_IGNORED; + else + git_buf_clear(&info->ignore_prefix); } - if (git_iterator_current(&oitem, old_iter) < 0 || - git_iterator_current(&nitem, new_iter) < 0) - goto fail; + if (nitem->mode == GIT_FILEMODE_TREE) { + bool recurse_into_dir = contains_oitem; - /* run iterators building diffs */ - while (oitem || nitem) { - int cmp = oitem ? (nitem ? diff->entrycomp(oitem, nitem) : -1) : 1; + /* if not already inside an ignored dir, check if this is ignored */ + if (delta_type != GIT_DELTA_IGNORED && + git_iterator_current_is_ignored(info->new_iter)) { + delta_type = GIT_DELTA_IGNORED; + git_buf_sets(&info->ignore_prefix, nitem->path); + } - /* create DELETED records for old items not matched in new */ - if (cmp < 0) { - if (diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem) < 0) - goto fail; + /* check if user requests recursion into this type of dir */ + recurse_into_dir = contains_oitem || + (delta_type == GIT_DELTA_UNTRACKED && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) || + (delta_type == GIT_DELTA_IGNORED && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)); + + /* do not advance into directories that contain a .git file */ + if (recurse_into_dir) { + git_buf *full = NULL; + if (git_iterator_current_workdir_path(&full, info->new_iter) < 0) + return -1; + if (full && git_path_contains_dir(full, DOT_GIT)) + recurse_into_dir = false; + } - /* if we are generating TYPECHANGE records then check for that - * instead of just generating a DELETE record - */ - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) && - entry_is_prefixed(diff, nitem, oitem)) - { - /* this entry has become a tree! convert to TYPECHANGE */ - git_diff_delta *last = diff_delta__last_for_item(diff, oitem); - if (last) { - last->status = GIT_DELTA_TYPECHANGE; - last->new_file.mode = GIT_FILEMODE_TREE; - } + /* still have to look into untracked directories to match core git - + * with no untracked files, directory is treated as ignored + */ + if (!recurse_into_dir && + delta_type == GIT_DELTA_UNTRACKED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_FAST_UNTRACKED_DIRS)) + { + git_diff_delta *last; + + /* attempt to insert record for this directory */ + if ((error = diff_delta__from_one(diff, delta_type, nitem)) < 0) + return error; + + /* if delta wasn't created (because of rules), just skip ahead */ + last = diff_delta__last_for_item(diff, nitem); + if (!last) + return git_iterator_advance(&info->nitem, info->new_iter); + + /* iterate into dir looking for an actual untracked file */ + if (diff_scan_inside_untracked_dir(diff, info, &delta_type) < 0) + return -1; + + /* it iteration changed delta type, the update the record */ + if (delta_type == GIT_DELTA_IGNORED) { + last->status = GIT_DELTA_IGNORED; - /* If new_iter is a workdir iterator, then this situation - * will certainly be followed by a series of untracked items. - * Unless RECURSE_UNTRACKED_DIRS is set, skip over them... - */ - if (S_ISDIR(nitem->mode) && - DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) - { - if (git_iterator_advance(&nitem, new_iter) < 0) - goto fail; + /* remove the record if we don't want ignored records */ + if (DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_IGNORED)) { + git_vector_pop(&diff->deltas); + git__free(last); } } - if (git_iterator_advance(&oitem, old_iter) < 0) - goto fail; + return 0; } - /* create ADDED, TRACKED, or IGNORED records for new items not - * matched in old (and/or descend into directories as needed) - */ - else if (cmp > 0) { - git_delta_t delta_type = GIT_DELTA_UNTRACKED; - bool contains_oitem = entry_is_prefixed(diff, oitem, nitem); - - /* check if contained in ignored parent directory */ - if (git_buf_len(&ignore_prefix) && - diff->pfxcomp(nitem->path, git_buf_cstr(&ignore_prefix)) == 0) - delta_type = GIT_DELTA_IGNORED; - - if (S_ISDIR(nitem->mode)) { - /* recurse into directory only if there are tracked items in - * it or if the user requested the contents of untracked - * directories and it is not under an ignored directory. - */ - bool recurse_into_dir = - (delta_type == GIT_DELTA_UNTRACKED && - DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) || - (delta_type == GIT_DELTA_IGNORED && - DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)); - - /* do not advance into directories that contain a .git file */ - if (!contains_oitem && recurse_into_dir) { - git_buf *full = NULL; - if (git_iterator_current_workdir_path(&full, new_iter) < 0) - goto fail; - if (git_path_contains_dir(full, DOT_GIT)) - recurse_into_dir = false; - } + /* try to advance into directory if necessary */ + if (recurse_into_dir) { + error = git_iterator_advance_into(&info->nitem, info->new_iter); - /* if directory is ignored, remember ignore_prefix */ - if ((contains_oitem || recurse_into_dir) && - delta_type == GIT_DELTA_UNTRACKED && - git_iterator_current_is_ignored(new_iter)) - { - git_buf_sets(&ignore_prefix, nitem->path); - delta_type = GIT_DELTA_IGNORED; - - /* skip recursion if we've just learned this is ignored */ - if (DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)) - recurse_into_dir = false; - } + /* if real error or no error, proceed with iteration */ + if (error != GIT_ENOTFOUND) + return error; + giterr_clear(); - if (contains_oitem || recurse_into_dir) { - /* advance into directory */ - error = git_iterator_advance_into(&nitem, new_iter); + /* if directory is empty, can't advance into it, so either skip + * it or ignore it + */ + if (contains_oitem) + return git_iterator_advance(&info->nitem, info->new_iter); + delta_type = GIT_DELTA_IGNORED; + } + } - /* if directory is empty, can't advance into it, so skip */ - if (error == GIT_ENOTFOUND) { - giterr_clear(); - error = git_iterator_advance(&nitem, new_iter); + /* In core git, the next two checks are effectively reversed -- + * i.e. when an file contained in an ignored directory is explicitly + * ignored, it shows up as an ignored file in the diff list, even though + * other untracked files in the same directory are skipped completely. + * + * To me, this seems odd. If the directory is ignored and the file is + * untracked, we should skip it consistently, regardless of whether it + * happens to match a pattern in the ignore file. + * + * To match the core git behavior, reverse the following two if checks + * so that individual file ignores are checked before container + * directory exclusions are used to skip the file. + */ + else if (delta_type == GIT_DELTA_IGNORED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)) + /* item contained in ignored directory, so skip over it */ + return git_iterator_advance(&info->nitem, info->new_iter); - git_buf_clear(&ignore_prefix); - } + else if (git_iterator_current_is_ignored(info->new_iter)) + delta_type = GIT_DELTA_IGNORED; - if (error < 0) - goto fail; - continue; - } - } + else if (info->new_iter->type != GIT_ITERATOR_TYPE_WORKDIR) + delta_type = GIT_DELTA_ADDED; - /* In core git, the next two "else if" clauses are effectively - * reversed -- i.e. when an untracked file contained in an - * ignored directory is individually ignored, it shows up as an - * ignored file in the diff list, even though other untracked - * files in the same directory are skipped completely. - * - * To me, this is odd. If the directory is ignored and the file - * is untracked, we should skip it consistently, regardless of - * whether it happens to match a pattern in the ignore file. - * - * To match the core git behavior, just reverse the following - * two "else if" cases so that individual file ignores are - * checked before container directory exclusions are used to - * skip the file. - */ - else if (delta_type == GIT_DELTA_IGNORED && - DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)) { - if (git_iterator_advance(&nitem, new_iter) < 0) - goto fail; - continue; /* ignored parent directory, so skip completely */ - } + else if (nitem->mode == GIT_FILEMODE_COMMIT) { + git_submodule *sm; - else if (git_iterator_current_is_ignored(new_iter)) - delta_type = GIT_DELTA_IGNORED; + /* ignore things that are not actual submodules */ + if (git_submodule_lookup(&sm, info->repo, nitem->path) != 0) { + giterr_clear(); + delta_type = GIT_DELTA_IGNORED; + } + } - else if (new_iter->type != GIT_ITERATOR_TYPE_WORKDIR) - delta_type = GIT_DELTA_ADDED; + /* Actually create the record for this item if necessary */ + if ((error = diff_delta__from_one(diff, delta_type, nitem)) < 0) + return error; - if (diff_delta__from_one(diff, delta_type, nitem) < 0) - goto fail; + /* If user requested TYPECHANGE records, then check for that instead of + * just generating an ADDED/UNTRACKED record + */ + if (delta_type != GIT_DELTA_IGNORED && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) && + contains_oitem) + { + /* this entry was prefixed with a tree - make TYPECHANGE */ + git_diff_delta *last = diff_delta__last_for_item(diff, nitem); + if (last) { + last->status = GIT_DELTA_TYPECHANGE; + last->old_file.mode = GIT_FILEMODE_TREE; + } + } - /* if we are generating TYPECHANGE records then check for that - * instead of just generating an ADDED/UNTRACKED record - */ - if (delta_type != GIT_DELTA_IGNORED && - DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) && - contains_oitem) - { - /* this entry was prefixed with a tree - make TYPECHANGE */ - git_diff_delta *last = diff_delta__last_for_item(diff, nitem); - if (last) { - last->status = GIT_DELTA_TYPECHANGE; - last->old_file.mode = GIT_FILEMODE_TREE; - } - } + return git_iterator_advance(&info->nitem, info->new_iter); +} + +static int handle_unmatched_old_item( + git_diff_list *diff, diff_in_progress *info) +{ + int error = diff_delta__from_one(diff, GIT_DELTA_DELETED, info->oitem); + if (error < 0) + return error; - if (git_iterator_advance(&nitem, new_iter) < 0) - goto fail; + /* if we are generating TYPECHANGE records then check for that + * instead of just generating a DELETE record + */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) && + entry_is_prefixed(diff, info->nitem, info->oitem)) + { + /* this entry has become a tree! convert to TYPECHANGE */ + git_diff_delta *last = diff_delta__last_for_item(diff, info->oitem); + if (last) { + last->status = GIT_DELTA_TYPECHANGE; + last->new_file.mode = GIT_FILEMODE_TREE; } + /* If new_iter is a workdir iterator, then this situation + * will certainly be followed by a series of untracked items. + * Unless RECURSE_UNTRACKED_DIRS is set, skip over them... + */ + if (S_ISDIR(info->nitem->mode) && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) + return git_iterator_advance(&info->nitem, info->new_iter); + } + + return git_iterator_advance(&info->oitem, info->old_iter); +} + +static int handle_matched_item( + git_diff_list *diff, diff_in_progress *info) +{ + int error = 0; + + if ((error = maybe_modified(diff, info)) < 0) + return error; + + if (!(error = git_iterator_advance(&info->oitem, info->old_iter)) || + error == GIT_ITEROVER) + error = git_iterator_advance(&info->nitem, info->new_iter); + + return error; +} + +int git_diff__from_iterators( + git_diff_list **diff_ptr, + git_repository *repo, + git_iterator *old_iter, + git_iterator *new_iter, + const git_diff_options *opts) +{ + int error = 0; + diff_in_progress info; + git_diff_list *diff; + + *diff_ptr = NULL; + + diff = diff_list_alloc(repo, old_iter, new_iter); + GITERR_CHECK_ALLOC(diff); + + info.repo = repo; + info.old_iter = old_iter; + info.new_iter = new_iter; + git_buf_init(&info.ignore_prefix, 0); + + /* make iterators have matching icase behavior */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE)) { + if ((error = git_iterator_set_ignore_case(old_iter, true)) < 0 || + (error = git_iterator_set_ignore_case(new_iter, true)) < 0) + goto cleanup; + } + + /* finish initialization */ + if ((error = diff_list_apply_options(diff, opts)) < 0) + goto cleanup; + + if ((error = git_iterator_current(&info.oitem, old_iter)) < 0 && + error != GIT_ITEROVER) + goto cleanup; + if ((error = git_iterator_current(&info.nitem, new_iter)) < 0 && + error != GIT_ITEROVER) + goto cleanup; + error = 0; + + /* run iterators building diffs */ + while (!error && (info.oitem || info.nitem)) { + int cmp = info.oitem ? + (info.nitem ? diff->entrycomp(info.oitem, info.nitem) : -1) : 1; + + /* create DELETED records for old items not matched in new */ + if (cmp < 0) + error = handle_unmatched_old_item(diff, &info); + + /* create ADDED, TRACKED, or IGNORED records for new items not + * matched in old (and/or descend into directories as needed) + */ + else if (cmp > 0) + error = handle_unmatched_new_item(diff, &info); + /* otherwise item paths match, so create MODIFIED record * (or ADDED and DELETED pair if type changed) */ - else { - assert(oitem && nitem && cmp == 0); + else + error = handle_matched_item(diff, &info); - if (maybe_modified(old_iter, oitem, new_iter, nitem, diff) < 0 || - git_iterator_advance(&oitem, old_iter) < 0 || - git_iterator_advance(&nitem, new_iter) < 0) - goto fail; - } + /* because we are iterating over two lists, ignore ITEROVER */ + if (error == GIT_ITEROVER) + error = 0; } - *diff_ptr = diff; - -fail: - if (!*diff_ptr) { +cleanup: + if (!error) + *diff_ptr = diff; + else git_diff_list_free(diff); - error = -1; - } - git_buf_free(&ignore_prefix); + git_buf_free(&info.ignore_prefix); return error; } @@ -859,12 +1135,20 @@ int git_diff_tree_to_tree( const git_diff_options *opts) { int error = 0; + git_iterator_flag_t iflag = GIT_ITERATOR_DONT_IGNORE_CASE; assert(diff && repo); + /* for tree to tree diff, be case sensitive even if the index is + * currently case insensitive, unless the user explicitly asked + * for case insensitivity + */ + if (opts && (opts->flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0) + iflag = GIT_ITERATOR_IGNORE_CASE; + DIFF_FROM_ITERATORS( - git_iterator_for_tree(&a, old_tree, 0, pfx, pfx), - git_iterator_for_tree(&b, new_tree, 0, pfx, pfx) + git_iterator_for_tree(&a, old_tree, iflag, pfx, pfx), + git_iterator_for_tree(&b, new_tree, iflag, pfx, pfx) ); return error; @@ -878,17 +1162,40 @@ int git_diff_tree_to_index( const git_diff_options *opts) { int error = 0; + bool reset_index_ignore_case = false; assert(diff && repo); if (!index && (error = git_repository_index__weakptr(&index, repo)) < 0) return error; + if (index->ignore_case) { + git_index__set_ignore_case(index, false); + reset_index_ignore_case = true; + } + DIFF_FROM_ITERATORS( git_iterator_for_tree(&a, old_tree, 0, pfx, pfx), git_iterator_for_index(&b, index, 0, pfx, pfx) ); + if (reset_index_ignore_case) { + git_index__set_ignore_case(index, true); + + if (!error) { + git_diff_list *d = *diff; + + d->opts.flags |= GIT_DIFF_DELTAS_ARE_ICASE; + d->strcomp = git__strcasecmp; + d->strncomp = git__strncasecmp; + d->pfxcomp = git__prefixcmp_icase; + d->entrycomp = git_index_entry__cmp_icase; + + git_vector_set_cmp(&d->deltas, git_diff_delta__casecmp); + git_vector_sort(&d->deltas); + } + } + return error; } @@ -933,3 +1240,107 @@ int git_diff_tree_to_workdir( return error; } + +size_t git_diff_num_deltas(git_diff_list *diff) +{ + assert(diff); + return (size_t)diff->deltas.length; +} + +size_t git_diff_num_deltas_of_type(git_diff_list *diff, git_delta_t type) +{ + size_t i, count = 0; + git_diff_delta *delta; + + assert(diff); + + git_vector_foreach(&diff->deltas, i, delta) { + count += (delta->status == 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, + int (*cb)(git_diff_delta *h2i, git_diff_delta *i2w, void *payload), + void *payload) +{ + int cmp; + git_diff_delta *h2i, *i2w; + size_t i, j, i_max, j_max; + int (*strcomp)(const char *, const char *) = git__strcmp; + bool icase_mismatch; + + i_max = head2idx ? head2idx->deltas.length : 0; + j_max = idx2wd ? idx2wd->deltas.length : 0; + if (!i_max && !j_max) + return 0; + + /* At some point, tree-to-index diffs will probably never ignore case, + * even if that isn't true now. Index-to-workdir diffs may or may not + * ignore case, but the index filename for the idx2wd diff should + * still be using the canonical case-preserving name. + * + * Therefore the main thing we need to do here is make sure the diffs + * are traversed in a compatible order. To do this, we temporarily + * resort a mismatched diff to get the order correct. + */ + icase_mismatch = + (head2idx != NULL && idx2wd != NULL && + ((head2idx->opts.flags ^ idx2wd->opts.flags) & GIT_DIFF_DELTAS_ARE_ICASE)); + + /* force case-sensitive delta sort */ + if (icase_mismatch) { + if (head2idx->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) { + git_vector_set_cmp(&head2idx->deltas, git_diff_delta__cmp); + git_vector_sort(&head2idx->deltas); + } else { + git_vector_set_cmp(&idx2wd->deltas, git_diff_delta__cmp); + git_vector_sort(&idx2wd->deltas); + } + } + else if (head2idx != NULL && head2idx->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) + strcomp = git__strcasecmp; + + for (i = 0, j = 0; i < i_max || j < j_max; ) { + h2i = head2idx ? GIT_VECTOR_GET(&head2idx->deltas, i) : NULL; + i2w = idx2wd ? GIT_VECTOR_GET(&idx2wd->deltas, j) : NULL; + + cmp = !i2w ? -1 : !h2i ? 1 : + strcomp(h2i->new_file.path, i2w->old_file.path); + + if (cmp < 0) { + if (cb(h2i, NULL, payload)) + return GIT_EUSER; + i++; + } else if (cmp > 0) { + if (cb(NULL, i2w, payload)) + return GIT_EUSER; + j++; + } else { + if (cb(h2i, i2w, payload)) + return GIT_EUSER; + i++; j++; + } + } + + /* restore case-insensitive delta sort */ + if (icase_mismatch) { + if (head2idx->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) { + git_vector_set_cmp(&head2idx->deltas, git_diff_delta__casecmp); + git_vector_sort(&head2idx->deltas); + } else { + git_vector_set_cmp(&idx2wd->deltas, git_diff_delta__casecmp); + git_vector_sort(&idx2wd->deltas); + } + } + + return 0; +} diff --git a/src/diff.h b/src/diff.h index 8e3cbcd46..bec7e27d7 100644 --- a/src/diff.h +++ b/src/diff.h @@ -16,6 +16,7 @@ #include "iterator.h" #include "repository.h" #include "pool.h" +#include "odb.h" #define DIFF_OLD_PREFIX_DEFAULT "a/" #define DIFF_NEW_PREFIX_DEFAULT "b/" @@ -26,17 +27,31 @@ enum { GIT_DIFFCAPS_TRUST_MODE_BITS = (1 << 2), /* use st_mode? */ GIT_DIFFCAPS_TRUST_CTIME = (1 << 3), /* use st_ctime? */ GIT_DIFFCAPS_USE_DEV = (1 << 4), /* use st_dev? */ + GIT_DIFFCAPS_TRUST_NANOSECS = (1 << 5), /* use stat time nanoseconds */ }; +#define DIFF_FLAGS_KNOWN_BINARY (GIT_DIFF_FLAG_BINARY|GIT_DIFF_FLAG_NOT_BINARY) +#define DIFF_FLAGS_NOT_BINARY (GIT_DIFF_FLAG_NOT_BINARY|GIT_DIFF_FLAG__NO_DATA) + enum { GIT_DIFF_FLAG__FREE_PATH = (1 << 7), /* `path` is allocated memory */ GIT_DIFF_FLAG__FREE_DATA = (1 << 8), /* internal file data is allocated */ GIT_DIFF_FLAG__UNMAP_DATA = (1 << 9), /* internal file data is mmap'ed */ GIT_DIFF_FLAG__NO_DATA = (1 << 10), /* file data should not be loaded */ - GIT_DIFF_FLAG__TO_DELETE = (1 << 11), /* delete entry during rename det. */ - GIT_DIFF_FLAG__TO_SPLIT = (1 << 12), /* split entry during rename det. */ + GIT_DIFF_FLAG__FREE_BLOB = (1 << 11), /* release the blob when done */ + GIT_DIFF_FLAG__LOADED = (1 << 12), /* file data has been loaded */ + + GIT_DIFF_FLAG__TO_DELETE = (1 << 16), /* delete entry during rename det. */ + GIT_DIFF_FLAG__TO_SPLIT = (1 << 17), /* split entry during rename det. */ + GIT_DIFF_FLAG__IS_RENAME_TARGET = (1 << 18), + GIT_DIFF_FLAG__IS_RENAME_SOURCE = (1 << 19), + GIT_DIFF_FLAG__HAS_SELF_SIMILARITY = (1 << 20), }; +#define GIT_DIFF_FLAG__CLEAR_INTERNAL(F) (F) = ((F) & 0x00FFFF) + +#define GIT_DIFF__VERBOSE (1 << 30) + struct git_diff_list { git_refcount rc; git_repository *repo; @@ -60,10 +75,20 @@ extern void git_diff__cleanup_modes( 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); +extern int git_diff_delta__format_file_header( + git_buf *out, + const git_diff_delta *delta, + const char *oldpfx, + const char *newpfx, + int oid_strlen); + extern int git_diff__oid_for_file( git_repository *, const char *, uint16_t, git_off_t, git_oid *); @@ -74,5 +99,50 @@ extern int git_diff__from_iterators( git_iterator *new_iter, const git_diff_options *opts); +extern int git_diff__paired_foreach( + git_diff_list *idx2head, + git_diff_list *wd2idx, + int (*cb)(git_diff_delta *i2h, git_diff_delta *w2i, void *payload), + void *payload); + +extern int git_diff_find_similar__hashsig_for_file( + void **out, const git_diff_file *f, const char *path, void *p); + +extern int git_diff_find_similar__hashsig_for_buf( + void **out, const git_diff_file *f, const char *buf, size_t len, void *p); + +extern void git_diff_find_similar__hashsig_free(void *sig, void *payload); + +extern int git_diff_find_similar__calc_similarity( + int *score, void *siga, void *sigb, void *payload); + +/* + * Sometimes a git_diff_file will have a zero size; this attempts to + * fill in the size without loading the blob if possible. If that is + * not possible, then it will return the git_odb_object that had to be + * loaded and the caller can use it or dispose of it as needed. + */ +GIT_INLINE(int) git_diff_file__resolve_zero_size( + git_diff_file *file, git_odb_object **odb_obj, git_repository *repo) +{ + int error; + git_odb *odb; + size_t len; + git_otype type; + + if ((error = git_repository_odb(&odb, repo)) < 0) + return error; + + error = git_odb__read_header_or_object( + odb_obj, &len, &type, odb, &file->oid); + + git_odb_free(odb); + + if (!error) + file->size = (git_off_t)len; + + return error; +} + #endif diff --git a/src/diff_driver.c b/src/diff_driver.c new file mode 100644 index 000000000..e82dfa50d --- /dev/null +++ b/src/diff_driver.c @@ -0,0 +1,406 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#include "common.h" + +#include "git2/attr.h" + +#include "diff.h" +#include "diff_patch.h" +#include "diff_driver.h" +#include "strmap.h" +#include "map.h" +#include "buf_text.h" +#include "repository.h" + +GIT__USE_STRMAP; + +typedef enum { + DIFF_DRIVER_AUTO = 0, + DIFF_DRIVER_BINARY = 1, + DIFF_DRIVER_TEXT = 2, + DIFF_DRIVER_PATTERNLIST = 3, +} git_diff_driver_t; + +enum { + DIFF_CONTEXT_FIND_NORMAL = 0, + DIFF_CONTEXT_FIND_ICASE = (1 << 0), + DIFF_CONTEXT_FIND_EXT = (1 << 1), +}; + +/* data for finding function context for a given file type */ +struct git_diff_driver { + git_diff_driver_t type; + uint32_t binary_flags; + uint32_t other_flags; + git_array_t(regex_t) fn_patterns; + regex_t word_pattern; + char name[GIT_FLEX_ARRAY]; +}; + +struct git_diff_driver_registry { + git_strmap *drivers; +}; + +#define FORCE_DIFFABLE (GIT_DIFF_FORCE_TEXT | GIT_DIFF_FORCE_BINARY) + +static git_diff_driver global_drivers[3] = { + { DIFF_DRIVER_AUTO, 0, 0, }, + { DIFF_DRIVER_BINARY, GIT_DIFF_FORCE_BINARY, 0 }, + { DIFF_DRIVER_TEXT, GIT_DIFF_FORCE_TEXT, 0 }, +}; + +git_diff_driver_registry *git_diff_driver_registry_new() +{ + git_diff_driver_registry *reg = + git__calloc(1, sizeof(git_diff_driver_registry)); + if (!reg) + return NULL; + + if ((reg->drivers = git_strmap_alloc()) == NULL) { + git_diff_driver_registry_free(reg); + return NULL; + } + + return reg; +} + +void git_diff_driver_registry_free(git_diff_driver_registry *reg) +{ + git_diff_driver *drv; + + if (!reg) + return; + + git_strmap_foreach_value(reg->drivers, drv, git_diff_driver_free(drv)); + git_strmap_free(reg->drivers); + git__free(reg); +} + +static int diff_driver_add_funcname( + git_diff_driver *drv, const char *name, int regex_flags) +{ + int error; + regex_t re, *re_ptr; + + if ((error = regcomp(&re, name, regex_flags)) != 0) { + /* TODO: warning about bad regex instead of failure */ + error = giterr_set_regex(&re, error); + regfree(&re); + return error; + } + + re_ptr = git_array_alloc(drv->fn_patterns); + GITERR_CHECK_ALLOC(re_ptr); + + memcpy(re_ptr, &re, sizeof(re)); + return 0; +} + +static int diff_driver_xfuncname(const git_config_entry *entry, void *payload) +{ + return diff_driver_add_funcname(payload, entry->value, REG_EXTENDED); +} + +static int diff_driver_funcname(const git_config_entry *entry, void *payload) +{ + return diff_driver_add_funcname(payload, entry->value, 0); +} + +static git_diff_driver_registry *git_repository_driver_registry( + git_repository *repo) +{ + if (!repo->diff_drivers) { + git_diff_driver_registry *reg = git_diff_driver_registry_new(); + reg = git__compare_and_swap(&repo->diff_drivers, NULL, reg); + + if (reg != NULL) /* if we race, free losing allocation */ + git_diff_driver_registry_free(reg); + } + + if (!repo->diff_drivers) + giterr_set(GITERR_REPOSITORY, "Unable to create diff driver registry"); + + return repo->diff_drivers; +} + +static int git_diff_driver_load( + git_diff_driver **out, git_repository *repo, const char *driver_name) +{ + int error = 0, bval; + git_diff_driver_registry *reg; + git_diff_driver *drv; + size_t namelen = strlen(driver_name); + khiter_t pos; + git_config *cfg; + git_buf name = GIT_BUF_INIT; + const char *val; + bool found_driver = false; + + reg = git_repository_driver_registry(repo); + if (!reg) + return -1; + else { + pos = git_strmap_lookup_index(reg->drivers, driver_name); + if (git_strmap_valid_index(reg->drivers, pos)) { + *out = git_strmap_value_at(reg->drivers, pos); + return 0; + } + } + + /* if you can't read config for repo, just use default driver */ + if (git_repository_config__weakptr(&cfg, repo) < 0) { + giterr_clear(); + return GIT_ENOTFOUND; + } + + drv = git__calloc(1, sizeof(git_diff_driver) + namelen + 1); + GITERR_CHECK_ALLOC(drv); + drv->type = DIFF_DRIVER_AUTO; + memcpy(drv->name, driver_name, namelen); + + if ((error = git_buf_printf(&name, "diff.%s.binary", driver_name)) < 0) + goto done; + if ((error = git_config_get_string(&val, cfg, name.ptr)) < 0) { + if (error != GIT_ENOTFOUND) + goto done; + /* diff.<driver>.binary unspecified, so just continue */ + giterr_clear(); + } else if (git_config_parse_bool(&bval, val) < 0) { + /* TODO: warn that diff.<driver>.binary has invalid value */ + giterr_clear(); + } else if (bval) { + /* if diff.<driver>.binary is true, just return the binary driver */ + *out = &global_drivers[DIFF_DRIVER_BINARY]; + goto done; + } else { + /* if diff.<driver>.binary is false, force binary checks off */ + /* but still may have custom function context patterns, etc. */ + drv->binary_flags = GIT_DIFF_FORCE_TEXT; + found_driver = true; + } + + /* TODO: warn if diff.<name>.command or diff.<name>.textconv are set */ + + git_buf_truncate(&name, namelen + strlen("diff..")); + git_buf_put(&name, "xfuncname", strlen("xfuncname")); + if ((error = git_config_get_multivar( + cfg, name.ptr, NULL, diff_driver_xfuncname, drv)) < 0) { + if (error != GIT_ENOTFOUND) + goto done; + giterr_clear(); /* no diff.<driver>.xfuncname, so just continue */ + } + + git_buf_truncate(&name, namelen + strlen("diff..")); + git_buf_put(&name, "funcname", strlen("funcname")); + if ((error = git_config_get_multivar( + cfg, name.ptr, NULL, diff_driver_funcname, drv)) < 0) { + if (error != GIT_ENOTFOUND) + goto done; + giterr_clear(); /* no diff.<driver>.funcname, so just continue */ + } + + /* if we found any patterns, set driver type to use correct callback */ + if (git_array_size(drv->fn_patterns) > 0) { + drv->type = DIFF_DRIVER_PATTERNLIST; + found_driver = true; + } + + git_buf_truncate(&name, namelen + strlen("diff..")); + git_buf_put(&name, "wordregex", strlen("wordregex")); + if ((error = git_config_get_string(&val, cfg, name.ptr)) < 0) { + if (error != GIT_ENOTFOUND) + goto done; + giterr_clear(); /* no diff.<driver>.wordregex, so just continue */ + } else if ((error = regcomp(&drv->word_pattern, val, REG_EXTENDED)) != 0) { + /* TODO: warning about bad regex instead of failure */ + error = giterr_set_regex(&drv->word_pattern, error); + goto done; + } else { + found_driver = true; + } + + /* TODO: look up diff.<driver>.algorithm to turn on minimal / patience + * diff in drv->other_flags + */ + + /* if no driver config found at all, fall back on AUTO driver */ + if (!found_driver) + goto done; + + /* store driver in registry */ + git_strmap_insert(reg->drivers, drv->name, drv, error); + if (error < 0) + goto done; + + *out = drv; + +done: + git_buf_free(&name); + + if (!*out) + *out = &global_drivers[DIFF_DRIVER_AUTO]; + + if (drv && drv != *out) + git_diff_driver_free(drv); + + return error; +} + +int git_diff_driver_lookup( + git_diff_driver **out, git_repository *repo, const char *path) +{ + int error = 0; + const char *value; + + assert(out); + + if (!repo || !path || !strlen(path)) + goto use_auto; + + if ((error = git_attr_get(&value, repo, 0, path, "diff")) < 0) + return error; + + if (GIT_ATTR_UNSPECIFIED(value)) + /* just use the auto value */; + else if (GIT_ATTR_FALSE(value)) + *out = &global_drivers[DIFF_DRIVER_BINARY]; + else if (GIT_ATTR_TRUE(value)) + *out = &global_drivers[DIFF_DRIVER_TEXT]; + + /* otherwise look for driver information in config and build driver */ + else if ((error = git_diff_driver_load(out, repo, value)) < 0) { + if (error != GIT_ENOTFOUND) + return error; + else + giterr_clear(); + } + +use_auto: + if (!*out) + *out = &global_drivers[DIFF_DRIVER_AUTO]; + + return 0; +} + +void git_diff_driver_free(git_diff_driver *driver) +{ + size_t i; + + if (!driver) + return; + + for (i = 0; i < git_array_size(driver->fn_patterns); ++i) + regfree(git_array_get(driver->fn_patterns, i)); + git_array_clear(driver->fn_patterns); + + regfree(&driver->word_pattern); + + git__free(driver); +} + +void git_diff_driver_update_options( + uint32_t *option_flags, git_diff_driver *driver) +{ + if ((*option_flags & FORCE_DIFFABLE) == 0) + *option_flags |= driver->binary_flags; + + *option_flags |= driver->other_flags; +} + +int git_diff_driver_content_is_binary( + git_diff_driver *driver, const char *content, size_t content_len) +{ + const git_buf search = { (char *)content, 0, min(content_len, 4000) }; + + GIT_UNUSED(driver); + + /* TODO: provide encoding / binary detection callbacks that can + * be UTF-8 aware, etc. For now, instead of trying to be smart, + * let's just use the simple NUL-byte detection that core git uses. + */ + + /* previously was: if (git_buf_text_is_binary(&search)) */ + if (git_buf_text_contains_nul(&search)) + return 1; + + return 0; +} + +static int diff_context_line__simple( + git_diff_driver *driver, const char *line, size_t line_len) +{ + GIT_UNUSED(driver); + GIT_UNUSED(line_len); + return (git__isalpha(*line) || *line == '_' || *line == '$'); +} + +static int diff_context_line__pattern_match( + git_diff_driver *driver, const char *line, size_t line_len) +{ + size_t i; + + GIT_UNUSED(line_len); + + for (i = 0; i < git_array_size(driver->fn_patterns); ++i) { + if (!regexec(git_array_get(driver->fn_patterns, i), line, 0, NULL, 0)) + return true; + } + + return false; +} + +static long diff_context_find( + const char *line, + long line_len, + char *out, + long out_size, + void *payload) +{ + git_diff_find_context_payload *ctxt = payload; + + if (git_buf_set(&ctxt->line, line, (size_t)line_len) < 0) + return -1; + git_buf_rtrim(&ctxt->line); + + if (!ctxt->line.size) + return -1; + + if (!ctxt->match_line || + !ctxt->match_line(ctxt->driver, ctxt->line.ptr, ctxt->line.size)) + return -1; + + if (out_size > (long)ctxt->line.size) + out_size = (long)ctxt->line.size; + memcpy(out, ctxt->line.ptr, (size_t)out_size); + + return out_size; +} + +void git_diff_find_context_init( + git_diff_find_context_fn *findfn_out, + git_diff_find_context_payload *payload_out, + git_diff_driver *driver) +{ + *findfn_out = driver ? diff_context_find : NULL; + + memset(payload_out, 0, sizeof(*payload_out)); + if (driver) { + payload_out->driver = driver; + payload_out->match_line = (driver->type == DIFF_DRIVER_PATTERNLIST) ? + diff_context_line__pattern_match : diff_context_line__simple; + git_buf_init(&payload_out->line, 0); + } +} + +void git_diff_find_context_clear(git_diff_find_context_payload *payload) +{ + if (payload) { + git_buf_free(&payload->line); + payload->driver = NULL; + } +} + diff --git a/src/diff_driver.h b/src/diff_driver.h new file mode 100644 index 000000000..9d3f18660 --- /dev/null +++ b/src/diff_driver.h @@ -0,0 +1,49 @@ +/* + * 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_diff_driver_h__ +#define INCLUDE_diff_driver_h__ + +#include "common.h" +#include "buffer.h" + +typedef struct git_diff_driver_registry git_diff_driver_registry; + +git_diff_driver_registry *git_diff_driver_registry_new(void); +void git_diff_driver_registry_free(git_diff_driver_registry *); + +typedef struct git_diff_driver git_diff_driver; + +int git_diff_driver_lookup(git_diff_driver **, git_repository *, const char *); +void git_diff_driver_free(git_diff_driver *); + +/* diff option flags to force off and on for this driver */ +void git_diff_driver_update_options(uint32_t *option_flags, git_diff_driver *); + +/* returns -1 meaning "unknown", 0 meaning not binary, 1 meaning binary */ +int git_diff_driver_content_is_binary( + git_diff_driver *, const char *content, size_t content_len); + +typedef long (*git_diff_find_context_fn)( + const char *, long, char *, long, void *); + +typedef int (*git_diff_find_context_line)( + git_diff_driver *, const char *, size_t); + +typedef struct { + git_diff_driver *driver; + git_diff_find_context_line match_line; + git_buf line; +} git_diff_find_context_payload; + +void git_diff_find_context_init( + git_diff_find_context_fn *findfn_out, + git_diff_find_context_payload *payload_out, + git_diff_driver *driver); + +void git_diff_find_context_clear(git_diff_find_context_payload *); + +#endif diff --git a/src/diff_file.c b/src/diff_file.c new file mode 100644 index 000000000..bcfef13cd --- /dev/null +++ b/src/diff_file.c @@ -0,0 +1,440 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#include "common.h" +#include "git2/blob.h" +#include "git2/submodule.h" +#include "diff.h" +#include "diff_file.h" +#include "odb.h" +#include "fileops.h" +#include "filter.h" + +#define DIFF_MAX_FILESIZE 0x20000000 + +static bool diff_file_content_binary_by_size(git_diff_file_content *fc) +{ + /* if we have diff opts, check max_size vs file size */ + if ((fc->file->flags & DIFF_FLAGS_KNOWN_BINARY) == 0 && + fc->opts_max_size > 0 && + fc->file->size > fc->opts_max_size) + fc->file->flags |= GIT_DIFF_FLAG_BINARY; + + return ((fc->file->flags & GIT_DIFF_FLAG_BINARY) != 0); +} + +static void diff_file_content_binary_by_content(git_diff_file_content *fc) +{ + if ((fc->file->flags & DIFF_FLAGS_KNOWN_BINARY) != 0) + return; + + switch (git_diff_driver_content_is_binary( + fc->driver, fc->map.data, fc->map.len)) { + case 0: fc->file->flags |= GIT_DIFF_FLAG_NOT_BINARY; break; + case 1: fc->file->flags |= GIT_DIFF_FLAG_BINARY; break; + default: break; + } +} + +static int diff_file_content_init_common( + git_diff_file_content *fc, const git_diff_options *opts) +{ + fc->opts_flags = opts ? opts->flags : GIT_DIFF_NORMAL; + + if (opts && opts->max_size >= 0) + fc->opts_max_size = opts->max_size ? + opts->max_size : DIFF_MAX_FILESIZE; + + if (fc->src == GIT_ITERATOR_TYPE_EMPTY) + fc->src = GIT_ITERATOR_TYPE_TREE; + + if (!fc->driver && + git_diff_driver_lookup(&fc->driver, fc->repo, fc->file->path) < 0) + return -1; + + /* give driver a chance to modify options */ + git_diff_driver_update_options(&fc->opts_flags, fc->driver); + + /* make sure file is conceivable mmap-able */ + if ((git_off_t)((size_t)fc->file->size) != fc->file->size) + fc->file->flags |= GIT_DIFF_FLAG_BINARY; + /* check if user is forcing text diff the file */ + else if (fc->opts_flags & GIT_DIFF_FORCE_TEXT) { + fc->file->flags &= ~GIT_DIFF_FLAG_BINARY; + fc->file->flags |= GIT_DIFF_FLAG_NOT_BINARY; + } + /* check if user is forcing binary diff the file */ + else if (fc->opts_flags & GIT_DIFF_FORCE_BINARY) { + fc->file->flags &= ~GIT_DIFF_FLAG_NOT_BINARY; + fc->file->flags |= GIT_DIFF_FLAG_BINARY; + } + + diff_file_content_binary_by_size(fc); + + if ((fc->flags & GIT_DIFF_FLAG__NO_DATA) != 0) { + fc->flags |= GIT_DIFF_FLAG__LOADED; + fc->map.len = 0; + fc->map.data = ""; + } + + if ((fc->flags & GIT_DIFF_FLAG__LOADED) != 0) + diff_file_content_binary_by_content(fc); + + return 0; +} + +int git_diff_file_content__init_from_diff( + git_diff_file_content *fc, + git_diff_list *diff, + size_t delta_index, + bool use_old) +{ + git_diff_delta *delta = git_vector_get(&diff->deltas, delta_index); + bool has_data = true; + + memset(fc, 0, sizeof(*fc)); + fc->repo = diff->repo; + fc->file = use_old ? &delta->old_file : &delta->new_file; + fc->src = use_old ? diff->old_src : diff->new_src; + + if (git_diff_driver_lookup(&fc->driver, fc->repo, fc->file->path) < 0) + return -1; + + switch (delta->status) { + case GIT_DELTA_ADDED: + has_data = !use_old; break; + case GIT_DELTA_DELETED: + has_data = use_old; break; + case GIT_DELTA_UNTRACKED: + has_data = !use_old && + (diff->opts.flags & GIT_DIFF_INCLUDE_UNTRACKED_CONTENT) != 0; + break; + case GIT_DELTA_MODIFIED: + case GIT_DELTA_COPIED: + case GIT_DELTA_RENAMED: + break; + default: + has_data = false; + break; + } + + if (!has_data) + fc->flags |= GIT_DIFF_FLAG__NO_DATA; + + return diff_file_content_init_common(fc, &diff->opts); +} + +int git_diff_file_content__init_from_blob( + git_diff_file_content *fc, + git_repository *repo, + const git_diff_options *opts, + const git_blob *blob, + git_diff_file *as_file) +{ + memset(fc, 0, sizeof(*fc)); + fc->repo = repo; + fc->file = as_file; + fc->blob = blob; + + if (!blob) { + fc->flags |= GIT_DIFF_FLAG__NO_DATA; + } else { + fc->flags |= GIT_DIFF_FLAG__LOADED; + fc->file->flags |= GIT_DIFF_FLAG_VALID_OID; + fc->file->size = git_blob_rawsize(blob); + fc->file->mode = GIT_FILEMODE_BLOB; + git_oid_cpy(&fc->file->oid, git_blob_id(blob)); + + fc->map.len = (size_t)fc->file->size; + fc->map.data = (char *)git_blob_rawcontent(blob); + } + + return diff_file_content_init_common(fc, opts); +} + +int git_diff_file_content__init_from_raw( + git_diff_file_content *fc, + git_repository *repo, + const git_diff_options *opts, + const char *buf, + size_t buflen, + git_diff_file *as_file) +{ + memset(fc, 0, sizeof(*fc)); + fc->repo = repo; + fc->file = as_file; + + if (!buf) { + fc->flags |= GIT_DIFF_FLAG__NO_DATA; + } else { + fc->flags |= GIT_DIFF_FLAG__LOADED; + fc->file->flags |= GIT_DIFF_FLAG_VALID_OID; + fc->file->size = buflen; + fc->file->mode = GIT_FILEMODE_BLOB; + git_odb_hash(&fc->file->oid, buf, buflen, GIT_OBJ_BLOB); + + fc->map.len = buflen; + fc->map.data = (char *)buf; + } + + return diff_file_content_init_common(fc, opts); +} + +static int diff_file_content_commit_to_str( + git_diff_file_content *fc, bool check_status) +{ + char oid[GIT_OID_HEXSZ+1]; + git_buf content = GIT_BUF_INIT; + const char *status = ""; + + if (check_status) { + int error = 0; + git_submodule *sm = NULL; + unsigned int sm_status = 0; + const git_oid *sm_head; + + if ((error = git_submodule_lookup(&sm, fc->repo, fc->file->path)) < 0 || + (error = git_submodule_status(&sm_status, sm)) < 0) { + /* GIT_EEXISTS means a "submodule" that has not been git added */ + if (error == GIT_EEXISTS) + error = 0; + return error; + } + + /* update OID if we didn't have it previously */ + if ((fc->file->flags & GIT_DIFF_FLAG_VALID_OID) == 0 && + ((sm_head = git_submodule_wd_id(sm)) != NULL || + (sm_head = git_submodule_head_id(sm)) != NULL)) + { + git_oid_cpy(&fc->file->oid, sm_head); + fc->file->flags |= GIT_DIFF_FLAG_VALID_OID; + } + + if (GIT_SUBMODULE_STATUS_IS_WD_DIRTY(sm_status)) + status = "-dirty"; + } + + git_oid_tostr(oid, sizeof(oid), &fc->file->oid); + if (git_buf_printf(&content, "Subproject commit %s%s\n", oid, status) < 0) + return -1; + + fc->map.len = git_buf_len(&content); + fc->map.data = git_buf_detach(&content); + fc->flags |= GIT_DIFF_FLAG__FREE_DATA; + + return 0; +} + +static int diff_file_content_load_blob(git_diff_file_content *fc) +{ + int error = 0; + git_odb_object *odb_obj = NULL; + + if (git_oid_iszero(&fc->file->oid)) + return 0; + + if (fc->file->mode == GIT_FILEMODE_COMMIT) + return diff_file_content_commit_to_str(fc, false); + + /* if we don't know size, try to peek at object header first */ + if (!fc->file->size) { + if ((error = git_diff_file__resolve_zero_size( + fc->file, &odb_obj, fc->repo)) < 0) + return error; + } + + if (diff_file_content_binary_by_size(fc)) + return 0; + + if (odb_obj != NULL) { + error = git_object__from_odb_object( + (git_object **)&fc->blob, fc->repo, odb_obj, GIT_OBJ_BLOB); + git_odb_object_free(odb_obj); + } else { + error = git_blob_lookup( + (git_blob **)&fc->blob, fc->repo, &fc->file->oid); + } + + if (!error) { + fc->flags |= GIT_DIFF_FLAG__FREE_BLOB; + fc->map.data = (void *)git_blob_rawcontent(fc->blob); + fc->map.len = (size_t)git_blob_rawsize(fc->blob); + } + + return error; +} + +static int diff_file_content_load_workdir_symlink( + git_diff_file_content *fc, git_buf *path) +{ + ssize_t alloc_len, read_len; + + /* link path on disk could be UTF-16, so prepare a buffer that is + * big enough to handle some UTF-8 data expansion + */ + alloc_len = (ssize_t)(fc->file->size * 2) + 1; + + fc->map.data = git__calloc(alloc_len, sizeof(char)); + GITERR_CHECK_ALLOC(fc->map.data); + + fc->flags |= GIT_DIFF_FLAG__FREE_DATA; + + read_len = p_readlink(git_buf_cstr(path), fc->map.data, alloc_len); + if (read_len < 0) { + giterr_set(GITERR_OS, "Failed to read symlink '%s'", fc->file->path); + return -1; + } + + fc->map.len = read_len; + return 0; +} + +static int diff_file_content_load_workdir_file( + git_diff_file_content *fc, git_buf *path) +{ + int error = 0; + git_vector filters = GIT_VECTOR_INIT; + git_buf raw = GIT_BUF_INIT, filtered = GIT_BUF_INIT; + git_file fd = git_futils_open_ro(git_buf_cstr(path)); + + if (fd < 0) + return fd; + + if (!fc->file->size && + !(fc->file->size = git_futils_filesize(fd))) + goto cleanup; + + if (diff_file_content_binary_by_size(fc)) + goto cleanup; + + if ((error = git_filters_load( + &filters, fc->repo, fc->file->path, GIT_FILTER_TO_ODB)) < 0) + goto cleanup; + /* error >= is a filter count */ + + if (error == 0) { + if (!(error = git_futils_mmap_ro( + &fc->map, fd, 0, (size_t)fc->file->size))) + fc->flags |= GIT_DIFF_FLAG__UNMAP_DATA; + else /* fall through to try readbuffer below */ + giterr_clear(); + } + + if (error != 0) { + error = git_futils_readbuffer_fd(&raw, fd, (size_t)fc->file->size); + if (error < 0) + goto cleanup; + + if (!filters.length) + git_buf_swap(&filtered, &raw); + else + error = git_filters_apply(&filtered, &raw, &filters); + + if (!error) { + fc->map.len = git_buf_len(&filtered); + fc->map.data = git_buf_detach(&filtered); + fc->flags |= GIT_DIFF_FLAG__FREE_DATA; + } + + git_buf_free(&raw); + git_buf_free(&filtered); + } + +cleanup: + git_filters_free(&filters); + p_close(fd); + + return error; +} + +static int diff_file_content_load_workdir(git_diff_file_content *fc) +{ + int error = 0; + git_buf path = GIT_BUF_INIT; + + if (fc->file->mode == GIT_FILEMODE_COMMIT) + return diff_file_content_commit_to_str(fc, true); + + if (fc->file->mode == GIT_FILEMODE_TREE) + return 0; + + if (git_buf_joinpath( + &path, git_repository_workdir(fc->repo), fc->file->path) < 0) + return -1; + + if (S_ISLNK(fc->file->mode)) + error = diff_file_content_load_workdir_symlink(fc, &path); + else + error = diff_file_content_load_workdir_file(fc, &path); + + /* once data is loaded, update OID if we didn't have it previously */ + if (!error && (fc->file->flags & GIT_DIFF_FLAG_VALID_OID) == 0) { + error = git_odb_hash( + &fc->file->oid, fc->map.data, fc->map.len, GIT_OBJ_BLOB); + fc->file->flags |= GIT_DIFF_FLAG_VALID_OID; + } + + git_buf_free(&path); + return error; +} + +int git_diff_file_content__load(git_diff_file_content *fc) +{ + int error = 0; + + if ((fc->flags & GIT_DIFF_FLAG__LOADED) != 0) + return 0; + + if ((fc->file->flags & GIT_DIFF_FLAG_BINARY) != 0) + return 0; + + if (fc->src == GIT_ITERATOR_TYPE_WORKDIR) + error = diff_file_content_load_workdir(fc); + else + error = diff_file_content_load_blob(fc); + if (error) + return error; + + fc->flags |= GIT_DIFF_FLAG__LOADED; + + diff_file_content_binary_by_content(fc); + + return 0; +} + +void git_diff_file_content__unload(git_diff_file_content *fc) +{ + if ((fc->flags & GIT_DIFF_FLAG__LOADED) == 0) + return; + + if (fc->flags & GIT_DIFF_FLAG__FREE_DATA) { + git__free(fc->map.data); + fc->map.data = ""; + fc->map.len = 0; + fc->flags &= ~GIT_DIFF_FLAG__FREE_DATA; + } + else if (fc->flags & GIT_DIFF_FLAG__UNMAP_DATA) { + git_futils_mmap_free(&fc->map); + fc->map.data = ""; + fc->map.len = 0; + fc->flags &= ~GIT_DIFF_FLAG__UNMAP_DATA; + } + + if (fc->flags & GIT_DIFF_FLAG__FREE_BLOB) { + git_blob_free((git_blob *)fc->blob); + fc->blob = NULL; + fc->flags &= ~GIT_DIFF_FLAG__FREE_BLOB; + } + + fc->flags &= ~GIT_DIFF_FLAG__LOADED; +} + +void git_diff_file_content__clear(git_diff_file_content *fc) +{ + git_diff_file_content__unload(fc); + + /* for now, nothing else to do */ +} diff --git a/src/diff_file.h b/src/diff_file.h new file mode 100644 index 000000000..fb08cca6a --- /dev/null +++ b/src/diff_file.h @@ -0,0 +1,58 @@ +/* + * 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_diff_file_h__ +#define INCLUDE_diff_file_h__ + +#include "common.h" +#include "diff.h" +#include "diff_driver.h" +#include "map.h" + +/* expanded information for one side of a delta */ +typedef struct { + git_repository *repo; + git_diff_file *file; + git_diff_driver *driver; + uint32_t flags; + uint32_t opts_flags; + git_off_t opts_max_size; + git_iterator_type_t src; + const git_blob *blob; + git_map map; +} git_diff_file_content; + +extern int git_diff_file_content__init_from_diff( + git_diff_file_content *fc, + git_diff_list *diff, + size_t delta_index, + bool use_old); + +extern int git_diff_file_content__init_from_blob( + git_diff_file_content *fc, + git_repository *repo, + const git_diff_options *opts, + const git_blob *blob, + git_diff_file *as_file); + +extern int git_diff_file_content__init_from_raw( + git_diff_file_content *fc, + git_repository *repo, + const git_diff_options *opts, + const char *buf, + size_t buflen, + git_diff_file *as_file); + +/* this loads the blob/file-on-disk as needed */ +extern int git_diff_file_content__load(git_diff_file_content *fc); + +/* this releases the blob/file-in-memory */ +extern void git_diff_file_content__unload(git_diff_file_content *fc); + +/* this unloads and also releases any other resources */ +extern void git_diff_file_content__clear(git_diff_file_content *fc); + +#endif diff --git a/src/diff_output.c b/src/diff_output.c deleted file mode 100644 index 34a3e506c..000000000 --- a/src/diff_output.c +++ /dev/null @@ -1,1819 +0,0 @@ -/* - * Copyright (C) the libgit2 contributors. All rights reserved. - * - * This file is part of libgit2, distributed under the GNU GPL v2 with - * a Linking Exception. For full terms see the included COPYING file. - */ -#include "common.h" -#include "git2/attr.h" -#include "git2/oid.h" -#include "git2/submodule.h" -#include "diff_output.h" -#include <ctype.h> -#include "fileops.h" -#include "filter.h" -#include "buf_text.h" - -static int read_next_int(const char **str, int *value) -{ - const char *scan = *str; - int v = 0, digits = 0; - /* find next digit */ - for (scan = *str; *scan && !isdigit(*scan); scan++); - /* parse next number */ - for (; isdigit(*scan); scan++, digits++) - v = (v * 10) + (*scan - '0'); - *str = scan; - *value = v; - return (digits > 0) ? 0 : -1; -} - -static int parse_hunk_header(git_diff_range *range, const char *header) -{ - /* expect something of the form "@@ -%d[,%d] +%d[,%d] @@" */ - if (*header != '@') - return -1; - if (read_next_int(&header, &range->old_start) < 0) - return -1; - if (*header == ',') { - if (read_next_int(&header, &range->old_lines) < 0) - return -1; - } else - range->old_lines = 1; - if (read_next_int(&header, &range->new_start) < 0) - return -1; - if (*header == ',') { - if (read_next_int(&header, &range->new_lines) < 0) - return -1; - } else - range->new_lines = 1; - if (range->old_start < 0 || range->new_start < 0) - return -1; - - return 0; -} - -#define KNOWN_BINARY_FLAGS (GIT_DIFF_FLAG_BINARY|GIT_DIFF_FLAG_NOT_BINARY) -#define NOT_BINARY_FLAGS (GIT_DIFF_FLAG_NOT_BINARY|GIT_DIFF_FLAG__NO_DATA) - -static int update_file_is_binary_by_attr( - git_repository *repo, git_diff_file *file) -{ - const char *value; - - /* because of blob diffs, cannot assume path is set */ - if (!file->path || !strlen(file->path)) - return 0; - - if (git_attr_get(&value, repo, 0, file->path, "diff") < 0) - return -1; - - if (GIT_ATTR_FALSE(value)) - file->flags |= GIT_DIFF_FLAG_BINARY; - else if (GIT_ATTR_TRUE(value)) - file->flags |= GIT_DIFF_FLAG_NOT_BINARY; - /* otherwise leave file->flags alone */ - - return 0; -} - -static void update_delta_is_binary(git_diff_delta *delta) -{ - if ((delta->old_file.flags & GIT_DIFF_FLAG_BINARY) != 0 || - (delta->new_file.flags & GIT_DIFF_FLAG_BINARY) != 0) - delta->flags |= GIT_DIFF_FLAG_BINARY; - - else if ((delta->old_file.flags & NOT_BINARY_FLAGS) != 0 && - (delta->new_file.flags & NOT_BINARY_FLAGS) != 0) - delta->flags |= GIT_DIFF_FLAG_NOT_BINARY; - - /* otherwise leave delta->flags binary value untouched */ -} - -/* returns if we forced binary setting (and no further checks needed) */ -static bool diff_delta_is_binary_forced( - diff_context *ctxt, - git_diff_delta *delta) -{ - /* return true if binary-ness has already been settled */ - if ((delta->flags & KNOWN_BINARY_FLAGS) != 0) - return true; - - /* make sure files are conceivably mmap-able */ - if ((git_off_t)((size_t)delta->old_file.size) != delta->old_file.size || - (git_off_t)((size_t)delta->new_file.size) != delta->new_file.size) - { - delta->old_file.flags |= GIT_DIFF_FLAG_BINARY; - delta->new_file.flags |= GIT_DIFF_FLAG_BINARY; - delta->flags |= GIT_DIFF_FLAG_BINARY; - return true; - } - - /* check if user is forcing us to text diff these files */ - if (ctxt->opts && (ctxt->opts->flags & GIT_DIFF_FORCE_TEXT) != 0) { - delta->old_file.flags |= GIT_DIFF_FLAG_NOT_BINARY; - delta->new_file.flags |= GIT_DIFF_FLAG_NOT_BINARY; - delta->flags |= GIT_DIFF_FLAG_NOT_BINARY; - return true; - } - - return false; -} - -static int diff_delta_is_binary_by_attr( - diff_context *ctxt, git_diff_patch *patch) -{ - int error = 0, mirror_new; - git_diff_delta *delta = patch->delta; - - if (diff_delta_is_binary_forced(ctxt, delta)) - return 0; - - /* check diff attribute +, -, or 0 */ - if (update_file_is_binary_by_attr(ctxt->repo, &delta->old_file) < 0) - return -1; - - mirror_new = (delta->new_file.path == delta->old_file.path || - ctxt->diff->strcomp(delta->new_file.path, delta->old_file.path) == 0); - if (mirror_new) - delta->new_file.flags |= (delta->old_file.flags & KNOWN_BINARY_FLAGS); - else - error = update_file_is_binary_by_attr(ctxt->repo, &delta->new_file); - - update_delta_is_binary(delta); - - return error; -} - -static int diff_delta_is_binary_by_content( - diff_context *ctxt, - git_diff_delta *delta, - git_diff_file *file, - const git_map *map) -{ - const git_buf search = { map->data, 0, min(map->len, 4000) }; - - if (diff_delta_is_binary_forced(ctxt, delta)) - return 0; - - /* TODO: provide encoding / binary detection callbacks that can - * be UTF-8 aware, etc. For now, instead of trying to be smart, - * let's just use the simple NUL-byte detection that core git uses. - */ - - /* previously was: if (git_buf_text_is_binary(&search)) */ - if (git_buf_text_contains_nul(&search)) - file->flags |= GIT_DIFF_FLAG_BINARY; - else - file->flags |= GIT_DIFF_FLAG_NOT_BINARY; - - update_delta_is_binary(delta); - - return 0; -} - -static int diff_delta_is_binary_by_size( - diff_context *ctxt, git_diff_delta *delta, git_diff_file *file) -{ - git_off_t threshold = MAX_DIFF_FILESIZE; - - if ((file->flags & KNOWN_BINARY_FLAGS) != 0) - return 0; - - if (ctxt && ctxt->opts) { - if (ctxt->opts->max_size < 0) - return 0; - - if (ctxt->opts->max_size > 0) - threshold = ctxt->opts->max_size; - } - - if (file->size > threshold) - file->flags |= GIT_DIFF_FLAG_BINARY; - - update_delta_is_binary(delta); - - return 0; -} - -static void setup_xdiff_options( - const git_diff_options *opts, xdemitconf_t *cfg, xpparam_t *param) -{ - memset(cfg, 0, sizeof(xdemitconf_t)); - memset(param, 0, sizeof(xpparam_t)); - - cfg->ctxlen = - (!opts) ? 3 : opts->context_lines; - cfg->interhunkctxlen = - (!opts) ? 0 : opts->interhunk_lines; - - if (!opts) - return; - - if (opts->flags & GIT_DIFF_IGNORE_WHITESPACE) - param->flags |= XDF_WHITESPACE_FLAGS; - if (opts->flags & GIT_DIFF_IGNORE_WHITESPACE_CHANGE) - param->flags |= XDF_IGNORE_WHITESPACE_CHANGE; - if (opts->flags & GIT_DIFF_IGNORE_WHITESPACE_EOL) - param->flags |= XDF_IGNORE_WHITESPACE_AT_EOL; -} - - -static int get_blob_content( - diff_context *ctxt, - git_diff_delta *delta, - git_diff_file *file, - git_map *map, - git_blob **blob) -{ - int error; - git_odb_object *odb_obj = NULL; - - if (git_oid_iszero(&file->oid)) - return 0; - - if (file->mode == GIT_FILEMODE_COMMIT) - { - char oidstr[GIT_OID_HEXSZ+1]; - git_buf content = GIT_BUF_INIT; - - git_oid_fmt(oidstr, &file->oid); - oidstr[GIT_OID_HEXSZ] = 0; - git_buf_printf(&content, "Subproject commit %s\n", oidstr ); - - map->data = git_buf_detach(&content); - map->len = strlen(map->data); - - file->flags |= GIT_DIFF_FLAG__FREE_DATA; - return 0; - } - - if (!file->size) { - git_odb *odb; - size_t len; - git_otype type; - - /* peek at object header to avoid loading if too large */ - if ((error = git_repository_odb__weakptr(&odb, ctxt->repo)) < 0 || - (error = git_odb__read_header_or_object( - &odb_obj, &len, &type, odb, &file->oid)) < 0) - return error; - - assert(type == GIT_OBJ_BLOB); - - file->size = len; - } - - /* if blob is too large to diff, mark as binary */ - if ((error = diff_delta_is_binary_by_size(ctxt, delta, file)) < 0) - return error; - if ((delta->flags & GIT_DIFF_FLAG_BINARY) != 0) - return 0; - - if (odb_obj != NULL) { - error = git_object__from_odb_object( - (git_object **)blob, ctxt->repo, odb_obj, GIT_OBJ_BLOB); - git_odb_object_free(odb_obj); - } else - error = git_blob_lookup(blob, ctxt->repo, &file->oid); - - if (error) - return error; - - map->data = (void *)git_blob_rawcontent(*blob); - map->len = (size_t)git_blob_rawsize(*blob); - - return diff_delta_is_binary_by_content(ctxt, delta, file, map); -} - -static int get_workdir_sm_content( - diff_context *ctxt, - git_diff_file *file, - git_map *map) -{ - int error = 0; - git_buf content = GIT_BUF_INIT; - git_submodule* sm = NULL; - unsigned int sm_status = 0; - const char* sm_status_text = ""; - char oidstr[GIT_OID_HEXSZ+1]; - - if ((error = git_submodule_lookup(&sm, ctxt->repo, file->path)) < 0 || - (error = git_submodule_status(&sm_status, sm)) < 0) - { - /* GIT_EEXISTS means a "submodule" that has not been git added */ - if (error == GIT_EEXISTS) - error = 0; - return error; - } - - /* update OID if we didn't have it previously */ - if ((file->flags & GIT_DIFF_FLAG_VALID_OID) == 0) { - const git_oid* sm_head; - - if ((sm_head = git_submodule_wd_id(sm)) != NULL || - (sm_head = git_submodule_head_id(sm)) != NULL) - { - git_oid_cpy(&file->oid, sm_head); - file->flags |= GIT_DIFF_FLAG_VALID_OID; - } - } - - git_oid_fmt(oidstr, &file->oid); - oidstr[GIT_OID_HEXSZ] = '\0'; - - if (GIT_SUBMODULE_STATUS_IS_WD_DIRTY(sm_status)) - sm_status_text = "-dirty"; - - git_buf_printf(&content, "Subproject commit %s%s\n", - oidstr, sm_status_text); - - map->data = git_buf_detach(&content); - map->len = strlen(map->data); - - file->flags |= GIT_DIFF_FLAG__FREE_DATA; - - return 0; -} - -static int get_filtered( - git_map *map, git_file fd, git_diff_file *file, git_vector *filters) -{ - int error; - git_buf raw = GIT_BUF_INIT, filtered = GIT_BUF_INIT; - - if ((error = git_futils_readbuffer_fd(&raw, fd, (size_t)file->size)) < 0) - return error; - - if (!filters->length) - git_buf_swap(&filtered, &raw); - else - error = git_filters_apply(&filtered, &raw, filters); - - if (!error) { - map->len = git_buf_len(&filtered); - map->data = git_buf_detach(&filtered); - - file->flags |= GIT_DIFF_FLAG__FREE_DATA; - } - - git_buf_free(&raw); - git_buf_free(&filtered); - - return error; -} - -static int get_workdir_content( - diff_context *ctxt, - git_diff_delta *delta, - git_diff_file *file, - git_map *map) -{ - int error = 0; - git_buf path = GIT_BUF_INIT; - const char *wd = git_repository_workdir(ctxt->repo); - - if (S_ISGITLINK(file->mode)) - return get_workdir_sm_content(ctxt, file, map); - - if (S_ISDIR(file->mode)) - return 0; - - if (git_buf_joinpath(&path, wd, file->path) < 0) - return -1; - - if (S_ISLNK(file->mode)) { - ssize_t alloc_len, read_len; - - file->flags |= GIT_DIFF_FLAG__FREE_DATA; - file->flags |= GIT_DIFF_FLAG_BINARY; - - /* link path on disk could be UTF-16, so prepare a buffer that is - * big enough to handle some UTF-8 data expansion - */ - alloc_len = (ssize_t)(file->size * 2) + 1; - - map->data = git__malloc(alloc_len); - GITERR_CHECK_ALLOC(map->data); - - read_len = p_readlink(path.ptr, map->data, alloc_len); - if (read_len < 0) { - giterr_set(GITERR_OS, "Failed to read symlink '%s'", file->path); - error = -1; - goto cleanup; - } - - map->len = read_len; - } - else { - git_file fd = git_futils_open_ro(path.ptr); - git_vector filters = GIT_VECTOR_INIT; - - if (fd < 0) { - error = fd; - goto cleanup; - } - - if (!file->size && !(file->size = git_futils_filesize(fd))) - goto close_and_cleanup; - - if ((error = diff_delta_is_binary_by_size(ctxt, delta, file)) < 0 || - (delta->flags & GIT_DIFF_FLAG_BINARY) != 0) - goto close_and_cleanup; - - error = git_filters_load( - &filters, ctxt->repo, file->path, GIT_FILTER_TO_ODB); - if (error < 0) - goto close_and_cleanup; - - if (error == 0) { /* note: git_filters_load returns filter count */ - error = git_futils_mmap_ro(map, fd, 0, (size_t)file->size); - if (!error) - file->flags |= GIT_DIFF_FLAG__UNMAP_DATA; - } - if (error != 0) - error = get_filtered(map, fd, file, &filters); - -close_and_cleanup: - git_filters_free(&filters); - p_close(fd); - } - - /* once data is loaded, update OID if we didn't have it previously */ - if (!error && (file->flags & GIT_DIFF_FLAG_VALID_OID) == 0) { - error = git_odb_hash( - &file->oid, map->data, map->len, GIT_OBJ_BLOB); - if (!error) - file->flags |= GIT_DIFF_FLAG_VALID_OID; - } - - if (!error) - error = diff_delta_is_binary_by_content(ctxt, delta, file, map); - -cleanup: - git_buf_free(&path); - return error; -} - -static void release_content(git_diff_file *file, git_map *map, git_blob *blob) -{ - if (blob != NULL) - git_blob_free(blob); - - if (file->flags & GIT_DIFF_FLAG__FREE_DATA) { - git__free(map->data); - map->data = ""; - map->len = 0; - file->flags &= ~GIT_DIFF_FLAG__FREE_DATA; - } - else if (file->flags & GIT_DIFF_FLAG__UNMAP_DATA) { - git_futils_mmap_free(map); - map->data = ""; - map->len = 0; - file->flags &= ~GIT_DIFF_FLAG__UNMAP_DATA; - } -} - - -static int diff_context_init( - diff_context *ctxt, - git_diff_list *diff, - git_repository *repo, - const git_diff_options *opts, - git_diff_file_cb file_cb, - git_diff_hunk_cb hunk_cb, - git_diff_data_cb data_cb, - void *payload) -{ - memset(ctxt, 0, sizeof(diff_context)); - - if (!repo && diff) - repo = diff->repo; - - if (!opts && diff) - opts = &diff->opts; - - ctxt->repo = repo; - ctxt->diff = diff; - ctxt->opts = opts; - ctxt->file_cb = file_cb; - ctxt->hunk_cb = hunk_cb; - ctxt->data_cb = data_cb; - ctxt->payload = payload; - ctxt->error = 0; - - setup_xdiff_options(ctxt->opts, &ctxt->xdiff_config, &ctxt->xdiff_params); - - return 0; -} - -static int diff_delta_file_callback( - diff_context *ctxt, git_diff_delta *delta, size_t idx) -{ - float progress; - - if (!ctxt->file_cb) - return 0; - - progress = ctxt->diff ? ((float)idx / ctxt->diff->deltas.length) : 1.0f; - - if (ctxt->file_cb(delta, progress, ctxt->payload) != 0) - ctxt->error = GIT_EUSER; - - return ctxt->error; -} - -static void diff_patch_init( - diff_context *ctxt, git_diff_patch *patch) -{ - memset(patch, 0, sizeof(git_diff_patch)); - - patch->diff = ctxt->diff; - patch->ctxt = ctxt; - - if (patch->diff) { - patch->old_src = patch->diff->old_src; - patch->new_src = patch->diff->new_src; - } else { - patch->old_src = patch->new_src = GIT_ITERATOR_TYPE_TREE; - } -} - -static git_diff_patch *diff_patch_alloc( - diff_context *ctxt, git_diff_delta *delta) -{ - git_diff_patch *patch = git__malloc(sizeof(git_diff_patch)); - if (!patch) - return NULL; - - diff_patch_init(ctxt, patch); - - git_diff_list_addref(patch->diff); - - GIT_REFCOUNT_INC(patch); - - patch->delta = delta; - patch->flags = GIT_DIFF_PATCH_ALLOCATED; - - return patch; -} - -static int diff_patch_load( - diff_context *ctxt, git_diff_patch *patch) -{ - int error = 0; - git_diff_delta *delta = patch->delta; - bool check_if_unmodified = false; - - if ((patch->flags & GIT_DIFF_PATCH_LOADED) != 0) - return 0; - - error = diff_delta_is_binary_by_attr(ctxt, patch); - - patch->old_data.data = ""; - patch->old_data.len = 0; - patch->old_blob = NULL; - - patch->new_data.data = ""; - patch->new_data.len = 0; - patch->new_blob = NULL; - - if ((delta->flags & GIT_DIFF_FLAG_BINARY) != 0) - goto cleanup; - - if (!ctxt->hunk_cb && - !ctxt->data_cb && - (ctxt->opts->flags & GIT_DIFF_SKIP_BINARY_CHECK) != 0) - goto cleanup; - - switch (delta->status) { - case GIT_DELTA_ADDED: - delta->old_file.flags |= GIT_DIFF_FLAG__NO_DATA; - break; - case GIT_DELTA_DELETED: - delta->new_file.flags |= GIT_DIFF_FLAG__NO_DATA; - break; - case GIT_DELTA_MODIFIED: - break; - case GIT_DELTA_UNTRACKED: - delta->old_file.flags |= GIT_DIFF_FLAG__NO_DATA; - if ((ctxt->opts->flags & GIT_DIFF_INCLUDE_UNTRACKED_CONTENT) == 0) - delta->new_file.flags |= GIT_DIFF_FLAG__NO_DATA; - break; - default: - delta->new_file.flags |= GIT_DIFF_FLAG__NO_DATA; - delta->old_file.flags |= GIT_DIFF_FLAG__NO_DATA; - break; - } - -#define CHECK_UNMODIFIED (GIT_DIFF_FLAG__NO_DATA | GIT_DIFF_FLAG_VALID_OID) - - check_if_unmodified = - (delta->old_file.flags & CHECK_UNMODIFIED) == 0 && - (delta->new_file.flags & CHECK_UNMODIFIED) == 0; - - /* Always try to load workdir content first, since it may need to be - * filtered (and hence use 2x memory) and we want to minimize the max - * memory footprint during diff. - */ - - if ((delta->old_file.flags & GIT_DIFF_FLAG__NO_DATA) == 0 && - patch->old_src == GIT_ITERATOR_TYPE_WORKDIR) { - if ((error = get_workdir_content( - ctxt, delta, &delta->old_file, &patch->old_data)) < 0) - goto cleanup; - if ((delta->flags & GIT_DIFF_FLAG_BINARY) != 0) - goto cleanup; - } - - if ((delta->new_file.flags & GIT_DIFF_FLAG__NO_DATA) == 0 && - patch->new_src == GIT_ITERATOR_TYPE_WORKDIR) { - if ((error = get_workdir_content( - ctxt, delta, &delta->new_file, &patch->new_data)) < 0) - goto cleanup; - if ((delta->flags & GIT_DIFF_FLAG_BINARY) != 0) - goto cleanup; - } - - if ((delta->old_file.flags & GIT_DIFF_FLAG__NO_DATA) == 0 && - patch->old_src != GIT_ITERATOR_TYPE_WORKDIR) { - if ((error = get_blob_content( - ctxt, delta, &delta->old_file, - &patch->old_data, &patch->old_blob)) < 0) - goto cleanup; - if ((delta->flags & GIT_DIFF_FLAG_BINARY) != 0) - goto cleanup; - } - - if ((delta->new_file.flags & GIT_DIFF_FLAG__NO_DATA) == 0 && - patch->new_src != GIT_ITERATOR_TYPE_WORKDIR) { - if ((error = get_blob_content( - ctxt, delta, &delta->new_file, - &patch->new_data, &patch->new_blob)) < 0) - goto cleanup; - if ((delta->flags & GIT_DIFF_FLAG_BINARY) != 0) - goto cleanup; - } - - /* if we did not previously have the definitive oid, we may have - * incorrect status and need to switch this to UNMODIFIED. - */ - if (check_if_unmodified && - delta->old_file.mode == delta->new_file.mode && - !git_oid_cmp(&delta->old_file.oid, &delta->new_file.oid)) - { - delta->status = GIT_DELTA_UNMODIFIED; - - if ((ctxt->opts->flags & GIT_DIFF_INCLUDE_UNMODIFIED) == 0) - goto cleanup; - } - -cleanup: - if ((delta->flags & KNOWN_BINARY_FLAGS) == 0) - update_delta_is_binary(delta); - - if (!error) { - patch->flags |= GIT_DIFF_PATCH_LOADED; - - /* patch is diffable only for non-binary, modified files where at - * least one side has data and there is actual change in the data - */ - if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0 && - delta->status != GIT_DELTA_UNMODIFIED && - (patch->old_data.len || patch->new_data.len) && - (patch->old_data.len != patch->new_data.len || - !git_oid_equal(&delta->old_file.oid, &delta->new_file.oid))) - patch->flags |= GIT_DIFF_PATCH_DIFFABLE; - } - - return error; -} - -static int diff_patch_cb(void *priv, mmbuffer_t *bufs, int len) -{ - git_diff_patch *patch = priv; - diff_context *ctxt = patch->ctxt; - - if (len == 1) { - ctxt->error = parse_hunk_header(&ctxt->range, bufs[0].ptr); - if (ctxt->error < 0) - return ctxt->error; - - if (ctxt->hunk_cb != NULL && - ctxt->hunk_cb(patch->delta, &ctxt->range, - bufs[0].ptr, bufs[0].size, ctxt->payload)) - ctxt->error = GIT_EUSER; - } - - if (len == 2 || len == 3) { - /* expect " "/"-"/"+", then data */ - char origin = - (*bufs[0].ptr == '+') ? GIT_DIFF_LINE_ADDITION : - (*bufs[0].ptr == '-') ? GIT_DIFF_LINE_DELETION : - GIT_DIFF_LINE_CONTEXT; - - if (ctxt->data_cb != NULL && - ctxt->data_cb(patch->delta, &ctxt->range, - origin, bufs[1].ptr, bufs[1].size, ctxt->payload)) - ctxt->error = GIT_EUSER; - } - - if (len == 3 && !ctxt->error) { - /* If we have a '+' and a third buf, then we have added a line - * without a newline and the old code had one, so DEL_EOFNL. - * If we have a '-' and a third buf, then we have removed a line - * with out a newline but added a blank line, so ADD_EOFNL. - */ - char origin = - (*bufs[0].ptr == '+') ? GIT_DIFF_LINE_DEL_EOFNL : - (*bufs[0].ptr == '-') ? GIT_DIFF_LINE_ADD_EOFNL : - GIT_DIFF_LINE_CONTEXT; - - if (ctxt->data_cb != NULL && - ctxt->data_cb(patch->delta, &ctxt->range, - origin, bufs[2].ptr, bufs[2].size, ctxt->payload)) - ctxt->error = GIT_EUSER; - } - - return ctxt->error; -} - -static int diff_patch_generate( - diff_context *ctxt, git_diff_patch *patch) -{ - int error = 0; - xdemitcb_t xdiff_callback; - mmfile_t old_xdiff_data, new_xdiff_data; - - if ((patch->flags & GIT_DIFF_PATCH_DIFFED) != 0) - return 0; - - if ((patch->flags & GIT_DIFF_PATCH_LOADED) == 0) - if ((error = diff_patch_load(ctxt, patch)) < 0) - return error; - - if ((patch->flags & GIT_DIFF_PATCH_DIFFABLE) == 0) - return 0; - - if (!ctxt->file_cb && !ctxt->hunk_cb) - return 0; - - patch->ctxt = ctxt; - - memset(&xdiff_callback, 0, sizeof(xdiff_callback)); - xdiff_callback.outf = diff_patch_cb; - xdiff_callback.priv = patch; - - old_xdiff_data.ptr = patch->old_data.data; - old_xdiff_data.size = patch->old_data.len; - new_xdiff_data.ptr = patch->new_data.data; - new_xdiff_data.size = patch->new_data.len; - - xdl_diff(&old_xdiff_data, &new_xdiff_data, - &ctxt->xdiff_params, &ctxt->xdiff_config, &xdiff_callback); - - error = ctxt->error; - - if (!error) - patch->flags |= GIT_DIFF_PATCH_DIFFED; - - return error; -} - -static void diff_patch_unload(git_diff_patch *patch) -{ - if ((patch->flags & GIT_DIFF_PATCH_DIFFED) != 0) { - patch->flags = (patch->flags & ~GIT_DIFF_PATCH_DIFFED); - - patch->hunks_size = 0; - patch->lines_size = 0; - } - - if ((patch->flags & GIT_DIFF_PATCH_LOADED) != 0) { - patch->flags = (patch->flags & ~GIT_DIFF_PATCH_LOADED); - - release_content( - &patch->delta->old_file, &patch->old_data, patch->old_blob); - release_content( - &patch->delta->new_file, &patch->new_data, patch->new_blob); - } -} - -static void diff_patch_free(git_diff_patch *patch) -{ - diff_patch_unload(patch); - - git__free(patch->lines); - patch->lines = NULL; - patch->lines_asize = 0; - - git__free(patch->hunks); - patch->hunks = NULL; - patch->hunks_asize = 0; - - if (!(patch->flags & GIT_DIFF_PATCH_ALLOCATED)) - return; - - patch->flags = 0; - - git_diff_list_free(patch->diff); /* decrements refcount */ - - git__free(patch); -} - -#define MAX_HUNK_STEP 128 -#define MIN_HUNK_STEP 8 -#define MAX_LINE_STEP 256 -#define MIN_LINE_STEP 8 - -static int diff_patch_hunk_cb( - const git_diff_delta *delta, - const git_diff_range *range, - const char *header, - size_t header_len, - void *payload) -{ - git_diff_patch *patch = payload; - diff_patch_hunk *hunk; - - GIT_UNUSED(delta); - - if (patch->hunks_size >= patch->hunks_asize) { - size_t new_size; - diff_patch_hunk *new_hunks; - - if (patch->hunks_asize > MAX_HUNK_STEP) - new_size = patch->hunks_asize + MAX_HUNK_STEP; - else - new_size = patch->hunks_asize * 2; - if (new_size < MIN_HUNK_STEP) - new_size = MIN_HUNK_STEP; - - new_hunks = git__realloc( - patch->hunks, new_size * sizeof(diff_patch_hunk)); - if (!new_hunks) - return -1; - - patch->hunks = new_hunks; - patch->hunks_asize = new_size; - } - - hunk = &patch->hunks[patch->hunks_size++]; - - memcpy(&hunk->range, range, sizeof(hunk->range)); - - assert(header_len + 1 < sizeof(hunk->header)); - memcpy(&hunk->header, header, header_len); - hunk->header[header_len] = '\0'; - hunk->header_len = header_len; - - hunk->line_start = patch->lines_size; - hunk->line_count = 0; - - patch->oldno = range->old_start; - patch->newno = range->new_start; - - return 0; -} - -static int diff_patch_line_cb( - const git_diff_delta *delta, - const git_diff_range *range, - char line_origin, - const char *content, - size_t content_len, - void *payload) -{ - git_diff_patch *patch = payload; - diff_patch_hunk *hunk; - diff_patch_line *line; - - GIT_UNUSED(delta); - GIT_UNUSED(range); - - assert(patch->hunks_size > 0); - assert(patch->hunks != NULL); - - hunk = &patch->hunks[patch->hunks_size - 1]; - - if (patch->lines_size >= patch->lines_asize) { - size_t new_size; - diff_patch_line *new_lines; - - if (patch->lines_asize > MAX_LINE_STEP) - new_size = patch->lines_asize + MAX_LINE_STEP; - else - new_size = patch->lines_asize * 2; - if (new_size < MIN_LINE_STEP) - new_size = MIN_LINE_STEP; - - new_lines = git__realloc( - patch->lines, new_size * sizeof(diff_patch_line)); - if (!new_lines) - return -1; - - patch->lines = new_lines; - patch->lines_asize = new_size; - } - - line = &patch->lines[patch->lines_size++]; - - line->ptr = content; - line->len = content_len; - line->origin = line_origin; - - /* do some bookkeeping so we can provide old/new line numbers */ - - for (line->lines = 0; content_len > 0; --content_len) { - if (*content++ == '\n') - ++line->lines; - } - - switch (line_origin) { - case GIT_DIFF_LINE_ADDITION: - line->oldno = -1; - line->newno = patch->newno; - patch->newno += line->lines; - break; - case GIT_DIFF_LINE_DELETION: - line->oldno = patch->oldno; - line->newno = -1; - patch->oldno += line->lines; - break; - default: - line->oldno = patch->oldno; - line->newno = patch->newno; - patch->oldno += line->lines; - patch->newno += line->lines; - break; - } - - hunk->line_count++; - - return 0; -} - -static int diff_required(git_diff_list *diff, const char *action) -{ - if (!diff) { - giterr_set(GITERR_INVALID, "Must provide valid diff to %s", action); - return -1; - } - - return 0; -} - -int git_diff_foreach( - git_diff_list *diff, - git_diff_file_cb file_cb, - git_diff_hunk_cb hunk_cb, - git_diff_data_cb data_cb, - void *payload) -{ - int error = 0; - diff_context ctxt; - size_t idx; - git_diff_patch patch; - - if (diff_required(diff, "git_diff_foreach") < 0) - return -1; - - if (diff_context_init( - &ctxt, diff, NULL, NULL, file_cb, hunk_cb, data_cb, payload) < 0) - return -1; - - diff_patch_init(&ctxt, &patch); - - git_vector_foreach(&diff->deltas, idx, patch.delta) { - - /* check flags against patch status */ - if (git_diff_delta__should_skip(ctxt.opts, patch.delta)) - continue; - - if (!(error = diff_patch_load(&ctxt, &patch))) { - - /* invoke file callback */ - error = diff_delta_file_callback(&ctxt, patch.delta, idx); - - /* generate diffs and invoke hunk and line callbacks */ - if (!error) - error = diff_patch_generate(&ctxt, &patch); - - diff_patch_unload(&patch); - } - - if (error < 0) - break; - } - - if (error == GIT_EUSER) - giterr_clear(); /* don't let error message leak */ - - return error; -} - - -typedef struct { - git_diff_list *diff; - git_diff_data_cb print_cb; - void *payload; - git_buf *buf; -} diff_print_info; - -static char pick_suffix(int mode) -{ - if (S_ISDIR(mode)) - return '/'; - else if (mode & 0100) //-V536 - /* in git, modes are very regular, so we must have 0100755 mode */ - return '*'; - else - return ' '; -} - -char git_diff_status_char(git_delta_t status) -{ - char code; - - switch (status) { - case GIT_DELTA_ADDED: code = 'A'; break; - case GIT_DELTA_DELETED: code = 'D'; break; - case GIT_DELTA_MODIFIED: code = 'M'; break; - case GIT_DELTA_RENAMED: code = 'R'; break; - case GIT_DELTA_COPIED: code = 'C'; break; - case GIT_DELTA_IGNORED: code = 'I'; break; - case GIT_DELTA_UNTRACKED: code = '?'; break; - default: code = ' '; break; - } - - return code; -} - -static int print_compact( - const git_diff_delta *delta, float progress, void *data) -{ - diff_print_info *pi = data; - char old_suffix, new_suffix, code = git_diff_status_char(delta->status); - - GIT_UNUSED(progress); - - if (code == ' ') - return 0; - - old_suffix = pick_suffix(delta->old_file.mode); - new_suffix = pick_suffix(delta->new_file.mode); - - git_buf_clear(pi->buf); - - if (delta->old_file.path != delta->new_file.path && - pi->diff->strcomp(delta->old_file.path,delta->new_file.path) != 0) - git_buf_printf(pi->buf, "%c\t%s%c -> %s%c\n", code, - delta->old_file.path, old_suffix, delta->new_file.path, new_suffix); - else if (delta->old_file.mode != delta->new_file.mode && - delta->old_file.mode != 0 && delta->new_file.mode != 0) - git_buf_printf(pi->buf, "%c\t%s%c (%o -> %o)\n", code, - delta->old_file.path, new_suffix, delta->old_file.mode, delta->new_file.mode); - else if (old_suffix != ' ') - git_buf_printf(pi->buf, "%c\t%s%c\n", code, delta->old_file.path, old_suffix); - else - git_buf_printf(pi->buf, "%c\t%s\n", code, delta->old_file.path); - - if (git_buf_oom(pi->buf)) - return -1; - - if (pi->print_cb(delta, NULL, GIT_DIFF_LINE_FILE_HDR, - git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) - { - giterr_clear(); - return GIT_EUSER; - } - - return 0; -} - -int git_diff_print_compact( - git_diff_list *diff, - git_diff_data_cb print_cb, - void *payload) -{ - int error; - git_buf buf = GIT_BUF_INIT; - diff_print_info pi; - - pi.diff = diff; - pi.print_cb = print_cb; - pi.payload = payload; - pi.buf = &buf; - - error = git_diff_foreach(diff, print_compact, NULL, NULL, &pi); - - git_buf_free(&buf); - - return error; -} - -static int print_oid_range(diff_print_info *pi, const git_diff_delta *delta) -{ - char start_oid[8], end_oid[8]; - - /* TODO: Determine a good actual OID range to print */ - git_oid_tostr(start_oid, sizeof(start_oid), &delta->old_file.oid); - git_oid_tostr(end_oid, sizeof(end_oid), &delta->new_file.oid); - - /* TODO: Match git diff more closely */ - if (delta->old_file.mode == delta->new_file.mode) { - git_buf_printf(pi->buf, "index %s..%s %o\n", - start_oid, end_oid, delta->old_file.mode); - } else { - if (delta->old_file.mode == 0) { - git_buf_printf(pi->buf, "new file mode %o\n", delta->new_file.mode); - } else if (delta->new_file.mode == 0) { - git_buf_printf(pi->buf, "deleted file mode %o\n", delta->old_file.mode); - } else { - git_buf_printf(pi->buf, "old mode %o\n", delta->old_file.mode); - git_buf_printf(pi->buf, "new mode %o\n", delta->new_file.mode); - } - git_buf_printf(pi->buf, "index %s..%s\n", start_oid, end_oid); - } - - if (git_buf_oom(pi->buf)) - return -1; - - return 0; -} - -static int print_patch_file( - const git_diff_delta *delta, float progress, void *data) -{ - diff_print_info *pi = data; - const char *oldpfx = pi->diff->opts.old_prefix; - const char *oldpath = delta->old_file.path; - const char *newpfx = pi->diff->opts.new_prefix; - const char *newpath = delta->new_file.path; - - GIT_UNUSED(progress); - - if (S_ISDIR(delta->new_file.mode) || - delta->status == GIT_DELTA_UNMODIFIED || - delta->status == GIT_DELTA_IGNORED || - (delta->status == GIT_DELTA_UNTRACKED && - (pi->diff->opts.flags & GIT_DIFF_INCLUDE_UNTRACKED_CONTENT) == 0)) - return 0; - - if (!oldpfx) - oldpfx = DIFF_OLD_PREFIX_DEFAULT; - - if (!newpfx) - newpfx = DIFF_NEW_PREFIX_DEFAULT; - - git_buf_clear(pi->buf); - git_buf_printf(pi->buf, "diff --git %s%s %s%s\n", oldpfx, delta->old_file.path, newpfx, delta->new_file.path); - - if (print_oid_range(pi, delta) < 0) - return -1; - - if (git_oid_iszero(&delta->old_file.oid)) { - oldpfx = ""; - oldpath = "/dev/null"; - } - if (git_oid_iszero(&delta->new_file.oid)) { - newpfx = ""; - newpath = "/dev/null"; - } - - if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0) { - git_buf_printf(pi->buf, "--- %s%s\n", oldpfx, oldpath); - git_buf_printf(pi->buf, "+++ %s%s\n", newpfx, newpath); - } - - if (git_buf_oom(pi->buf)) - return -1; - - if (pi->print_cb(delta, NULL, GIT_DIFF_LINE_FILE_HDR, - git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) - { - giterr_clear(); - return GIT_EUSER; - } - - if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0) - return 0; - - git_buf_clear(pi->buf); - git_buf_printf( - pi->buf, "Binary files %s%s and %s%s differ\n", - oldpfx, oldpath, newpfx, newpath); - if (git_buf_oom(pi->buf)) - return -1; - - if (pi->print_cb(delta, NULL, GIT_DIFF_LINE_BINARY, - git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) - { - giterr_clear(); - return GIT_EUSER; - } - - return 0; -} - -static int print_patch_hunk( - const git_diff_delta *d, - const git_diff_range *r, - const char *header, - size_t header_len, - void *data) -{ - diff_print_info *pi = data; - - if (S_ISDIR(d->new_file.mode)) - return 0; - - git_buf_clear(pi->buf); - if (git_buf_printf(pi->buf, "%.*s", (int)header_len, header) < 0) - return -1; - - if (pi->print_cb(d, r, GIT_DIFF_LINE_HUNK_HDR, - git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) - { - giterr_clear(); - return GIT_EUSER; - } - - return 0; -} - -static int print_patch_line( - const git_diff_delta *delta, - const git_diff_range *range, - char line_origin, /* GIT_DIFF_LINE value from above */ - const char *content, - size_t content_len, - void *data) -{ - diff_print_info *pi = data; - - if (S_ISDIR(delta->new_file.mode)) - return 0; - - git_buf_clear(pi->buf); - - if (line_origin == GIT_DIFF_LINE_ADDITION || - line_origin == GIT_DIFF_LINE_DELETION || - line_origin == GIT_DIFF_LINE_CONTEXT) - git_buf_printf(pi->buf, "%c%.*s", line_origin, (int)content_len, content); - else if (content_len > 0) - git_buf_printf(pi->buf, "%.*s", (int)content_len, content); - - if (git_buf_oom(pi->buf)) - return -1; - - if (pi->print_cb(delta, range, line_origin, - git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) - { - giterr_clear(); - return GIT_EUSER; - } - - return 0; -} - -int git_diff_print_patch( - git_diff_list *diff, - git_diff_data_cb print_cb, - void *payload) -{ - int error; - git_buf buf = GIT_BUF_INIT; - diff_print_info pi; - - pi.diff = diff; - pi.print_cb = print_cb; - pi.payload = payload; - pi.buf = &buf; - - error = git_diff_foreach( - diff, print_patch_file, print_patch_hunk, print_patch_line, &pi); - - git_buf_free(&buf); - - return error; -} - -static void set_data_from_blob( - const git_blob *blob, git_map *map, git_diff_file *file) -{ - if (blob) { - file->size = git_blob_rawsize(blob); - git_oid_cpy(&file->oid, git_object_id((const git_object *)blob)); - file->mode = 0644; - - map->len = (size_t)file->size; - map->data = (char *)git_blob_rawcontent(blob); - } else { - file->size = 0; - file->flags |= GIT_DIFF_FLAG__NO_DATA; - - map->len = 0; - map->data = ""; - } -} - -static void set_data_from_buffer( - const char *buffer, size_t buffer_len, git_map *map, git_diff_file *file) -{ - file->size = (git_off_t)buffer_len; - file->mode = 0644; - map->len = buffer_len; - - if (!buffer) { - file->flags |= GIT_DIFF_FLAG__NO_DATA; - map->data = NULL; - } else { - map->data = (char *)buffer; - git_odb_hash(&file->oid, buffer, buffer_len, GIT_OBJ_BLOB); - } -} - -typedef struct { - diff_context ctxt; - git_diff_delta delta; - git_diff_patch patch; -} diff_single_data; - -static int diff_single_init( - diff_single_data *data, - git_repository *repo, - const git_diff_options *opts, - git_diff_file_cb file_cb, - git_diff_hunk_cb hunk_cb, - git_diff_data_cb data_cb, - void *payload) -{ - GITERR_CHECK_VERSION(opts, GIT_DIFF_OPTIONS_VERSION, "git_diff_options"); - - memset(data, 0, sizeof(*data)); - - if (diff_context_init( - &data->ctxt, NULL, repo, opts, - file_cb, hunk_cb, data_cb, payload) < 0) - return -1; - - diff_patch_init(&data->ctxt, &data->patch); - - return 0; -} - -static int diff_single_apply(diff_single_data *data) -{ - int error; - git_diff_delta *delta = &data->delta; - bool has_old = ((delta->old_file.flags & GIT_DIFF_FLAG__NO_DATA) == 0); - bool has_new = ((delta->new_file.flags & GIT_DIFF_FLAG__NO_DATA) == 0); - - /* finish setting up fake git_diff_delta record and loaded data */ - - data->patch.delta = delta; - delta->flags = delta->flags & ~KNOWN_BINARY_FLAGS; - - delta->status = has_new ? - (has_old ? GIT_DELTA_MODIFIED : GIT_DELTA_ADDED) : - (has_old ? GIT_DELTA_DELETED : GIT_DELTA_UNTRACKED); - - if (git_oid_cmp(&delta->new_file.oid, &delta->old_file.oid) == 0) - delta->status = GIT_DELTA_UNMODIFIED; - - if ((error = diff_delta_is_binary_by_content( - &data->ctxt, delta, &delta->old_file, &data->patch.old_data)) < 0 || - (error = diff_delta_is_binary_by_content( - &data->ctxt, delta, &delta->new_file, &data->patch.new_data)) < 0) - goto cleanup; - - data->patch.flags |= GIT_DIFF_PATCH_LOADED; - - if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0 && - delta->status != GIT_DELTA_UNMODIFIED) - data->patch.flags |= GIT_DIFF_PATCH_DIFFABLE; - - /* do diffs */ - - if (!(error = diff_delta_file_callback(&data->ctxt, delta, 1))) - error = diff_patch_generate(&data->ctxt, &data->patch); - -cleanup: - if (error == GIT_EUSER) - giterr_clear(); - - diff_patch_unload(&data->patch); - - return error; -} - -int git_diff_blobs( - const git_blob *old_blob, - const git_blob *new_blob, - const git_diff_options *options, - git_diff_file_cb file_cb, - git_diff_hunk_cb hunk_cb, - git_diff_data_cb data_cb, - void *payload) -{ - int error; - diff_single_data d; - git_repository *repo = - new_blob ? git_object_owner((const git_object *)new_blob) : - old_blob ? git_object_owner((const git_object *)old_blob) : NULL; - - if (!repo) /* Hmm, given two NULL blobs, silently do no callbacks? */ - return 0; - - if ((error = diff_single_init( - &d, repo, options, file_cb, hunk_cb, data_cb, payload)) < 0) - return error; - - if (options && (options->flags & GIT_DIFF_REVERSE) != 0) { - const git_blob *swap = old_blob; - old_blob = new_blob; - new_blob = swap; - } - - set_data_from_blob(old_blob, &d.patch.old_data, &d.delta.old_file); - set_data_from_blob(new_blob, &d.patch.new_data, &d.delta.new_file); - - return diff_single_apply(&d); -} - -int git_diff_blob_to_buffer( - const git_blob *old_blob, - const char *buf, - size_t buflen, - const git_diff_options *options, - git_diff_file_cb file_cb, - git_diff_hunk_cb hunk_cb, - git_diff_data_cb data_cb, - void *payload) -{ - int error; - diff_single_data d; - git_repository *repo = - old_blob ? git_object_owner((const git_object *)old_blob) : NULL; - - if (!repo && !buf) /* Hmm, given NULLs, silently do no callbacks? */ - return 0; - - if ((error = diff_single_init( - &d, repo, options, file_cb, hunk_cb, data_cb, payload)) < 0) - return error; - - if (options && (options->flags & GIT_DIFF_REVERSE) != 0) { - set_data_from_buffer(buf, buflen, &d.patch.old_data, &d.delta.old_file); - set_data_from_blob(old_blob, &d.patch.new_data, &d.delta.new_file); - } else { - set_data_from_blob(old_blob, &d.patch.old_data, &d.delta.old_file); - set_data_from_buffer(buf, buflen, &d.patch.new_data, &d.delta.new_file); - } - - return diff_single_apply(&d); -} - -size_t git_diff_num_deltas(git_diff_list *diff) -{ - assert(diff); - return (size_t)diff->deltas.length; -} - -size_t git_diff_num_deltas_of_type(git_diff_list *diff, git_delta_t type) -{ - size_t i, count = 0; - git_diff_delta *delta; - - assert(diff); - - git_vector_foreach(&diff->deltas, i, delta) { - count += (delta->status == type); - } - - return count; -} - -int git_diff_get_patch( - git_diff_patch **patch_ptr, - const git_diff_delta **delta_ptr, - git_diff_list *diff, - size_t idx) -{ - int error; - diff_context ctxt; - git_diff_delta *delta; - git_diff_patch *patch; - - if (patch_ptr) - *patch_ptr = NULL; - if (delta_ptr) - *delta_ptr = NULL; - - if (diff_required(diff, "git_diff_get_patch") < 0) - return -1; - - if (diff_context_init( - &ctxt, diff, NULL, NULL, - NULL, diff_patch_hunk_cb, diff_patch_line_cb, NULL) < 0) - return -1; - - delta = git_vector_get(&diff->deltas, idx); - if (!delta) { - giterr_set(GITERR_INVALID, "Index out of range for delta in diff"); - return GIT_ENOTFOUND; - } - - if (delta_ptr) - *delta_ptr = delta; - - if (!patch_ptr && - ((delta->flags & KNOWN_BINARY_FLAGS) != 0 || - (diff->opts.flags & GIT_DIFF_SKIP_BINARY_CHECK) != 0)) - return 0; - - if (git_diff_delta__should_skip(ctxt.opts, delta)) - return 0; - - /* Don't load the patch if the user doesn't want it */ - if (!patch_ptr) - return 0; - - patch = diff_patch_alloc(&ctxt, delta); - if (!patch) - return -1; - - if (!(error = diff_patch_load(&ctxt, patch))) { - ctxt.payload = patch; - - error = diff_patch_generate(&ctxt, patch); - - if (error == GIT_EUSER) - error = ctxt.error; - } - - if (error) - git_diff_patch_free(patch); - else if (patch_ptr) - *patch_ptr = patch; - - return error; -} - -void git_diff_patch_free(git_diff_patch *patch) -{ - if (patch) - GIT_REFCOUNT_DEC(patch, diff_patch_free); -} - -const git_diff_delta *git_diff_patch_delta(git_diff_patch *patch) -{ - assert(patch); - return patch->delta; -} - -size_t git_diff_patch_num_hunks(git_diff_patch *patch) -{ - assert(patch); - return patch->hunks_size; -} - -int git_diff_patch_line_stats( - size_t *total_ctxt, - size_t *total_adds, - size_t *total_dels, - const git_diff_patch *patch) -{ - size_t totals[3], idx; - - memset(totals, 0, sizeof(totals)); - - for (idx = 0; idx < patch->lines_size; ++idx) { - switch (patch->lines[idx].origin) { - case GIT_DIFF_LINE_CONTEXT: totals[0]++; break; - case GIT_DIFF_LINE_ADDITION: totals[1]++; break; - case GIT_DIFF_LINE_DELETION: totals[2]++; break; - default: - /* diff --stat and --numstat don't count EOFNL marks because - * they will always be paired with a ADDITION or DELETION line. - */ - break; - } - } - - if (total_ctxt) - *total_ctxt = totals[0]; - if (total_adds) - *total_adds = totals[1]; - if (total_dels) - *total_dels = totals[2]; - - return 0; -} - -int git_diff_patch_get_hunk( - const git_diff_range **range, - const char **header, - size_t *header_len, - size_t *lines_in_hunk, - git_diff_patch *patch, - size_t hunk_idx) -{ - diff_patch_hunk *hunk; - - assert(patch); - - if (hunk_idx >= patch->hunks_size) { - if (range) *range = NULL; - if (header) *header = NULL; - if (header_len) *header_len = 0; - if (lines_in_hunk) *lines_in_hunk = 0; - return GIT_ENOTFOUND; - } - - hunk = &patch->hunks[hunk_idx]; - - if (range) *range = &hunk->range; - if (header) *header = hunk->header; - if (header_len) *header_len = hunk->header_len; - if (lines_in_hunk) *lines_in_hunk = hunk->line_count; - - return 0; -} - -int git_diff_patch_num_lines_in_hunk( - git_diff_patch *patch, - size_t hunk_idx) -{ - assert(patch); - - if (hunk_idx >= patch->hunks_size) - return GIT_ENOTFOUND; - else - return (int)patch->hunks[hunk_idx].line_count; -} - -int git_diff_patch_get_line_in_hunk( - char *line_origin, - const char **content, - size_t *content_len, - int *old_lineno, - int *new_lineno, - git_diff_patch *patch, - size_t hunk_idx, - size_t line_of_hunk) -{ - diff_patch_hunk *hunk; - diff_patch_line *line; - - assert(patch); - - if (hunk_idx >= patch->hunks_size) - goto notfound; - hunk = &patch->hunks[hunk_idx]; - - if (line_of_hunk >= hunk->line_count) - goto notfound; - - line = &patch->lines[hunk->line_start + line_of_hunk]; - - if (line_origin) *line_origin = line->origin; - if (content) *content = line->ptr; - if (content_len) *content_len = line->len; - if (old_lineno) *old_lineno = (int)line->oldno; - if (new_lineno) *new_lineno = (int)line->newno; - - return 0; - -notfound: - if (line_origin) *line_origin = GIT_DIFF_LINE_CONTEXT; - if (content) *content = NULL; - if (content_len) *content_len = 0; - if (old_lineno) *old_lineno = -1; - if (new_lineno) *new_lineno = -1; - - return GIT_ENOTFOUND; -} - -static int print_to_buffer_cb( - const git_diff_delta *delta, - const git_diff_range *range, - char line_origin, - const char *content, - size_t content_len, - void *payload) -{ - git_buf *output = payload; - GIT_UNUSED(delta); GIT_UNUSED(range); GIT_UNUSED(line_origin); - return git_buf_put(output, content, content_len); -} - -int git_diff_patch_print( - git_diff_patch *patch, - git_diff_data_cb print_cb, - void *payload) -{ - int error; - git_buf temp = GIT_BUF_INIT; - diff_print_info pi; - size_t h, l; - - assert(patch && print_cb); - - pi.diff = patch->diff; - pi.print_cb = print_cb; - pi.payload = payload; - pi.buf = &temp; - - error = print_patch_file(patch->delta, 0, &pi); - - for (h = 0; h < patch->hunks_size && !error; ++h) { - diff_patch_hunk *hunk = &patch->hunks[h]; - - error = print_patch_hunk( - patch->delta, &hunk->range, hunk->header, hunk->header_len, &pi); - - for (l = 0; l < hunk->line_count && !error; ++l) { - diff_patch_line *line = &patch->lines[hunk->line_start + l]; - - error = print_patch_line( - patch->delta, &hunk->range, - line->origin, line->ptr, line->len, &pi); - } - } - - git_buf_free(&temp); - - return error; -} - -int git_diff_patch_to_str( - char **string, - git_diff_patch *patch) -{ - int error; - git_buf output = GIT_BUF_INIT; - - error = git_diff_patch_print(patch, print_to_buffer_cb, &output); - - /* GIT_EUSER means git_buf_put in print_to_buffer_cb returned -1, - * meaning a memory allocation failure, so just map to -1... - */ - if (error == GIT_EUSER) - error = -1; - - *string = git_buf_detach(&output); - - return error; -} - -int git_diff__paired_foreach( - git_diff_list *idx2head, - git_diff_list *wd2idx, - int (*cb)(git_diff_delta *i2h, git_diff_delta *w2i, void *payload), - void *payload) -{ - int cmp; - git_diff_delta *i2h, *w2i; - size_t i, j, i_max, j_max; - int (*strcomp)(const char *, const char *); - - i_max = idx2head ? idx2head->deltas.length : 0; - j_max = wd2idx ? wd2idx->deltas.length : 0; - - /* Get appropriate strcmp function */ - strcomp = idx2head ? idx2head->strcomp : wd2idx ? wd2idx->strcomp : NULL; - - /* Assert both iterators use matching ignore-case. If this function ever - * supports merging diffs that are not sorted by the same function, then - * it will need to spool and sort on one of the results before merging - */ - if (idx2head && wd2idx) { - assert(idx2head->strcomp == wd2idx->strcomp); - } - - for (i = 0, j = 0; i < i_max || j < j_max; ) { - i2h = idx2head ? GIT_VECTOR_GET(&idx2head->deltas,i) : NULL; - w2i = wd2idx ? GIT_VECTOR_GET(&wd2idx->deltas,j) : NULL; - - cmp = !w2i ? -1 : !i2h ? 1 : - strcomp(i2h->old_file.path, w2i->old_file.path); - - if (cmp < 0) { - if (cb(i2h, NULL, payload)) - return GIT_EUSER; - i++; - } else if (cmp > 0) { - if (cb(NULL, w2i, payload)) - return GIT_EUSER; - j++; - } else { - if (cb(i2h, w2i, payload)) - return GIT_EUSER; - i++; j++; - } - } - - return 0; -} diff --git a/src/diff_output.h b/src/diff_output.h deleted file mode 100644 index 083355676..000000000 --- a/src/diff_output.h +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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_diff_output_h__ -#define INCLUDE_diff_output_h__ - -#include "git2/blob.h" -#include "diff.h" -#include "map.h" -#include "xdiff/xdiff.h" - -#define MAX_DIFF_FILESIZE 0x20000000 - -enum { - GIT_DIFF_PATCH_ALLOCATED = (1 << 0), - GIT_DIFF_PATCH_PREPPED = (1 << 1), - GIT_DIFF_PATCH_LOADED = (1 << 2), - GIT_DIFF_PATCH_DIFFABLE = (1 << 3), - GIT_DIFF_PATCH_DIFFED = (1 << 4), -}; - -/* context for performing diffs */ -typedef struct { - git_repository *repo; - git_diff_list *diff; - const git_diff_options *opts; - git_diff_file_cb file_cb; - git_diff_hunk_cb hunk_cb; - git_diff_data_cb data_cb; - void *payload; - int error; - git_diff_range range; - xdemitconf_t xdiff_config; - xpparam_t xdiff_params; -} diff_context; - -/* cached information about a single span in a diff */ -typedef struct diff_patch_line diff_patch_line; -struct diff_patch_line { - const char *ptr; - size_t len; - size_t lines, oldno, newno; - char origin; -}; - -/* cached information about a hunk in a diff */ -typedef struct diff_patch_hunk diff_patch_hunk; -struct diff_patch_hunk { - git_diff_range range; - char header[128]; - size_t header_len; - size_t line_start; - size_t line_count; -}; - -struct git_diff_patch { - git_refcount rc; - git_diff_list *diff; /* for refcount purposes, maybe NULL for blob diffs */ - git_diff_delta *delta; - diff_context *ctxt; /* only valid while generating patch */ - git_iterator_type_t old_src; - git_iterator_type_t new_src; - git_blob *old_blob; - git_blob *new_blob; - git_map old_data; - git_map new_data; - uint32_t flags; - diff_patch_hunk *hunks; - size_t hunks_asize, hunks_size; - diff_patch_line *lines; - size_t lines_asize, lines_size; - size_t oldno, newno; -}; - -/* context for performing diff on a single delta */ -typedef struct { - git_diff_patch *patch; - uint32_t prepped : 1; - uint32_t loaded : 1; - uint32_t diffable : 1; - uint32_t diffed : 1; -} diff_delta_context; - -extern int git_diff__paired_foreach( - git_diff_list *idx2head, - git_diff_list *wd2idx, - int (*cb)(git_diff_delta *i2h, git_diff_delta *w2i, void *payload), - void *payload); - -#endif diff --git a/src/diff_patch.c b/src/diff_patch.c new file mode 100644 index 000000000..cc45b6ddb --- /dev/null +++ b/src/diff_patch.c @@ -0,0 +1,1044 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#include "common.h" +#include "diff.h" +#include "diff_file.h" +#include "diff_driver.h" +#include "diff_patch.h" +#include "diff_xdiff.h" +#include "fileops.h" + +/* cached information about a single span in a diff */ +typedef struct diff_patch_line diff_patch_line; +struct diff_patch_line { + const char *ptr; + size_t len; + size_t lines, oldno, newno; + char origin; +}; + +/* cached information about a hunk in a diff */ +typedef struct diff_patch_hunk diff_patch_hunk; +struct diff_patch_hunk { + git_diff_range range; + char header[128]; + size_t header_len; + size_t line_start; + size_t line_count; +}; + +struct git_diff_patch { + git_refcount rc; + git_diff_list *diff; /* for refcount purposes, maybe NULL for blob diffs */ + git_diff_delta *delta; + size_t delta_index; + git_diff_file_content ofile; + git_diff_file_content nfile; + uint32_t flags; + git_array_t(diff_patch_hunk) hunks; + git_array_t(diff_patch_line) lines; + size_t oldno, newno; + size_t content_size, context_size, header_size; + git_pool flattened; +}; + +enum { + GIT_DIFF_PATCH_ALLOCATED = (1 << 0), + GIT_DIFF_PATCH_INITIALIZED = (1 << 1), + GIT_DIFF_PATCH_LOADED = (1 << 2), + GIT_DIFF_PATCH_DIFFABLE = (1 << 3), + GIT_DIFF_PATCH_DIFFED = (1 << 4), + GIT_DIFF_PATCH_FLATTENED = (1 << 5), +}; + +static void diff_output_init(git_diff_output*, const git_diff_options*, + git_diff_file_cb, git_diff_hunk_cb, git_diff_data_cb, void*); + +static void diff_output_to_patch(git_diff_output *, git_diff_patch *); + +static void diff_patch_update_binary(git_diff_patch *patch) +{ + if ((patch->delta->flags & DIFF_FLAGS_KNOWN_BINARY) != 0) + return; + + if ((patch->ofile.file->flags & GIT_DIFF_FLAG_BINARY) != 0 || + (patch->nfile.file->flags & GIT_DIFF_FLAG_BINARY) != 0) + patch->delta->flags |= GIT_DIFF_FLAG_BINARY; + + else if ((patch->ofile.file->flags & DIFF_FLAGS_NOT_BINARY) != 0 && + (patch->nfile.file->flags & DIFF_FLAGS_NOT_BINARY) != 0) + patch->delta->flags |= GIT_DIFF_FLAG_NOT_BINARY; +} + +static void diff_patch_init_common(git_diff_patch *patch) +{ + diff_patch_update_binary(patch); + + if ((patch->delta->flags & GIT_DIFF_FLAG_BINARY) != 0) + patch->flags |= GIT_DIFF_PATCH_LOADED; /* set LOADED but not DIFFABLE */ + + patch->flags |= GIT_DIFF_PATCH_INITIALIZED; + + if (patch->diff) + git_diff_list_addref(patch->diff); +} + +static int diff_patch_init_from_diff( + git_diff_patch *patch, git_diff_list *diff, size_t delta_index) +{ + int error = 0; + + memset(patch, 0, sizeof(*patch)); + patch->diff = diff; + patch->delta = git_vector_get(&diff->deltas, delta_index); + patch->delta_index = delta_index; + + if ((error = git_diff_file_content__init_from_diff( + &patch->ofile, diff, delta_index, true)) < 0 || + (error = git_diff_file_content__init_from_diff( + &patch->nfile, diff, delta_index, false)) < 0) + return error; + + diff_patch_init_common(patch); + + return 0; +} + +static int diff_patch_alloc_from_diff( + git_diff_patch **out, + git_diff_list *diff, + size_t delta_index) +{ + int error; + git_diff_patch *patch = git__calloc(1, sizeof(git_diff_patch)); + GITERR_CHECK_ALLOC(patch); + + if (!(error = diff_patch_init_from_diff(patch, diff, delta_index))) { + patch->flags |= GIT_DIFF_PATCH_ALLOCATED; + GIT_REFCOUNT_INC(patch); + } else { + git__free(patch); + patch = NULL; + } + + *out = patch; + return error; +} + +static int diff_patch_load(git_diff_patch *patch, git_diff_output *output) +{ + int error = 0; + bool incomplete_data; + + if ((patch->flags & GIT_DIFF_PATCH_LOADED) != 0) + return 0; + + /* if no hunk and data callbacks and user doesn't care if data looks + * binary, then there is no need to actually load the data + */ + if ((patch->ofile.opts_flags & GIT_DIFF_SKIP_BINARY_CHECK) != 0 && + output && !output->hunk_cb && !output->data_cb) + return 0; + + incomplete_data = + (((patch->ofile.flags & GIT_DIFF_FLAG__NO_DATA) != 0 || + (patch->ofile.file->flags & GIT_DIFF_FLAG_VALID_OID) != 0) && + ((patch->nfile.flags & GIT_DIFF_FLAG__NO_DATA) != 0 || + (patch->nfile.file->flags & GIT_DIFF_FLAG_VALID_OID) != 0)); + + /* always try to load workdir content first because filtering may + * need 2x data size and this minimizes peak memory footprint + */ + if (patch->ofile.src == GIT_ITERATOR_TYPE_WORKDIR) { + if ((error = git_diff_file_content__load(&patch->ofile)) < 0 || + (patch->ofile.file->flags & GIT_DIFF_FLAG_BINARY) != 0) + goto cleanup; + } + if (patch->nfile.src == GIT_ITERATOR_TYPE_WORKDIR) { + if ((error = git_diff_file_content__load(&patch->nfile)) < 0 || + (patch->nfile.file->flags & GIT_DIFF_FLAG_BINARY) != 0) + goto cleanup; + } + + /* once workdir has been tried, load other data as needed */ + if (patch->ofile.src != GIT_ITERATOR_TYPE_WORKDIR) { + if ((error = git_diff_file_content__load(&patch->ofile)) < 0 || + (patch->ofile.file->flags & GIT_DIFF_FLAG_BINARY) != 0) + goto cleanup; + } + if (patch->nfile.src != GIT_ITERATOR_TYPE_WORKDIR) { + if ((error = git_diff_file_content__load(&patch->nfile)) < 0 || + (patch->nfile.file->flags & GIT_DIFF_FLAG_BINARY) != 0) + goto cleanup; + } + + /* if previously missing an oid, and now that we have it the two sides + * are the same (and not submodules), update MODIFIED -> UNMODIFIED + */ + if (incomplete_data && + patch->ofile.file->mode == patch->nfile.file->mode && + patch->ofile.file->mode != GIT_FILEMODE_COMMIT && + git_oid_equal(&patch->ofile.file->oid, &patch->nfile.file->oid) && + patch->delta->status == GIT_DELTA_MODIFIED) /* not RENAMED/COPIED! */ + patch->delta->status = GIT_DELTA_UNMODIFIED; + +cleanup: + diff_patch_update_binary(patch); + + if (!error) { + /* patch is diffable only for non-binary, modified files where + * at least one side has data and the data actually changed + */ + if ((patch->delta->flags & GIT_DIFF_FLAG_BINARY) == 0 && + patch->delta->status != GIT_DELTA_UNMODIFIED && + (patch->ofile.map.len || patch->nfile.map.len) && + (patch->ofile.map.len != patch->nfile.map.len || + !git_oid_equal(&patch->ofile.file->oid, &patch->nfile.file->oid))) + patch->flags |= GIT_DIFF_PATCH_DIFFABLE; + + patch->flags |= GIT_DIFF_PATCH_LOADED; + } + + return error; +} + +static int diff_patch_file_callback( + git_diff_patch *patch, git_diff_output *output) +{ + float progress; + + if (!output->file_cb) + return 0; + + progress = patch->diff ? + ((float)patch->delta_index / patch->diff->deltas.length) : 1.0f; + + if (output->file_cb(patch->delta, progress, output->payload) != 0) + output->error = GIT_EUSER; + + return output->error; +} + +static int diff_patch_generate(git_diff_patch *patch, git_diff_output *output) +{ + int error = 0; + + if ((patch->flags & GIT_DIFF_PATCH_DIFFED) != 0) + return 0; + + /* if we are not looking at the hunks and lines, don't do the diff */ + if (!output->hunk_cb && !output->data_cb) + return 0; + + if ((patch->flags & GIT_DIFF_PATCH_LOADED) == 0 && + (error = diff_patch_load(patch, output)) < 0) + return error; + + if ((patch->flags & GIT_DIFF_PATCH_DIFFABLE) == 0) + return 0; + + if (output->diff_cb != NULL && + !(error = output->diff_cb(output, patch))) + patch->flags |= GIT_DIFF_PATCH_DIFFED; + + return error; +} + +static void diff_patch_free(git_diff_patch *patch) +{ + git_diff_file_content__clear(&patch->ofile); + git_diff_file_content__clear(&patch->nfile); + + git_array_clear(patch->lines); + git_array_clear(patch->hunks); + + git_diff_list_free(patch->diff); /* decrements refcount */ + patch->diff = NULL; + + git_pool_clear(&patch->flattened); + + if (patch->flags & GIT_DIFF_PATCH_ALLOCATED) + git__free(patch); +} + +static int diff_required(git_diff_list *diff, const char *action) +{ + if (diff) + return 0; + giterr_set(GITERR_INVALID, "Must provide valid diff to %s", action); + return -1; +} + +int git_diff_foreach( + git_diff_list *diff, + git_diff_file_cb file_cb, + git_diff_hunk_cb hunk_cb, + git_diff_data_cb data_cb, + void *payload) +{ + int error = 0; + git_xdiff_output xo; + size_t idx; + git_diff_patch patch; + + if (diff_required(diff, "git_diff_foreach") < 0) + return -1; + + diff_output_init( + &xo.output, &diff->opts, file_cb, hunk_cb, data_cb, payload); + git_xdiff_init(&xo, &diff->opts); + + git_vector_foreach(&diff->deltas, idx, patch.delta) { + + /* check flags against patch status */ + if (git_diff_delta__should_skip(&diff->opts, patch.delta)) + continue; + + if (!(error = diff_patch_init_from_diff(&patch, diff, idx))) { + + error = diff_patch_file_callback(&patch, &xo.output); + + if (!error) + error = diff_patch_generate(&patch, &xo.output); + + git_diff_patch_free(&patch); + } + + if (error < 0) + break; + } + + if (error == GIT_EUSER) + giterr_clear(); /* don't leave error message set invalidly */ + return error; +} + +typedef struct { + git_diff_patch patch; + git_diff_delta delta; + char paths[GIT_FLEX_ARRAY]; +} diff_patch_with_delta; + +static int diff_single_generate(diff_patch_with_delta *pd, git_xdiff_output *xo) +{ + int error = 0; + git_diff_patch *patch = &pd->patch; + bool has_old = ((patch->ofile.flags & GIT_DIFF_FLAG__NO_DATA) == 0); + bool has_new = ((patch->nfile.flags & GIT_DIFF_FLAG__NO_DATA) == 0); + + pd->delta.status = has_new ? + (has_old ? GIT_DELTA_MODIFIED : GIT_DELTA_ADDED) : + (has_old ? GIT_DELTA_DELETED : GIT_DELTA_UNTRACKED); + + if (git_oid_equal(&patch->nfile.file->oid, &patch->ofile.file->oid)) + pd->delta.status = GIT_DELTA_UNMODIFIED; + + patch->delta = &pd->delta; + + diff_patch_init_common(patch); + + if (pd->delta.status == GIT_DELTA_UNMODIFIED && + !(patch->ofile.opts_flags & GIT_DIFF_INCLUDE_UNMODIFIED)) + return error; + + error = diff_patch_file_callback(patch, (git_diff_output *)xo); + + if (!error) + error = diff_patch_generate(patch, (git_diff_output *)xo); + + if (error == GIT_EUSER) + giterr_clear(); /* don't leave error message set invalidly */ + + return error; +} + +static int diff_patch_from_blobs( + diff_patch_with_delta *pd, + git_xdiff_output *xo, + const git_blob *old_blob, + const char *old_path, + const git_blob *new_blob, + const char *new_path, + const git_diff_options *opts) +{ + int error = 0; + git_repository *repo = + new_blob ? git_object_owner((const git_object *)new_blob) : + old_blob ? git_object_owner((const git_object *)old_blob) : NULL; + + GITERR_CHECK_VERSION(opts, GIT_DIFF_OPTIONS_VERSION, "git_diff_options"); + + if (opts && (opts->flags & GIT_DIFF_REVERSE) != 0) { + const git_blob *tmp_blob; + const char *tmp_path; + tmp_blob = old_blob; old_blob = new_blob; new_blob = tmp_blob; + tmp_path = old_path; old_path = new_path; new_path = tmp_path; + } + + pd->patch.delta = &pd->delta; + + pd->delta.old_file.path = old_path; + pd->delta.new_file.path = new_path; + + if ((error = git_diff_file_content__init_from_blob( + &pd->patch.ofile, repo, opts, old_blob, &pd->delta.old_file)) < 0 || + (error = git_diff_file_content__init_from_blob( + &pd->patch.nfile, repo, opts, new_blob, &pd->delta.new_file)) < 0) + return error; + + return diff_single_generate(pd, xo); +} + +static int diff_patch_with_delta_alloc( + diff_patch_with_delta **out, + const char **old_path, + const char **new_path) +{ + diff_patch_with_delta *pd; + size_t old_len = *old_path ? strlen(*old_path) : 0; + size_t new_len = *new_path ? strlen(*new_path) : 0; + + *out = pd = git__calloc(1, sizeof(*pd) + old_len + new_len + 2); + GITERR_CHECK_ALLOC(pd); + + pd->patch.flags = GIT_DIFF_PATCH_ALLOCATED; + + if (*old_path) { + memcpy(&pd->paths[0], *old_path, old_len); + *old_path = &pd->paths[0]; + } else if (*new_path) + *old_path = &pd->paths[old_len + 1]; + + if (*new_path) { + memcpy(&pd->paths[old_len + 1], *new_path, new_len); + *new_path = &pd->paths[old_len + 1]; + } else if (*old_path) + *new_path = &pd->paths[0]; + + return 0; +} + +int git_diff_blobs( + const git_blob *old_blob, + const char *old_path, + const git_blob *new_blob, + const char *new_path, + const git_diff_options *opts, + git_diff_file_cb file_cb, + git_diff_hunk_cb hunk_cb, + git_diff_data_cb data_cb, + void *payload) +{ + int error = 0; + diff_patch_with_delta pd; + git_xdiff_output xo; + + memset(&pd, 0, sizeof(pd)); + memset(&xo, 0, sizeof(xo)); + + diff_output_init( + &xo.output, opts, file_cb, hunk_cb, data_cb, payload); + git_xdiff_init(&xo, opts); + + if (!old_path && new_path) + old_path = new_path; + else if (!new_path && old_path) + new_path = old_path; + + error = diff_patch_from_blobs( + &pd, &xo, old_blob, old_path, new_blob, new_path, opts); + + git_diff_patch_free(&pd.patch); + + return error; +} + +int git_diff_patch_from_blobs( + git_diff_patch **out, + const git_blob *old_blob, + const char *old_path, + const git_blob *new_blob, + const char *new_path, + const git_diff_options *opts) +{ + int error = 0; + diff_patch_with_delta *pd; + git_xdiff_output xo; + + assert(out); + *out = NULL; + + if (diff_patch_with_delta_alloc(&pd, &old_path, &new_path) < 0) + return -1; + + memset(&xo, 0, sizeof(xo)); + + diff_output_to_patch(&xo.output, &pd->patch); + git_xdiff_init(&xo, opts); + + error = diff_patch_from_blobs( + pd, &xo, old_blob, old_path, new_blob, new_path, opts); + + if (!error) + *out = (git_diff_patch *)pd; + else + git_diff_patch_free((git_diff_patch *)pd); + + return error; +} + +static int diff_patch_from_blob_and_buffer( + diff_patch_with_delta *pd, + git_xdiff_output *xo, + const git_blob *old_blob, + const char *old_path, + const char *buf, + size_t buflen, + const char *buf_path, + const git_diff_options *opts) +{ + int error = 0; + git_repository *repo = + old_blob ? git_object_owner((const git_object *)old_blob) : NULL; + + GITERR_CHECK_VERSION(opts, GIT_DIFF_OPTIONS_VERSION, "git_diff_options"); + + pd->patch.delta = &pd->delta; + + if (opts && (opts->flags & GIT_DIFF_REVERSE) != 0) { + pd->delta.old_file.path = buf_path; + pd->delta.new_file.path = old_path; + + if (!(error = git_diff_file_content__init_from_raw( + &pd->patch.ofile, repo, opts, buf, buflen, &pd->delta.old_file))) + error = git_diff_file_content__init_from_blob( + &pd->patch.nfile, repo, opts, old_blob, &pd->delta.new_file); + } else { + pd->delta.old_file.path = old_path; + pd->delta.new_file.path = buf_path; + + if (!(error = git_diff_file_content__init_from_blob( + &pd->patch.ofile, repo, opts, old_blob, &pd->delta.old_file))) + error = git_diff_file_content__init_from_raw( + &pd->patch.nfile, repo, opts, buf, buflen, &pd->delta.new_file); + } + + if (error < 0) + return error; + + return diff_single_generate(pd, xo); +} + +int git_diff_blob_to_buffer( + const git_blob *old_blob, + const char *old_path, + const char *buf, + size_t buflen, + const char *buf_path, + const git_diff_options *opts, + git_diff_file_cb file_cb, + git_diff_hunk_cb hunk_cb, + git_diff_data_cb data_cb, + void *payload) +{ + int error = 0; + diff_patch_with_delta pd; + git_xdiff_output xo; + + memset(&pd, 0, sizeof(pd)); + memset(&xo, 0, sizeof(xo)); + + diff_output_init( + &xo.output, opts, file_cb, hunk_cb, data_cb, payload); + git_xdiff_init(&xo, opts); + + if (!old_path && buf_path) + old_path = buf_path; + else if (!buf_path && old_path) + buf_path = old_path; + + error = diff_patch_from_blob_and_buffer( + &pd, &xo, old_blob, old_path, buf, buflen, buf_path, opts); + + git_diff_patch_free(&pd.patch); + + return error; +} + +int git_diff_patch_from_blob_and_buffer( + git_diff_patch **out, + const git_blob *old_blob, + const char *old_path, + const char *buf, + size_t buflen, + const char *buf_path, + const git_diff_options *opts) +{ + int error = 0; + diff_patch_with_delta *pd; + git_xdiff_output xo; + + assert(out); + *out = NULL; + + if (diff_patch_with_delta_alloc(&pd, &old_path, &buf_path) < 0) + return -1; + + memset(&xo, 0, sizeof(xo)); + + diff_output_to_patch(&xo.output, &pd->patch); + git_xdiff_init(&xo, opts); + + error = diff_patch_from_blob_and_buffer( + pd, &xo, old_blob, old_path, buf, buflen, buf_path, opts); + + if (!error) + *out = (git_diff_patch *)pd; + else + git_diff_patch_free((git_diff_patch *)pd); + + return error; +} + +int git_diff_get_patch( + git_diff_patch **patch_ptr, + const git_diff_delta **delta_ptr, + git_diff_list *diff, + size_t idx) +{ + int error = 0; + git_xdiff_output xo; + git_diff_delta *delta = NULL; + git_diff_patch *patch = NULL; + + if (patch_ptr) *patch_ptr = NULL; + if (delta_ptr) *delta_ptr = NULL; + + if (diff_required(diff, "git_diff_get_patch") < 0) + return -1; + + delta = git_vector_get(&diff->deltas, idx); + if (!delta) { + giterr_set(GITERR_INVALID, "Index out of range for delta in diff"); + return GIT_ENOTFOUND; + } + + if (delta_ptr) + *delta_ptr = delta; + + if (git_diff_delta__should_skip(&diff->opts, delta)) + return 0; + + /* don't load the patch data unless we need it for binary check */ + if (!patch_ptr && + ((delta->flags & DIFF_FLAGS_KNOWN_BINARY) != 0 || + (diff->opts.flags & GIT_DIFF_SKIP_BINARY_CHECK) != 0)) + return 0; + + if ((error = diff_patch_alloc_from_diff(&patch, diff, idx)) < 0) + return error; + + diff_output_to_patch(&xo.output, patch); + git_xdiff_init(&xo, &diff->opts); + + error = diff_patch_file_callback(patch, &xo.output); + + if (!error) + error = diff_patch_generate(patch, &xo.output); + + if (!error) { + /* if cumulative diff size is < 0.5 total size, flatten the patch */ + /* unload the file content */ + } + + if (error || !patch_ptr) + git_diff_patch_free(patch); + else + *patch_ptr = patch; + + if (error == GIT_EUSER) + giterr_clear(); /* don't leave error message set invalidly */ + return error; +} + +void git_diff_patch_free(git_diff_patch *patch) +{ + if (patch) + GIT_REFCOUNT_DEC(patch, diff_patch_free); +} + +const git_diff_delta *git_diff_patch_delta(git_diff_patch *patch) +{ + assert(patch); + return patch->delta; +} + +size_t git_diff_patch_num_hunks(git_diff_patch *patch) +{ + assert(patch); + return git_array_size(patch->hunks); +} + +int git_diff_patch_line_stats( + size_t *total_ctxt, + size_t *total_adds, + size_t *total_dels, + const git_diff_patch *patch) +{ + size_t totals[3], idx; + + memset(totals, 0, sizeof(totals)); + + for (idx = 0; idx < git_array_size(patch->lines); ++idx) { + diff_patch_line *line = git_array_get(patch->lines, idx); + if (!line) + continue; + + switch (line->origin) { + case GIT_DIFF_LINE_CONTEXT: totals[0]++; break; + case GIT_DIFF_LINE_ADDITION: totals[1]++; break; + case GIT_DIFF_LINE_DELETION: totals[2]++; break; + default: + /* diff --stat and --numstat don't count EOFNL marks because + * they will always be paired with a ADDITION or DELETION line. + */ + break; + } + } + + if (total_ctxt) + *total_ctxt = totals[0]; + if (total_adds) + *total_adds = totals[1]; + if (total_dels) + *total_dels = totals[2]; + + return 0; +} + +static int diff_error_outofrange(const char *thing) +{ + giterr_set(GITERR_INVALID, "Diff patch %s index out of range", thing); + return GIT_ENOTFOUND; +} + +int git_diff_patch_get_hunk( + const git_diff_range **range, + const char **header, + size_t *header_len, + size_t *lines_in_hunk, + git_diff_patch *patch, + size_t hunk_idx) +{ + diff_patch_hunk *hunk; + assert(patch); + + hunk = git_array_get(patch->hunks, hunk_idx); + + if (!hunk) { + if (range) *range = NULL; + if (header) *header = NULL; + if (header_len) *header_len = 0; + if (lines_in_hunk) *lines_in_hunk = 0; + return diff_error_outofrange("hunk"); + } + + if (range) *range = &hunk->range; + if (header) *header = hunk->header; + if (header_len) *header_len = hunk->header_len; + if (lines_in_hunk) *lines_in_hunk = hunk->line_count; + return 0; +} + +int git_diff_patch_num_lines_in_hunk(git_diff_patch *patch, size_t hunk_idx) +{ + diff_patch_hunk *hunk; + assert(patch); + + if (!(hunk = git_array_get(patch->hunks, hunk_idx))) + return diff_error_outofrange("hunk"); + return (int)hunk->line_count; +} + +int git_diff_patch_get_line_in_hunk( + char *line_origin, + const char **content, + size_t *content_len, + int *old_lineno, + int *new_lineno, + git_diff_patch *patch, + size_t hunk_idx, + size_t line_of_hunk) +{ + diff_patch_hunk *hunk; + diff_patch_line *line; + const char *thing; + + assert(patch); + + if (!(hunk = git_array_get(patch->hunks, hunk_idx))) { + thing = "hunk"; + goto notfound; + } + + if (line_of_hunk >= hunk->line_count || + !(line = git_array_get( + patch->lines, hunk->line_start + line_of_hunk))) { + thing = "line"; + goto notfound; + } + + if (line_origin) *line_origin = line->origin; + if (content) *content = line->ptr; + if (content_len) *content_len = line->len; + if (old_lineno) *old_lineno = (int)line->oldno; + if (new_lineno) *new_lineno = (int)line->newno; + + return 0; + +notfound: + if (line_origin) *line_origin = GIT_DIFF_LINE_CONTEXT; + if (content) *content = NULL; + if (content_len) *content_len = 0; + if (old_lineno) *old_lineno = -1; + if (new_lineno) *new_lineno = -1; + + return diff_error_outofrange(thing); +} + +size_t git_diff_patch_size( + git_diff_patch *patch, + int include_context, + int include_hunk_headers, + int include_file_headers) +{ + size_t out; + + assert(patch); + + out = patch->content_size; + + if (!include_context) + out -= patch->context_size; + + if (include_hunk_headers) + out += patch->header_size; + + if (include_file_headers) { + git_buf file_header = GIT_BUF_INIT; + + if (git_diff_delta__format_file_header( + &file_header, patch->delta, NULL, NULL, 0) < 0) + giterr_clear(); + else + out += git_buf_len(&file_header); + + git_buf_free(&file_header); + } + + return out; +} + +git_diff_list *git_diff_patch__diff(git_diff_patch *patch) +{ + return patch->diff; +} + +git_diff_driver *git_diff_patch__driver(git_diff_patch *patch) +{ + /* ofile driver is representative for whole patch */ + return patch->ofile.driver; +} + +void git_diff_patch__old_data( + char **ptr, size_t *len, git_diff_patch *patch) +{ + *ptr = patch->ofile.map.data; + *len = patch->ofile.map.len; +} + +void git_diff_patch__new_data( + char **ptr, size_t *len, git_diff_patch *patch) +{ + *ptr = patch->nfile.map.data; + *len = patch->nfile.map.len; +} + +int git_diff_patch__invoke_callbacks( + git_diff_patch *patch, + git_diff_file_cb file_cb, + git_diff_hunk_cb hunk_cb, + git_diff_data_cb line_cb, + void *payload) +{ + int error = 0; + uint32_t i, j; + + if (file_cb) + error = file_cb(patch->delta, 0, payload); + + if (!hunk_cb && !line_cb) + return error; + + for (i = 0; !error && i < git_array_size(patch->hunks); ++i) { + diff_patch_hunk *h = git_array_get(patch->hunks, i); + + error = hunk_cb( + patch->delta, &h->range, h->header, h->header_len, payload); + + if (!line_cb) + continue; + + for (j = 0; !error && j < h->line_count; ++j) { + diff_patch_line *l = + git_array_get(patch->lines, h->line_start + j); + + error = line_cb( + patch->delta, &h->range, l->origin, l->ptr, l->len, payload); + } + } + + return error; +} + + +static int diff_patch_file_cb( + const git_diff_delta *delta, + float progress, + void *payload) +{ + GIT_UNUSED(delta); GIT_UNUSED(progress); GIT_UNUSED(payload); + return 0; +} + +static int diff_patch_hunk_cb( + const git_diff_delta *delta, + const git_diff_range *range, + const char *header, + size_t header_len, + void *payload) +{ + git_diff_patch *patch = payload; + diff_patch_hunk *hunk; + + GIT_UNUSED(delta); + + hunk = git_array_alloc(patch->hunks); + GITERR_CHECK_ALLOC(hunk); + + memcpy(&hunk->range, range, sizeof(hunk->range)); + + assert(header_len + 1 < sizeof(hunk->header)); + memcpy(&hunk->header, header, header_len); + hunk->header[header_len] = '\0'; + hunk->header_len = header_len; + + patch->header_size += header_len; + + hunk->line_start = git_array_size(patch->lines); + hunk->line_count = 0; + + patch->oldno = range->old_start; + patch->newno = range->new_start; + + return 0; +} + +static int diff_patch_line_cb( + const git_diff_delta *delta, + const git_diff_range *range, + char line_origin, + const char *content, + size_t content_len, + void *payload) +{ + git_diff_patch *patch = payload; + diff_patch_hunk *hunk; + diff_patch_line *line; + const char *content_end = content + content_len; + + GIT_UNUSED(delta); + GIT_UNUSED(range); + + hunk = git_array_last(patch->hunks); + GITERR_CHECK_ALLOC(hunk); + + line = git_array_alloc(patch->lines); + GITERR_CHECK_ALLOC(line); + + line->ptr = content; + line->len = content_len; + line->origin = line_origin; + + /* do some bookkeeping so we can provide old/new line numbers */ + + line->lines = 0; + while (content < content_end) + if (*content++ == '\n') + ++line->lines; + + patch->content_size += content_len; + + switch (line_origin) { + case GIT_DIFF_LINE_ADDITION: + patch->content_size += 1; + case GIT_DIFF_LINE_DEL_EOFNL: + line->oldno = -1; + line->newno = patch->newno; + patch->newno += line->lines; + break; + case GIT_DIFF_LINE_DELETION: + patch->content_size += 1; + case GIT_DIFF_LINE_ADD_EOFNL: + line->oldno = patch->oldno; + line->newno = -1; + patch->oldno += line->lines; + break; + case GIT_DIFF_LINE_CONTEXT: + patch->content_size += 1; + patch->context_size += 1; + case GIT_DIFF_LINE_CONTEXT_EOFNL: + patch->context_size += content_len; + line->oldno = patch->oldno; + line->newno = patch->newno; + patch->oldno += line->lines; + patch->newno += line->lines; + break; + default: + assert(false); + break; + } + + hunk->line_count++; + + return 0; +} + +static void diff_output_init( + git_diff_output *out, + const git_diff_options *opts, + git_diff_file_cb file_cb, + git_diff_hunk_cb hunk_cb, + git_diff_data_cb data_cb, + void *payload) +{ + GIT_UNUSED(opts); + + memset(out, 0, sizeof(*out)); + + out->file_cb = file_cb; + out->hunk_cb = hunk_cb; + out->data_cb = data_cb; + out->payload = payload; +} + +static void diff_output_to_patch(git_diff_output *out, git_diff_patch *patch) +{ + diff_output_init( + out, NULL, + diff_patch_file_cb, diff_patch_hunk_cb, diff_patch_line_cb, patch); +} diff --git a/src/diff_patch.h b/src/diff_patch.h new file mode 100644 index 000000000..56af14600 --- /dev/null +++ b/src/diff_patch.h @@ -0,0 +1,46 @@ +/* + * 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_diff_patch_h__ +#define INCLUDE_diff_patch_h__ + +#include "common.h" +#include "diff.h" +#include "diff_file.h" +#include "array.h" + +extern git_diff_list *git_diff_patch__diff(git_diff_patch *); + +extern git_diff_driver *git_diff_patch__driver(git_diff_patch *); + +extern void git_diff_patch__old_data(char **, size_t *, git_diff_patch *); +extern void git_diff_patch__new_data(char **, size_t *, git_diff_patch *); + +extern int git_diff_patch__invoke_callbacks( + git_diff_patch *patch, + git_diff_file_cb file_cb, + git_diff_hunk_cb hunk_cb, + git_diff_data_cb line_cb, + void *payload); + +typedef struct git_diff_output git_diff_output; +struct git_diff_output { + /* these callbacks are issued with the diff data */ + git_diff_file_cb file_cb; + git_diff_hunk_cb hunk_cb; + git_diff_data_cb data_cb; + void *payload; + + /* this records the actual error in cases where it may be obscured */ + int error; + + /* this callback is used to do the diff and drive the other callbacks. + * see diff_xdiff.h for how to use this in practice for now. + */ + int (*diff_cb)(git_diff_output *output, git_diff_patch *patch); +}; + +#endif diff --git a/src/diff_print.c b/src/diff_print.c new file mode 100644 index 000000000..4ddd72443 --- /dev/null +++ b/src/diff_print.c @@ -0,0 +1,456 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#include "common.h" +#include "diff.h" +#include "diff_patch.h" +#include "buffer.h" + +typedef struct { + git_diff_list *diff; + git_diff_data_cb print_cb; + void *payload; + git_buf *buf; + int oid_strlen; +} diff_print_info; + +static int diff_print_info_init( + diff_print_info *pi, + git_buf *out, git_diff_list *diff, git_diff_data_cb cb, void *payload) +{ + pi->diff = diff; + pi->print_cb = cb; + pi->payload = payload; + pi->buf = out; + + if (!diff || !diff->repo) + pi->oid_strlen = GIT_ABBREV_DEFAULT; + else if (git_repository__cvar( + &pi->oid_strlen, diff->repo, GIT_CVAR_ABBREV) < 0) + return -1; + + pi->oid_strlen += 1; /* for NUL byte */ + + if (pi->oid_strlen < 2) + pi->oid_strlen = 2; + else if (pi->oid_strlen > GIT_OID_HEXSZ + 1) + pi->oid_strlen = GIT_OID_HEXSZ + 1; + + return 0; +} + +static char diff_pick_suffix(int mode) +{ + if (S_ISDIR(mode)) + return '/'; + else if (mode & 0100) /* -V536 */ + /* in git, modes are very regular, so we must have 0100755 mode */ + return '*'; + else + return ' '; +} + +char git_diff_status_char(git_delta_t status) +{ + char code; + + switch (status) { + case GIT_DELTA_ADDED: code = 'A'; break; + case GIT_DELTA_DELETED: code = 'D'; break; + case GIT_DELTA_MODIFIED: code = 'M'; break; + case GIT_DELTA_RENAMED: code = 'R'; break; + case GIT_DELTA_COPIED: code = 'C'; break; + case GIT_DELTA_IGNORED: code = 'I'; break; + case GIT_DELTA_UNTRACKED: code = '?'; break; + default: code = ' '; break; + } + + return code; +} + +static int callback_error(void) +{ + giterr_clear(); + return GIT_EUSER; +} + +static int diff_print_one_compact( + const git_diff_delta *delta, float progress, void *data) +{ + diff_print_info *pi = data; + git_buf *out = pi->buf; + char old_suffix, new_suffix, code = git_diff_status_char(delta->status); + int (*strcomp)(const char *, const char *) = + pi->diff ? pi->diff->strcomp : git__strcmp; + + GIT_UNUSED(progress); + + if (code == ' ') + return 0; + + old_suffix = diff_pick_suffix(delta->old_file.mode); + new_suffix = diff_pick_suffix(delta->new_file.mode); + + git_buf_clear(out); + + if (delta->old_file.path != delta->new_file.path && + strcomp(delta->old_file.path,delta->new_file.path) != 0) + git_buf_printf(out, "%c\t%s%c %s%c\n", code, + delta->old_file.path, old_suffix, delta->new_file.path, new_suffix); + else if (delta->old_file.mode != delta->new_file.mode && + delta->old_file.mode != 0 && delta->new_file.mode != 0) + git_buf_printf(out, "%c\t%s%c %s%c\n", code, + delta->old_file.path, old_suffix, delta->new_file.path, new_suffix); + else if (old_suffix != ' ') + git_buf_printf(out, "%c\t%s%c\n", code, delta->old_file.path, old_suffix); + else + git_buf_printf(out, "%c\t%s\n", code, delta->old_file.path); + + if (git_buf_oom(out)) + return -1; + + if (pi->print_cb(delta, NULL, GIT_DIFF_LINE_FILE_HDR, + git_buf_cstr(out), git_buf_len(out), pi->payload)) + return callback_error(); + + return 0; +} + +/* print a git_diff_list to a print callback in compact format */ +int git_diff_print_compact( + git_diff_list *diff, + git_diff_data_cb print_cb, + void *payload) +{ + int error; + git_buf buf = GIT_BUF_INIT; + diff_print_info pi; + + if (!(error = diff_print_info_init(&pi, &buf, diff, print_cb, payload))) + error = git_diff_foreach(diff, diff_print_one_compact, NULL, NULL, &pi); + + git_buf_free(&buf); + + return error; +} + +static int diff_print_one_raw( + const git_diff_delta *delta, float progress, void *data) +{ + diff_print_info *pi = data; + git_buf *out = pi->buf; + char code = git_diff_status_char(delta->status); + char start_oid[GIT_OID_HEXSZ+1], end_oid[GIT_OID_HEXSZ+1]; + + GIT_UNUSED(progress); + + if (code == ' ') + return 0; + + git_buf_clear(out); + + git_oid_tostr(start_oid, pi->oid_strlen, &delta->old_file.oid); + git_oid_tostr(end_oid, pi->oid_strlen, &delta->new_file.oid); + + git_buf_printf( + out, ":%06o %06o %s... %s... %c", + delta->old_file.mode, delta->new_file.mode, start_oid, end_oid, code); + + if (delta->similarity > 0) + git_buf_printf(out, "%03u", delta->similarity); + + if (delta->old_file.path != delta->new_file.path) + git_buf_printf( + out, "\t%s %s\n", delta->old_file.path, delta->new_file.path); + else + git_buf_printf( + out, "\t%s\n", delta->old_file.path ? + delta->old_file.path : delta->new_file.path); + + if (git_buf_oom(out)) + return -1; + + if (pi->print_cb(delta, NULL, GIT_DIFF_LINE_FILE_HDR, + git_buf_cstr(out), git_buf_len(out), pi->payload)) + return callback_error(); + + return 0; +} + +/* print a git_diff_list to a print callback in raw output format */ +int git_diff_print_raw( + git_diff_list *diff, + git_diff_data_cb print_cb, + void *payload) +{ + int error; + git_buf buf = GIT_BUF_INIT; + diff_print_info pi; + + if (!(error = diff_print_info_init(&pi, &buf, diff, print_cb, payload))) + error = git_diff_foreach(diff, diff_print_one_raw, NULL, NULL, &pi); + + git_buf_free(&buf); + + return error; +} + +static int diff_print_oid_range( + git_buf *out, const git_diff_delta *delta, int oid_strlen) +{ + char start_oid[GIT_OID_HEXSZ+1], end_oid[GIT_OID_HEXSZ+1]; + + git_oid_tostr(start_oid, oid_strlen, &delta->old_file.oid); + git_oid_tostr(end_oid, oid_strlen, &delta->new_file.oid); + + /* TODO: Match git diff more closely */ + if (delta->old_file.mode == delta->new_file.mode) { + git_buf_printf(out, "index %s..%s %o\n", + start_oid, end_oid, delta->old_file.mode); + } else { + if (delta->old_file.mode == 0) { + git_buf_printf(out, "new file mode %o\n", delta->new_file.mode); + } else if (delta->new_file.mode == 0) { + git_buf_printf(out, "deleted file mode %o\n", delta->old_file.mode); + } else { + git_buf_printf(out, "old mode %o\n", delta->old_file.mode); + git_buf_printf(out, "new mode %o\n", delta->new_file.mode); + } + git_buf_printf(out, "index %s..%s\n", start_oid, end_oid); + } + + if (git_buf_oom(out)) + return -1; + + return 0; +} + +static int diff_delta_format_with_paths( + git_buf *out, + const git_diff_delta *delta, + const char *oldpfx, + const char *newpfx, + const char *template) +{ + const char *oldpath = delta->old_file.path; + const char *newpath = delta->new_file.path; + + if (git_oid_iszero(&delta->old_file.oid)) { + oldpfx = ""; + oldpath = "/dev/null"; + } + if (git_oid_iszero(&delta->new_file.oid)) { + newpfx = ""; + newpath = "/dev/null"; + } + + return git_buf_printf(out, template, oldpfx, oldpath, newpfx, newpath); +} + +int git_diff_delta__format_file_header( + git_buf *out, + const git_diff_delta *delta, + const char *oldpfx, + const char *newpfx, + int oid_strlen) +{ + if (!oldpfx) + oldpfx = DIFF_OLD_PREFIX_DEFAULT; + if (!newpfx) + newpfx = DIFF_NEW_PREFIX_DEFAULT; + if (!oid_strlen) + oid_strlen = GIT_ABBREV_DEFAULT + 1; + + git_buf_clear(out); + + git_buf_printf(out, "diff --git %s%s %s%s\n", + oldpfx, delta->old_file.path, newpfx, delta->new_file.path); + + if (diff_print_oid_range(out, delta, oid_strlen) < 0) + return -1; + + if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0) + diff_delta_format_with_paths( + out, delta, oldpfx, newpfx, "--- %s%s\n+++ %s%s\n"); + + return git_buf_oom(out) ? -1 : 0; +} + +static int diff_print_patch_file( + const git_diff_delta *delta, float progress, void *data) +{ + diff_print_info *pi = data; + const char *oldpfx = + pi->diff ? pi->diff->opts.old_prefix : DIFF_OLD_PREFIX_DEFAULT; + const char *newpfx = + pi->diff ? pi->diff->opts.new_prefix : DIFF_NEW_PREFIX_DEFAULT; + uint32_t opts_flags = pi->diff ? pi->diff->opts.flags : GIT_DIFF_NORMAL; + + GIT_UNUSED(progress); + + if (S_ISDIR(delta->new_file.mode) || + delta->status == GIT_DELTA_UNMODIFIED || + delta->status == GIT_DELTA_IGNORED || + (delta->status == GIT_DELTA_UNTRACKED && + (opts_flags & GIT_DIFF_INCLUDE_UNTRACKED_CONTENT) == 0)) + return 0; + + if (git_diff_delta__format_file_header( + pi->buf, delta, oldpfx, newpfx, pi->oid_strlen) < 0) + return -1; + + if (pi->print_cb(delta, NULL, GIT_DIFF_LINE_FILE_HDR, + git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) + return callback_error(); + + if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0) + return 0; + + git_buf_clear(pi->buf); + + if (diff_delta_format_with_paths( + pi->buf, delta, oldpfx, newpfx, + "Binary files %s%s and %s%s differ\n") < 0) + return -1; + + if (pi->print_cb(delta, NULL, GIT_DIFF_LINE_BINARY, + git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) + return callback_error(); + + return 0; +} + +static int diff_print_patch_hunk( + const git_diff_delta *d, + const git_diff_range *r, + const char *header, + size_t header_len, + void *data) +{ + diff_print_info *pi = data; + + if (S_ISDIR(d->new_file.mode)) + return 0; + + git_buf_clear(pi->buf); + if (git_buf_printf(pi->buf, "%.*s", (int)header_len, header) < 0) + return -1; + + if (pi->print_cb(d, r, GIT_DIFF_LINE_HUNK_HDR, + git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) + return callback_error(); + + return 0; +} + +static int diff_print_patch_line( + const git_diff_delta *delta, + const git_diff_range *range, + char line_origin, /* GIT_DIFF_LINE value from above */ + const char *content, + size_t content_len, + void *data) +{ + diff_print_info *pi = data; + + if (S_ISDIR(delta->new_file.mode)) + return 0; + + git_buf_clear(pi->buf); + + if (line_origin == GIT_DIFF_LINE_ADDITION || + line_origin == GIT_DIFF_LINE_DELETION || + line_origin == GIT_DIFF_LINE_CONTEXT) + git_buf_printf(pi->buf, "%c%.*s", line_origin, (int)content_len, content); + else if (content_len > 0) + git_buf_printf(pi->buf, "%.*s", (int)content_len, content); + + if (git_buf_oom(pi->buf)) + return -1; + + if (pi->print_cb(delta, range, line_origin, + git_buf_cstr(pi->buf), git_buf_len(pi->buf), pi->payload)) + return callback_error(); + + return 0; +} + +/* print a git_diff_list to an output callback in patch format */ +int git_diff_print_patch( + git_diff_list *diff, + git_diff_data_cb print_cb, + void *payload) +{ + int error; + git_buf buf = GIT_BUF_INIT; + diff_print_info pi; + + if (!(error = diff_print_info_init(&pi, &buf, diff, print_cb, payload))) + error = git_diff_foreach( + diff, diff_print_patch_file, diff_print_patch_hunk, + diff_print_patch_line, &pi); + + git_buf_free(&buf); + + return error; +} + +/* print a git_diff_patch to an output callback */ +int git_diff_patch_print( + git_diff_patch *patch, + git_diff_data_cb print_cb, + void *payload) +{ + int error; + git_buf temp = GIT_BUF_INIT; + diff_print_info pi; + + assert(patch && print_cb); + + if (!(error = diff_print_info_init( + &pi, &temp, git_diff_patch__diff(patch), print_cb, payload))) + error = git_diff_patch__invoke_callbacks( + patch, diff_print_patch_file, diff_print_patch_hunk, + diff_print_patch_line, &pi); + + git_buf_free(&temp); + + return error; +} + +static int diff_print_to_buffer_cb( + const git_diff_delta *delta, + const git_diff_range *range, + char line_origin, + const char *content, + size_t content_len, + void *payload) +{ + git_buf *output = payload; + GIT_UNUSED(delta); GIT_UNUSED(range); GIT_UNUSED(line_origin); + return git_buf_put(output, content, content_len); +} + +/* print a git_diff_patch to a string buffer */ +int git_diff_patch_to_str( + char **string, + git_diff_patch *patch) +{ + int error; + git_buf output = GIT_BUF_INIT; + + error = git_diff_patch_print(patch, diff_print_to_buffer_cb, &output); + + /* GIT_EUSER means git_buf_put in print_to_buffer_cb returned -1, + * meaning a memory allocation failure, so just map to -1... + */ + if (error == GIT_EUSER) + error = -1; + + *string = git_buf_detach(&output); + + return error; +} diff --git a/src/diff_tform.c b/src/diff_tform.c index efcb19d95..ba35d3c14 100644 --- a/src/diff_tform.c +++ b/src/diff_tform.c @@ -5,10 +5,14 @@ * a Linking Exception. For full terms see the included COPYING file. */ #include "common.h" -#include "diff.h" + #include "git2/config.h" #include "git2/blob.h" + +#include "diff.h" #include "hashsig.h" +#include "path.h" +#include "fileops.h" static git_diff_delta *diff_delta__dup( const git_diff_delta *d, git_pool *pool) @@ -18,12 +22,15 @@ static git_diff_delta *diff_delta__dup( return NULL; memcpy(delta, d, sizeof(git_diff_delta)); + GIT_DIFF_FLAG__CLEAR_INTERNAL(delta->flags); - delta->old_file.path = git_pool_strdup(pool, d->old_file.path); - if (delta->old_file.path == NULL) - goto fail; + if (d->old_file.path != NULL) { + delta->old_file.path = git_pool_strdup(pool, d->old_file.path); + if (delta->old_file.path == NULL) + goto fail; + } - if (d->new_file.path != d->old_file.path) { + if (d->new_file.path != d->old_file.path && d->new_file.path != NULL) { delta->new_file.path = git_pool_strdup(pool, d->new_file.path); if (delta->new_file.path == NULL) goto fail; @@ -170,7 +177,7 @@ int git_diff_merge( return error; } -static int find_similar__hashsig_for_file( +int git_diff_find_similar__hashsig_for_file( void **out, const git_diff_file *f, const char *path, void *p) { git_hashsig_option_t opt = (git_hashsig_option_t)p; @@ -178,7 +185,7 @@ static int find_similar__hashsig_for_file( GIT_UNUSED(f); error = git_hashsig_create_fromfile((git_hashsig **)out, path, opt); - + if (error == GIT_EBUFS) { error = 0; giterr_clear(); @@ -187,15 +194,15 @@ static int find_similar__hashsig_for_file( return error; } -static int find_similar__hashsig_for_buf( +int git_diff_find_similar__hashsig_for_buf( void **out, const git_diff_file *f, const char *buf, size_t len, void *p) { git_hashsig_option_t opt = (git_hashsig_option_t)p; int error = 0; - + GIT_UNUSED(f); error = git_hashsig_create((git_hashsig **)out, buf, len, opt); - + if (error == GIT_EBUFS) { error = 0; giterr_clear(); @@ -204,13 +211,13 @@ static int find_similar__hashsig_for_buf( return error; } -static void find_similar__hashsig_free(void *sig, void *payload) +void git_diff_find_similar__hashsig_free(void *sig, void *payload) { GIT_UNUSED(payload); git_hashsig_free(sig); } -static int find_similar__calc_similarity( +int git_diff_find_similar__calc_similarity( int *score, void *siga, void *sigb, void *payload) { GIT_UNUSED(payload); @@ -220,7 +227,7 @@ static int find_similar__calc_similarity( #define DEFAULT_THRESHOLD 50 #define DEFAULT_BREAK_REWRITE_THRESHOLD 60 -#define DEFAULT_TARGET_LIMIT 200 +#define DEFAULT_RENAME_LIMIT 200 static int normalize_find_opts( git_diff_list *diff, @@ -253,12 +260,25 @@ static int normalize_find_opts( /* some flags imply others */ + if (opts->flags & GIT_DIFF_FIND_EXACT_MATCH_ONLY) { + /* if we are only looking for exact matches, then don't turn + * MODIFIED items into ADD/DELETE pairs because it's too picky + */ + opts->flags &= ~(GIT_DIFF_FIND_REWRITES | GIT_DIFF_BREAK_REWRITES); + + /* similarly, don't look for self-rewrites to split */ + opts->flags &= ~GIT_DIFF_FIND_RENAMES_FROM_REWRITES; + } + if (opts->flags & GIT_DIFF_FIND_RENAMES_FROM_REWRITES) opts->flags |= GIT_DIFF_FIND_RENAMES; if (opts->flags & GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED) opts->flags |= GIT_DIFF_FIND_COPIES; + if (opts->flags & GIT_DIFF_BREAK_REWRITES) + opts->flags |= GIT_DIFF_FIND_REWRITES; + #define USE_DEFAULT(X) ((X) == 0 || (X) > 100) if (USE_DEFAULT(opts->rename_threshold)) @@ -275,15 +295,15 @@ static int normalize_find_opts( #undef USE_DEFAULT - if (!opts->target_limit) { + if (!opts->rename_limit) { int32_t limit = 0; - opts->target_limit = DEFAULT_TARGET_LIMIT; + opts->rename_limit = DEFAULT_RENAME_LIMIT; if (git_config_get_int32(&limit, cfg, "diff.renameLimit") < 0) giterr_clear(); else if (limit > 0) - opts->target_limit = limit; + opts->rename_limit = limit; } /* assign the internal metric with whitespace flag as payload */ @@ -291,10 +311,10 @@ static int normalize_find_opts( opts->metric = git__malloc(sizeof(git_diff_similarity_metric)); GITERR_CHECK_ALLOC(opts->metric); - opts->metric->file_signature = find_similar__hashsig_for_file; - opts->metric->buffer_signature = find_similar__hashsig_for_buf; - opts->metric->free_signature = find_similar__hashsig_free; - opts->metric->similarity = find_similar__calc_similarity; + opts->metric->file_signature = git_diff_find_similar__hashsig_for_file; + opts->metric->buffer_signature = git_diff_find_similar__hashsig_for_buf; + opts->metric->free_signature = git_diff_find_similar__hashsig_free; + opts->metric->similarity = git_diff_find_similar__calc_similarity; if (opts->flags & GIT_DIFF_FIND_IGNORE_WHITESPACE) opts->metric->payload = (void *)GIT_HASHSIG_IGNORE_WHITESPACE; @@ -307,11 +327,12 @@ static int normalize_find_opts( return 0; } -static int apply_splits_and_deletes(git_diff_list *diff, size_t expected_size) +static int apply_splits_and_deletes( + git_diff_list *diff, size_t expected_size, bool actually_split) { git_vector onto = GIT_VECTOR_INIT; size_t i; - git_diff_delta *delta; + git_diff_delta *delta, *deleted; if (git_vector_init(&onto, expected_size, git_diff_delta__cmp) < 0) return -1; @@ -321,9 +342,11 @@ static int apply_splits_and_deletes(git_diff_list *diff, size_t expected_size) if ((delta->flags & GIT_DIFF_FLAG__TO_DELETE) != 0) continue; - if ((delta->flags & GIT_DIFF_FLAG__TO_SPLIT) != 0) { - git_diff_delta *deleted = diff_delta__dup(delta, &diff->pool); - if (!deleted) + if ((delta->flags & GIT_DIFF_FLAG__TO_SPLIT) != 0 && actually_split) { + delta->similarity = 0; + + /* make new record for DELETED side of split */ + if (!(deleted = diff_delta__dup(delta, &diff->pool))) goto on_error; deleted->status = GIT_DELTA_DELETED; @@ -334,32 +357,46 @@ static int apply_splits_and_deletes(git_diff_list *diff, size_t expected_size) if (git_vector_insert(&onto, deleted) < 0) goto on_error; - delta->status = GIT_DELTA_ADDED; + if (diff->new_src == GIT_ITERATOR_TYPE_WORKDIR) + delta->status = GIT_DELTA_UNTRACKED; + else + delta->status = GIT_DELTA_ADDED; memset(&delta->old_file, 0, sizeof(delta->old_file)); delta->old_file.path = delta->new_file.path; delta->old_file.flags |= GIT_DIFF_FLAG_VALID_OID; } + /* clean up delta before inserting into new list */ + GIT_DIFF_FLAG__CLEAR_INTERNAL(delta->flags); + + if (delta->status != GIT_DELTA_COPIED && + delta->status != GIT_DELTA_RENAMED && + (delta->status != GIT_DELTA_MODIFIED || actually_split)) + delta->similarity = 0; + + /* insert into new list */ if (git_vector_insert(&onto, delta) < 0) goto on_error; } /* cannot return an error past this point */ - git_vector_foreach(&diff->deltas, i, delta) + + /* free deltas from old list that didn't make it to the new one */ + git_vector_foreach(&diff->deltas, i, delta) { if ((delta->flags & GIT_DIFF_FLAG__TO_DELETE) != 0) git__free(delta); + } /* swap new delta list into place */ - git_vector_sort(&onto); git_vector_swap(&diff->deltas, &onto); git_vector_free(&onto); + git_vector_sort(&diff->deltas); return 0; on_error: git_vector_foreach(&onto, i, delta) git__free(delta); - git_vector_free(&onto); return -1; @@ -371,312 +408,646 @@ GIT_INLINE(git_diff_file *) similarity_get_file(git_diff_list *diff, size_t idx) return (idx & 1) ? &delta->new_file : &delta->old_file; } -static int similarity_calc( - git_diff_list *diff, - git_diff_find_options *opts, - size_t file_idx, +typedef struct { + size_t idx; + git_iterator_type_t src; + git_repository *repo; + git_diff_file *file; + git_buf data; + git_odb_object *odb_obj; + git_blob *blob; +} similarity_info; + +static int similarity_init( + similarity_info *info, git_diff_list *diff, size_t file_idx) +{ + info->idx = file_idx; + info->src = (file_idx & 1) ? diff->new_src : diff->old_src; + info->repo = diff->repo; + info->file = similarity_get_file(diff, file_idx); + info->odb_obj = NULL; + info->blob = NULL; + git_buf_init(&info->data, 0); + + if (info->file->size > 0) + return 0; + + return git_diff_file__resolve_zero_size( + info->file, &info->odb_obj, info->repo); +} + +static int similarity_sig( + similarity_info *info, + const git_diff_find_options *opts, void **cache) { int error = 0; - git_diff_file *file = similarity_get_file(diff, file_idx); - git_iterator_type_t src = (file_idx & 1) ? diff->old_src : diff->new_src; - - if (src == GIT_ITERATOR_TYPE_WORKDIR) { /* compute hashsig from file */ - git_buf path = GIT_BUF_INIT; + git_diff_file *file = info->file; - /* TODO: apply wd-to-odb filters to file data if necessary */ + if (info->src == GIT_ITERATOR_TYPE_WORKDIR) { + if ((error = git_buf_joinpath( + &info->data, git_repository_workdir(info->repo), file->path)) < 0) + return error; - if (!(error = git_buf_joinpath( - &path, git_repository_workdir(diff->repo), file->path))) - error = opts->metric->file_signature( - &cache[file_idx], file, path.ptr, opts->metric->payload); + /* if path is not a regular file, just skip this item */ + if (!git_path_isfile(info->data.ptr)) + return 0; - git_buf_free(&path); - } else { /* compute hashsig from blob buffer */ - git_blob *blob = NULL; - git_off_t blobsize; + /* TODO: apply wd-to-odb filters to file data if necessary */ - /* TODO: add max size threshold a la diff? */ + error = opts->metric->file_signature( + &cache[info->idx], info->file, + info->data.ptr, opts->metric->payload); + } else { + /* if we didn't initially know the size, we might have an odb_obj + * around from earlier, so convert that, otherwise load the blob now + */ + if (info->odb_obj != NULL) + error = git_object__from_odb_object( + (git_object **)&info->blob, info->repo, + info->odb_obj, GIT_OBJ_BLOB); + else + error = git_blob_lookup(&info->blob, info->repo, &file->oid); - if ((error = git_blob_lookup(&blob, diff->repo, &file->oid)) < 0) - return error; + if (error < 0) { + /* if lookup fails, just skip this item in similarity calc */ + giterr_clear(); + } else { + size_t sz; - blobsize = git_blob_rawsize(blob); - if (!git__is_sizet(blobsize)) /* ? what to do ? */ - blobsize = (size_t)-1; + /* index size may not be actual blob size if filtered */ + if (file->size != git_blob_rawsize(info->blob)) + file->size = git_blob_rawsize(info->blob); - error = opts->metric->buffer_signature( - &cache[file_idx], file, git_blob_rawcontent(blob), - (size_t)blobsize, opts->metric->payload); + sz = (size_t)(git__is_sizet(file->size) ? file->size : -1); - git_blob_free(blob); + error = opts->metric->buffer_signature( + &cache[info->idx], info->file, + git_blob_rawcontent(info->blob), sz, opts->metric->payload); + } } return error; } +static void similarity_unload(similarity_info *info) +{ + if (info->odb_obj) + git_odb_object_free(info->odb_obj); + + if (info->blob) + git_blob_free(info->blob); + else + git_buf_free(&info->data); +} + +#define FLAG_SET(opts,flag_name) (((opts)->flags & flag_name) != 0) + +/* - score < 0 means files cannot be compared + * - score >= 100 means files are exact match + * - score == 0 means files are completely different + */ static int similarity_measure( + int *score, git_diff_list *diff, - git_diff_find_options *opts, + const git_diff_find_options *opts, void **cache, size_t a_idx, size_t b_idx) { - int score = 0; git_diff_file *a_file = similarity_get_file(diff, a_idx); git_diff_file *b_file = similarity_get_file(diff, b_idx); + bool exact_match = FLAG_SET(opts, GIT_DIFF_FIND_EXACT_MATCH_ONLY); + int error = 0; + similarity_info a_info, b_info; + + *score = -1; + /* don't try to compare files of different types */ if (GIT_MODE_TYPE(a_file->mode) != GIT_MODE_TYPE(b_file->mode)) return 0; - if (git_oid_cmp(&a_file->oid, &b_file->oid) == 0) - return 100; + /* if exact match is requested, force calculation of missing OIDs now */ + if (exact_match) { + if (git_oid_iszero(&a_file->oid) && + diff->old_src == GIT_ITERATOR_TYPE_WORKDIR && + !git_diff__oid_for_file(diff->repo, a_file->path, + a_file->mode, a_file->size, &a_file->oid)) + a_file->flags |= GIT_DIFF_FLAG_VALID_OID; + + if (git_oid_iszero(&b_file->oid) && + diff->new_src == GIT_ITERATOR_TYPE_WORKDIR && + !git_diff__oid_for_file(diff->repo, b_file->path, + b_file->mode, b_file->size, &b_file->oid)) + b_file->flags |= GIT_DIFF_FLAG_VALID_OID; + } + + /* check OID match as a quick test */ + if (git_oid__cmp(&a_file->oid, &b_file->oid) == 0) { + *score = 100; + return 0; + } + + /* don't calculate signatures if we are doing exact match */ + if (exact_match) { + *score = 0; + return 0; + } + + memset(&a_info, 0, sizeof(a_info)); + memset(&b_info, 0, sizeof(b_info)); + + /* set up similarity data (will try to update missing file sizes) */ + if (!cache[a_idx] && (error = similarity_init(&a_info, diff, a_idx)) < 0) + return error; + if (!cache[b_idx] && (error = similarity_init(&b_info, diff, b_idx)) < 0) + goto cleanup; + + /* check if file sizes are nowhere near each other */ + if (a_file->size > 127 && + b_file->size > 127 && + (a_file->size > (b_file->size << 3) || + b_file->size > (a_file->size << 3))) + goto cleanup; /* update signature cache if needed */ - if (!cache[a_idx] && similarity_calc(diff, opts, a_idx, cache) < 0) - return -1; - if (!cache[b_idx] && similarity_calc(diff, opts, b_idx, cache) < 0) - return -1; - - /* some metrics may not wish to process this file (too big / too small) */ - if (!cache[a_idx] || !cache[b_idx]) + if (!cache[a_idx]) { + if ((error = similarity_sig(&a_info, opts, cache)) < 0) + goto cleanup; + } + if (!cache[b_idx]) { + if ((error = similarity_sig(&b_info, opts, cache)) < 0) + goto cleanup; + } + + /* calculate similarity provided that the metric choose to process + * both the a and b files (some may not if file is too big, etc). + */ + if (cache[a_idx] && cache[b_idx]) + error = opts->metric->similarity( + score, cache[a_idx], cache[b_idx], opts->metric->payload); + +cleanup: + similarity_unload(&a_info); + similarity_unload(&b_info); + + return error; +} + +static int calc_self_similarity( + git_diff_list *diff, + const git_diff_find_options *opts, + size_t delta_idx, + void **cache) +{ + int error, similarity = -1; + git_diff_delta *delta = GIT_VECTOR_GET(&diff->deltas, delta_idx); + + if ((delta->flags & GIT_DIFF_FLAG__HAS_SELF_SIMILARITY) != 0) return 0; - /* compare signatures */ - if (opts->metric->similarity( - &score, cache[a_idx], cache[b_idx], opts->metric->payload) < 0) - return -1; + error = similarity_measure( + &similarity, diff, opts, cache, 2 * delta_idx, 2 * delta_idx + 1); + if (error < 0) + return error; - /* clip score */ - if (score < 0) - score = 0; - else if (score > 100) - score = 100; + if (similarity >= 0) { + delta->similarity = (uint32_t)similarity; + delta->flags |= GIT_DIFF_FLAG__HAS_SELF_SIMILARITY; + } - return score; + return 0; } -#define FLAG_SET(opts,flag_name) ((opts.flags & flag_name) != 0) +static bool is_rename_target( + git_diff_list *diff, + const git_diff_find_options *opts, + size_t delta_idx, + void **cache) +{ + git_diff_delta *delta = GIT_VECTOR_GET(&diff->deltas, delta_idx); + + /* skip things that aren't plain blobs */ + if (!GIT_MODE_ISBLOB(delta->new_file.mode)) + return false; + + /* only consider ADDED, RENAMED, COPIED, and split MODIFIED as + * targets; maybe include UNTRACKED and IGNORED if requested. + */ + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: + case GIT_DELTA_DELETED: + return false; + + case GIT_DELTA_MODIFIED: + if (!FLAG_SET(opts, GIT_DIFF_FIND_REWRITES) && + !FLAG_SET(opts, GIT_DIFF_FIND_RENAMES_FROM_REWRITES)) + return false; + + if (calc_self_similarity(diff, opts, delta_idx, cache) < 0) + return false; + + if (FLAG_SET(opts, GIT_DIFF_BREAK_REWRITES) && + delta->similarity < opts->break_rewrite_threshold) { + delta->flags |= GIT_DIFF_FLAG__TO_SPLIT; + break; + } + if (FLAG_SET(opts, GIT_DIFF_FIND_RENAMES_FROM_REWRITES) && + delta->similarity < opts->rename_from_rewrite_threshold) + break; + + return false; + + case GIT_DELTA_UNTRACKED: + if (!FLAG_SET(opts, GIT_DIFF_FIND_FOR_UNTRACKED)) + return false; + break; + + case GIT_DELTA_IGNORED: + return false; + + default: /* all other status values should be checked */ + break; + } + + delta->flags |= GIT_DIFF_FLAG__IS_RENAME_TARGET; + return true; +} + +static bool is_rename_source( + git_diff_list *diff, + const git_diff_find_options *opts, + size_t delta_idx, + void **cache) +{ + git_diff_delta *delta = GIT_VECTOR_GET(&diff->deltas, delta_idx); + + /* skip things that aren't blobs */ + if (!GIT_MODE_ISBLOB(delta->old_file.mode)) + return false; + + switch (delta->status) { + case GIT_DELTA_ADDED: + case GIT_DELTA_UNTRACKED: + case GIT_DELTA_IGNORED: + return false; + + case GIT_DELTA_DELETED: + case GIT_DELTA_TYPECHANGE: + break; + + case GIT_DELTA_UNMODIFIED: + if (!FLAG_SET(opts, GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED)) + return false; + break; + + default: /* MODIFIED, RENAMED, COPIED */ + /* if we're finding copies, this could be a source */ + if (FLAG_SET(opts, GIT_DIFF_FIND_COPIES)) + break; + + /* otherwise, this is only a source if we can split it */ + if (!FLAG_SET(opts, GIT_DIFF_FIND_REWRITES) && + !FLAG_SET(opts, GIT_DIFF_FIND_RENAMES_FROM_REWRITES)) + return false; + + if (calc_self_similarity(diff, opts, delta_idx, cache) < 0) + return false; + + if (FLAG_SET(opts, GIT_DIFF_BREAK_REWRITES) && + delta->similarity < opts->break_rewrite_threshold) { + delta->flags |= GIT_DIFF_FLAG__TO_SPLIT; + break; + } + + if (FLAG_SET(opts, GIT_DIFF_FIND_RENAMES_FROM_REWRITES) && + delta->similarity < opts->rename_from_rewrite_threshold) + break; + + return false; + } + + delta->flags |= GIT_DIFF_FLAG__IS_RENAME_SOURCE; + return true; +} + +GIT_INLINE(bool) delta_is_split(git_diff_delta *delta) +{ + return (delta->status == GIT_DELTA_TYPECHANGE || + (delta->flags & GIT_DIFF_FLAG__TO_SPLIT) != 0); +} + +GIT_INLINE(bool) delta_is_new_only(git_diff_delta *delta) +{ + return (delta->status == GIT_DELTA_ADDED || + delta->status == GIT_DELTA_UNTRACKED || + delta->status == GIT_DELTA_IGNORED); +} + +GIT_INLINE(void) delta_make_rename( + git_diff_delta *to, const git_diff_delta *from, uint32_t similarity) +{ + to->status = GIT_DELTA_RENAMED; + to->similarity = similarity; + memcpy(&to->old_file, &from->old_file, sizeof(to->old_file)); + to->flags &= ~GIT_DIFF_FLAG__TO_SPLIT; +} + +typedef struct { + uint32_t idx; + uint32_t similarity; +} diff_find_match; int git_diff_find_similar( git_diff_list *diff, git_diff_find_options *given_opts) { - size_t i, j, cache_size, *matches; + size_t s, t; int error = 0, similarity; - git_diff_delta *from, *to; + git_diff_delta *src, *tgt; git_diff_find_options opts; - size_t tried_targets, num_rewrites = 0; - void **cache; + size_t num_deltas, num_srcs = 0, num_tgts = 0; + size_t tried_srcs = 0, tried_tgts = 0; + size_t num_rewrites = 0, num_updates = 0, num_bumped = 0; + void **sigcache; /* cache of similarity metric file signatures */ + diff_find_match *tgt2src = NULL; + diff_find_match *src2tgt = NULL; + diff_find_match *tgt2src_copy = NULL; + diff_find_match *best_match; + git_diff_file swap; if ((error = normalize_find_opts(diff, &opts, given_opts)) < 0) return error; - /* TODO: maybe abort if deltas.length > target_limit ??? */ + num_deltas = diff->deltas.length; - cache_size = diff->deltas.length * 2; /* must store b/c length may change */ - cache = git__calloc(cache_size, sizeof(void *)); - GITERR_CHECK_ALLOC(cache); + /* TODO: maybe abort if deltas.length > rename_limit ??? */ + if (!git__is_uint32(num_deltas)) + return 0; - matches = git__calloc(diff->deltas.length, sizeof(size_t)); - GITERR_CHECK_ALLOC(matches); + sigcache = git__calloc(num_deltas * 2, sizeof(void *)); + GITERR_CHECK_ALLOC(sigcache); - /* first break MODIFIED records that are too different (if requested) */ + /* Label rename sources and targets + * + * This will also set self-similarity scores for MODIFIED files and + * mark them for splitting if break-rewrites is enabled + */ + git_vector_foreach(&diff->deltas, t, tgt) { + if (is_rename_source(diff, &opts, t, sigcache)) + ++num_srcs; - if (FLAG_SET(opts, GIT_DIFF_FIND_AND_BREAK_REWRITES)) { - git_vector_foreach(&diff->deltas, i, from) { - if (from->status != GIT_DELTA_MODIFIED) - continue; + if (is_rename_target(diff, &opts, t, sigcache)) + ++num_tgts; + } - similarity = similarity_measure( - diff, &opts, cache, 2 * i, 2 * i + 1); + /* if there are no candidate srcs or tgts, we're done */ + if (!num_srcs || !num_tgts) + goto cleanup; - if (similarity < 0) { - error = similarity; - goto cleanup; - } + src2tgt = git__calloc(num_deltas, sizeof(diff_find_match)); + GITERR_CHECK_ALLOC(src2tgt); + tgt2src = git__calloc(num_deltas, sizeof(diff_find_match)); + GITERR_CHECK_ALLOC(tgt2src); - if ((unsigned int)similarity < opts.break_rewrite_threshold) { - from->flags |= GIT_DIFF_FLAG__TO_SPLIT; - num_rewrites++; - } - } + if (FLAG_SET(&opts, GIT_DIFF_FIND_COPIES)) { + tgt2src_copy = git__calloc(num_deltas, sizeof(diff_find_match)); + GITERR_CHECK_ALLOC(tgt2src_copy); } - /* next find the most similar delta for each rename / copy candidate */ - - git_vector_foreach(&diff->deltas, i, from) { - tried_targets = 0; + /* + * Find best-fit matches for rename / copy candidates + */ - /* skip things that aren't blobs */ - if (GIT_MODE_TYPE(from->old_file.mode) != - GIT_MODE_TYPE(GIT_FILEMODE_BLOB)) - continue; +find_best_matches: + tried_tgts = num_bumped = 0; - /* don't check UNMODIFIED files as source unless given option */ - if (from->status == GIT_DELTA_UNMODIFIED && - !FLAG_SET(opts, GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED)) + git_vector_foreach(&diff->deltas, t, tgt) { + /* skip things that are not rename targets */ + if ((tgt->flags & GIT_DIFF_FLAG__IS_RENAME_TARGET) == 0) continue; - /* skip all but DELETED files unless copy detection is on */ - if (!FLAG_SET(opts, GIT_DIFF_FIND_COPIES) && - from->status != GIT_DELTA_DELETED && - (from->flags & GIT_DIFF_FLAG__TO_SPLIT) == 0) - continue; + tried_srcs = 0; - git_vector_foreach(&diff->deltas, j, to) { - if (i == j) + git_vector_foreach(&diff->deltas, s, src) { + /* skip things that are not rename sources */ + if ((src->flags & GIT_DIFF_FLAG__IS_RENAME_SOURCE) == 0) continue; - /* skip things that aren't blobs */ - if (GIT_MODE_TYPE(to->new_file.mode) != - GIT_MODE_TYPE(GIT_FILEMODE_BLOB)) - continue; + /* calculate similarity for this pair and find best match */ + if (s == t) + similarity = -1; /* don't measure self-similarity here */ + else if ((error = similarity_measure( + &similarity, diff, &opts, sigcache, 2 * s, 2 * t + 1)) < 0) + goto cleanup; - switch (to->status) { - case GIT_DELTA_ADDED: - case GIT_DELTA_UNTRACKED: - case GIT_DELTA_RENAMED: - case GIT_DELTA_COPIED: - break; - case GIT_DELTA_MODIFIED: - if ((to->flags & GIT_DIFF_FLAG__TO_SPLIT) == 0) - continue; - break; - default: - /* only the above status values should be checked */ + if (similarity < 0) continue; - } - - /* cap on maximum files we'll examine (per "from" file) */ - if (++tried_targets > opts.target_limit) - break; - - /* calculate similarity and see if this pair beats the - * similarity score of the current best pair. - */ - similarity = similarity_measure( - diff, &opts, cache, 2 * i, 2 * j + 1); - if (similarity < 0) { - error = similarity; - goto cleanup; + /* is this a better rename? */ + if (tgt2src[t].similarity < (uint32_t)similarity && + src2tgt[s].similarity < (uint32_t)similarity) + { + /* eject old mapping */ + if (src2tgt[s].similarity > 0) { + tgt2src[src2tgt[s].idx].similarity = 0; + num_bumped++; + } + if (tgt2src[t].similarity > 0) { + src2tgt[tgt2src[t].idx].similarity = 0; + num_bumped++; + } + + /* write new mapping */ + tgt2src[t].idx = s; + tgt2src[t].similarity = (uint32_t)similarity; + src2tgt[s].idx = t; + src2tgt[s].similarity = (uint32_t)similarity; } - if (to->similarity < (unsigned int)similarity) { - to->similarity = (unsigned int)similarity; - matches[j] = i + 1; + /* keep best absolute match for copies */ + if (tgt2src_copy != NULL && + tgt2src_copy[t].similarity < (uint32_t)similarity) + { + tgt2src_copy[t].idx = s; + tgt2src_copy[t].similarity = (uint32_t)similarity; } + + if (++tried_srcs >= num_srcs) + break; + + /* cap on maximum targets we'll examine (per "tgt" file) */ + if (tried_srcs > opts.rename_limit) + break; } + + if (++tried_tgts >= num_tgts) + break; } - /* next rewrite the diffs with renames / copies */ + if (num_bumped > 0) /* try again if we bumped some items */ + goto find_best_matches; + + /* + * Rewrite the diffs with renames / copies + */ + + tried_tgts = 0; - git_vector_foreach(&diff->deltas, j, to) { - if (!matches[j]) { - assert(to->similarity == 0); + git_vector_foreach(&diff->deltas, t, tgt) { + /* skip things that are not rename targets */ + if ((tgt->flags & GIT_DIFF_FLAG__IS_RENAME_TARGET) == 0) continue; - } - i = matches[j] - 1; - from = GIT_VECTOR_GET(&diff->deltas, i); - assert(from); - - /* four possible outcomes here: - * 1. old DELETED and if over rename threshold, - * new becomes RENAMED and old goes away - * 2. old SPLIT and if over rename threshold, - * new becomes RENAMED and old becomes ADDED (clear SPLIT) - * 3. old was MODIFIED but FIND_RENAMES_FROM_REWRITES is on and - * old is more similar to new than it is to itself, in which - * case, new becomes RENAMED and old becomed ADDED - * 4. otherwise if over copy threshold, new becomes COPIED + /* check if this delta was the target of a similarity */ + if (tgt2src[t].similarity) + best_match = &tgt2src[t]; + else if (tgt2src_copy && tgt2src_copy[t].similarity) + best_match = &tgt2src_copy[t]; + else + continue; + + s = best_match->idx; + src = GIT_VECTOR_GET(&diff->deltas, s); + + /* possible scenarios: + * 1. from DELETE to ADD/UNTRACK/IGNORE = RENAME + * 2. from DELETE to SPLIT/TYPECHANGE = RENAME + DELETE + * 3. from SPLIT/TYPECHANGE to ADD/UNTRACK/IGNORE = ADD + RENAME + * 4. from SPLIT/TYPECHANGE to SPLIT/TYPECHANGE = RENAME + SPLIT + * 5. from OTHER to ADD/UNTRACK/IGNORE = OTHER + COPY */ - if (from->status == GIT_DELTA_DELETED) { - if (to->similarity < opts.rename_threshold) { - to->similarity = 0; - continue; - } + if (src->status == GIT_DELTA_DELETED) { + + if (delta_is_new_only(tgt)) { - to->status = GIT_DELTA_RENAMED; - memcpy(&to->old_file, &from->old_file, sizeof(to->old_file)); + if (best_match->similarity < opts.rename_threshold) + continue; - from->flags |= GIT_DIFF_FLAG__TO_DELETE; - num_rewrites++; + delta_make_rename(tgt, src, best_match->similarity); - continue; - } + src->flags |= GIT_DIFF_FLAG__TO_DELETE; + num_rewrites++; + } else { + assert(delta_is_split(tgt)); - if (from->status == GIT_DELTA_MODIFIED && - (from->flags & GIT_DIFF_FLAG__TO_SPLIT) != 0) - { - if (to->similarity < opts.rename_threshold) { - to->similarity = 0; - continue; - } + if (best_match->similarity < opts.rename_from_rewrite_threshold) + continue; - to->status = GIT_DELTA_RENAMED; - memcpy(&to->old_file, &from->old_file, sizeof(to->old_file)); + memcpy(&swap, &tgt->old_file, sizeof(swap)); - from->status = GIT_DELTA_ADDED; - from->flags &= ~GIT_DIFF_FLAG__TO_SPLIT; - memset(&from->old_file, 0, sizeof(from->old_file)); - num_rewrites--; + delta_make_rename(tgt, src, best_match->similarity); + num_rewrites--; - continue; - } + src->status = GIT_DELTA_DELETED; + memcpy(&src->old_file, &swap, sizeof(src->old_file)); + memset(&src->new_file, 0, sizeof(src->new_file)); + src->new_file.path = src->old_file.path; + src->new_file.flags |= GIT_DIFF_FLAG_VALID_OID; - if (from->status == GIT_DELTA_MODIFIED && - FLAG_SET(opts, GIT_DIFF_FIND_RENAMES_FROM_REWRITES) && - to->similarity > opts.rename_threshold) - { - similarity = similarity_measure( - diff, &opts, cache, 2 * i, 2 * i + 1); + num_updates++; - if (similarity < 0) { - error = similarity; - goto cleanup; + if (src2tgt[t].similarity > 0 && src2tgt[t].idx > t) { + /* what used to be at src t is now at src s */ + tgt2src[src2tgt[t].idx].idx = (uint32_t)s; + } } + } - if ((unsigned int)similarity < opts.rename_from_rewrite_threshold) { - to->status = GIT_DELTA_RENAMED; - memcpy(&to->old_file, &from->old_file, sizeof(to->old_file)); + else if (delta_is_split(src)) { - from->status = GIT_DELTA_ADDED; - memset(&from->old_file, 0, sizeof(from->old_file)); - from->old_file.path = to->old_file.path; - from->old_file.flags |= GIT_DIFF_FLAG_VALID_OID; + if (delta_is_new_only(tgt)) { - continue; + if (best_match->similarity < opts.rename_threshold) + continue; + + delta_make_rename(tgt, src, best_match->similarity); + + src->status = (diff->new_src == GIT_ITERATOR_TYPE_WORKDIR) ? + GIT_DELTA_UNTRACKED : GIT_DELTA_ADDED; + memset(&src->old_file, 0, sizeof(src->old_file)); + src->old_file.path = src->new_file.path; + src->old_file.flags |= GIT_DIFF_FLAG_VALID_OID; + + src->flags &= ~GIT_DIFF_FLAG__TO_SPLIT; + num_rewrites--; + + num_updates++; + } else { + assert(delta_is_split(src)); + + if (best_match->similarity < opts.rename_from_rewrite_threshold) + continue; + + memcpy(&swap, &tgt->old_file, sizeof(swap)); + + delta_make_rename(tgt, src, best_match->similarity); + num_rewrites--; + num_updates++; + + memcpy(&src->old_file, &swap, sizeof(src->old_file)); + + /* if we've just swapped the new element into the correct + * place, clear the SPLIT flag + */ + if (tgt2src[s].idx == t && + tgt2src[s].similarity > + opts.rename_from_rewrite_threshold) { + src->status = GIT_DELTA_RENAMED; + src->similarity = tgt2src[s].similarity; + tgt2src[s].similarity = 0; + src->flags &= ~GIT_DIFF_FLAG__TO_SPLIT; + num_rewrites--; + } + /* otherwise, if we just overwrote a source, update mapping */ + else if (src2tgt[t].similarity > 0 && src2tgt[t].idx > t) { + /* what used to be at src t is now at src s */ + tgt2src[src2tgt[t].idx].idx = (uint32_t)s; + } + + num_updates++; } } - if (to->similarity < opts.copy_threshold) { - to->similarity = 0; - continue; - } + else if (delta_is_new_only(tgt)) { + if (!FLAG_SET(&opts, GIT_DIFF_FIND_COPIES)) + continue; + + if (tgt2src_copy[t].similarity < opts.copy_threshold) + continue; + + /* always use best possible source for copy */ + best_match = &tgt2src_copy[t]; + src = GIT_VECTOR_GET(&diff->deltas, best_match->idx); + + tgt->status = GIT_DELTA_COPIED; + tgt->similarity = best_match->similarity; + memcpy(&tgt->old_file, &src->old_file, sizeof(tgt->old_file)); - /* convert "to" to a COPIED record */ - to->status = GIT_DELTA_COPIED; - memcpy(&to->old_file, &from->old_file, sizeof(to->old_file)); + num_updates++; + } } - if (num_rewrites > 0) { - assert(num_rewrites < diff->deltas.length); + /* + * Actually split and delete entries as needed + */ + if (num_rewrites > 0 || num_updates > 0) error = apply_splits_and_deletes( - diff, diff->deltas.length - num_rewrites); - } + diff, diff->deltas.length - num_rewrites, + FLAG_SET(&opts, GIT_DIFF_BREAK_REWRITES)); cleanup: - git__free(matches); + git__free(tgt2src); + git__free(src2tgt); + git__free(tgt2src_copy); - for (i = 0; i < cache_size; ++i) { - if (cache[i] != NULL) - opts.metric->free_signature(cache[i], opts.metric->payload); + for (t = 0; t < num_deltas * 2; ++t) { + if (sigcache[t] != NULL) + opts.metric->free_signature(sigcache[t], opts.metric->payload); } - git__free(cache); + git__free(sigcache); if (!given_opts || !given_opts->metric) git__free(opts.metric); diff --git a/src/diff_xdiff.c b/src/diff_xdiff.c new file mode 100644 index 000000000..7694fb996 --- /dev/null +++ b/src/diff_xdiff.c @@ -0,0 +1,166 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#include "common.h" +#include "diff.h" +#include "diff_driver.h" +#include "diff_patch.h" +#include "diff_xdiff.h" + +static int git_xdiff_scan_int(const char **str, int *value) +{ + const char *scan = *str; + int v = 0, digits = 0; + /* find next digit */ + for (scan = *str; *scan && !git__isdigit(*scan); scan++); + /* parse next number */ + for (; git__isdigit(*scan); scan++, digits++) + v = (v * 10) + (*scan - '0'); + *str = scan; + *value = v; + return (digits > 0) ? 0 : -1; +} + +static int git_xdiff_parse_hunk(git_diff_range *range, const char *header) +{ + /* expect something of the form "@@ -%d[,%d] +%d[,%d] @@" */ + if (*header != '@') + return -1; + if (git_xdiff_scan_int(&header, &range->old_start) < 0) + return -1; + if (*header == ',') { + if (git_xdiff_scan_int(&header, &range->old_lines) < 0) + return -1; + } else + range->old_lines = 1; + if (git_xdiff_scan_int(&header, &range->new_start) < 0) + return -1; + if (*header == ',') { + if (git_xdiff_scan_int(&header, &range->new_lines) < 0) + return -1; + } else + range->new_lines = 1; + if (range->old_start < 0 || range->new_start < 0) + return -1; + + return 0; +} + +typedef struct { + git_xdiff_output *xo; + git_diff_patch *patch; + git_diff_range range; +} git_xdiff_info; + +static int git_xdiff_cb(void *priv, mmbuffer_t *bufs, int len) +{ + git_xdiff_info *info = priv; + git_diff_patch *patch = info->patch; + const git_diff_delta *delta = git_diff_patch_delta(patch); + git_diff_output *output = &info->xo->output; + + if (len == 1) { + output->error = git_xdiff_parse_hunk(&info->range, bufs[0].ptr); + if (output->error < 0) + return output->error; + + if (output->hunk_cb != NULL && + output->hunk_cb(delta, &info->range, + bufs[0].ptr, bufs[0].size, output->payload)) + output->error = GIT_EUSER; + } + + if (len == 2 || len == 3) { + /* expect " "/"-"/"+", then data */ + char origin = + (*bufs[0].ptr == '+') ? GIT_DIFF_LINE_ADDITION : + (*bufs[0].ptr == '-') ? GIT_DIFF_LINE_DELETION : + GIT_DIFF_LINE_CONTEXT; + + if (output->data_cb != NULL && + output->data_cb(delta, &info->range, + origin, bufs[1].ptr, bufs[1].size, output->payload)) + output->error = GIT_EUSER; + } + + if (len == 3 && !output->error) { + /* If we have a '+' and a third buf, then we have added a line + * without a newline and the old code had one, so DEL_EOFNL. + * If we have a '-' and a third buf, then we have removed a line + * with out a newline but added a blank line, so ADD_EOFNL. + */ + char origin = + (*bufs[0].ptr == '+') ? GIT_DIFF_LINE_DEL_EOFNL : + (*bufs[0].ptr == '-') ? GIT_DIFF_LINE_ADD_EOFNL : + GIT_DIFF_LINE_CONTEXT_EOFNL; + + if (output->data_cb != NULL && + output->data_cb(delta, &info->range, + origin, bufs[2].ptr, bufs[2].size, output->payload)) + output->error = GIT_EUSER; + } + + return output->error; +} + +static int git_xdiff(git_diff_output *output, git_diff_patch *patch) +{ + git_xdiff_output *xo = (git_xdiff_output *)output; + git_xdiff_info info; + git_diff_find_context_payload findctxt; + mmfile_t xd_old_data, xd_new_data; + + memset(&info, 0, sizeof(info)); + info.patch = patch; + info.xo = xo; + + xo->callback.priv = &info; + + git_diff_find_context_init( + &xo->config.find_func, &findctxt, git_diff_patch__driver(patch)); + xo->config.find_func_priv = &findctxt; + + if (xo->config.find_func != NULL) + xo->config.flags |= XDL_EMIT_FUNCNAMES; + else + xo->config.flags &= ~XDL_EMIT_FUNCNAMES; + + /* TODO: check ofile.opts_flags to see if driver-specific per-file + * updates are needed to xo->params.flags + */ + + git_diff_patch__old_data(&xd_old_data.ptr, &xd_old_data.size, patch); + git_diff_patch__new_data(&xd_new_data.ptr, &xd_new_data.size, patch); + + xdl_diff(&xd_old_data, &xd_new_data, + &xo->params, &xo->config, &xo->callback); + + git_diff_find_context_clear(&findctxt); + + return xo->output.error; +} + +void git_xdiff_init(git_xdiff_output *xo, const git_diff_options *opts) +{ + uint32_t flags = opts ? opts->flags : GIT_DIFF_NORMAL; + + xo->output.diff_cb = git_xdiff; + + memset(&xo->config, 0, sizeof(xo->config)); + xo->config.ctxlen = opts ? opts->context_lines : 3; + xo->config.interhunkctxlen = opts ? opts->interhunk_lines : 0; + + memset(&xo->params, 0, sizeof(xo->params)); + if (flags & GIT_DIFF_IGNORE_WHITESPACE) + xo->params.flags |= XDF_WHITESPACE_FLAGS; + if (flags & GIT_DIFF_IGNORE_WHITESPACE_CHANGE) + xo->params.flags |= XDF_IGNORE_WHITESPACE_CHANGE; + if (flags & GIT_DIFF_IGNORE_WHITESPACE_EOL) + xo->params.flags |= XDF_IGNORE_WHITESPACE_AT_EOL; + + memset(&xo->callback, 0, sizeof(xo->callback)); + xo->callback.outf = git_xdiff_cb; +} diff --git a/src/diff_xdiff.h b/src/diff_xdiff.h new file mode 100644 index 000000000..c547b00cf --- /dev/null +++ b/src/diff_xdiff.h @@ -0,0 +1,28 @@ +/* + * 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_diff_xdiff_h__ +#define INCLUDE_diff_xdiff_h__ + +#include "diff.h" +#include "diff_patch.h" +#include "xdiff/xdiff.h" + +/* A git_xdiff_output is a git_diff_output with extra fields necessary + * to use libxdiff. Calling git_xdiff_init() will set the diff_cb field + * of the output to use xdiff to generate the diffs. + */ +typedef struct { + git_diff_output output; + + xdemitconf_t config; + xpparam_t params; + xdemitcb_t callback; +} git_xdiff_output; + +void git_xdiff_init(git_xdiff_output *xo, const git_diff_options *opts); + +#endif diff --git a/src/fetch.c b/src/fetch.c index b60a95232..03fad5fec 100644 --- a/src/fetch.c +++ b/src/fetch.c @@ -16,6 +16,8 @@ #include "pack.h" #include "fetch.h" #include "netops.h" +#include "repository.h" +#include "refs.h" struct filter_payload { git_remote *remote; @@ -34,10 +36,16 @@ static int filter_ref__cb(git_remote_head *head, void *payload) if (!p->found_head && strcmp(head->name, GIT_HEAD_FILE) == 0) p->found_head = 1; - else if (git_refspec_src_matches(p->spec, head->name)) + else if (p->remote->download_tags == GIT_REMOTE_DOWNLOAD_TAGS_ALL) { + /* + * If tagopt is --tags, then we only use the default + * tags refspec and ignore the remote's + */ + if (git_refspec_src_matches(p->tagspec, head->name)) match = 1; - else if (p->remote->download_tags == GIT_REMOTE_DOWNLOAD_TAGS_ALL && - git_refspec_src_matches(p->tagspec, head->name)) + else + return 0; + } else if (git_remote__matching_refspec(p->remote, head->name)) match = 1; if (!match) @@ -68,7 +76,6 @@ static int filter_wants(git_remote *remote) * not interested in any particular branch but just the remote's * HEAD, which will be stored in FETCH_HEAD after the fetch. */ - p.spec = git_remote_fetchspec(remote); p.tagspec = &tagspec; p.found_head = 0; p.remote = remote; diff --git a/src/fileops.c b/src/fileops.c index d6244711f..7f8418d7a 100644 --- a/src/fileops.c +++ b/src/fileops.c @@ -61,9 +61,11 @@ int git_futils_creat_locked(const char *path, const mode_t mode) wchar_t buf[GIT_WIN_PATH]; git__utf8_to_16(buf, GIT_WIN_PATH, path); - fd = _wopen(buf, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | O_EXCL, mode); + fd = _wopen(buf, O_WRONLY | O_CREAT | O_TRUNC | + O_EXCL | O_BINARY | O_CLOEXEC, mode); #else - fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | O_EXCL, mode); + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | + O_EXCL | O_BINARY | O_CLOEXEC, mode); #endif if (fd < 0) { @@ -202,6 +204,33 @@ int git_futils_readbuffer(git_buf *buf, const char *path) return git_futils_readbuffer_updated(buf, path, NULL, NULL, NULL); } +int git_futils_writebuffer( + const git_buf *buf, const char *path, int flags, mode_t mode) +{ + int fd, error = 0; + + if (flags <= 0) + flags = O_CREAT | O_TRUNC | O_WRONLY; + if (!mode) + mode = GIT_FILEMODE_BLOB; + + if ((fd = p_open(path, flags, mode)) < 0) { + giterr_set(GITERR_OS, "Could not open '%s' for writing", path); + return fd; + } + + if ((error = p_write(fd, git_buf_cstr(buf), git_buf_len(buf))) < 0) { + giterr_set(GITERR_OS, "Could not write to '%s'", path); + (void)p_close(fd); + return error; + } + + if ((error = p_close(fd)) < 0) + giterr_set(GITERR_OS, "Error while closing '%s'", path); + + return error; +} + int git_futils_mv_withpath(const char *from, const char *to, const mode_t dirmode) { if (git_futils_mkpath2file(to, dirmode) < 0) @@ -253,8 +282,9 @@ int git_futils_mkdir( { int error = -1; git_buf make_path = GIT_BUF_INIT; - ssize_t root = 0; - char lastch, *tail; + ssize_t root = 0, min_root_len; + char lastch = '/', *tail; + struct stat st; /* build path and find "root" where we should start calling mkdir */ if (git_path_join_unrooted(&make_path, path, base, &root) < 0) @@ -262,7 +292,7 @@ int git_futils_mkdir( if (make_path.size == 0) { giterr_set(GITERR_OS, "Attempt to create empty path"); - goto fail; + goto done; } /* remove trailing slashes on path */ @@ -279,19 +309,32 @@ int git_futils_mkdir( if ((flags & GIT_MKDIR_SKIP_LAST) != 0) git_buf_rtruncate_at_char(&make_path, '/'); + /* if nothing left after truncation, then we're done! */ + if (!make_path.size) { + error = 0; + goto done; + } + /* if we are not supposed to make the whole path, reset root */ if ((flags & GIT_MKDIR_PATH) == 0) root = git_buf_rfind(&make_path, '/'); + /* advance root past drive name or network mount prefix */ + min_root_len = git_path_root(make_path.ptr); + if (root < min_root_len) + root = min_root_len; + while (root >= 0 && make_path.ptr[root] == '/') + ++root; + /* clip root to make_path length */ - if (root >= (ssize_t)make_path.size) - root = (ssize_t)make_path.size - 1; + if (root > (ssize_t)make_path.size) + root = (ssize_t)make_path.size; /* i.e. NUL byte of string */ if (root < 0) root = 0; - tail = & make_path.ptr[root]; + /* walk down tail of path making each directory */ + for (tail = &make_path.ptr[root]; *tail; *tail = lastch) { - while (*tail) { /* advance tail to include next path component */ while (*tail == '/') tail++; @@ -301,68 +344,48 @@ int git_futils_mkdir( /* truncate path at next component */ lastch = *tail; *tail = '\0'; + st.st_mode = 0; /* make directory */ if (p_mkdir(make_path.ptr, mode) < 0) { - int already_exists = 0; - - switch (errno) { - case EEXIST: - if (!lastch && (flags & GIT_MKDIR_VERIFY_DIR) != 0 && - !git_path_isdir(make_path.ptr)) { - giterr_set( - GITERR_OS, "Existing path is not a directory '%s'", - make_path.ptr); - error = GIT_ENOTFOUND; - goto fail; - } - - already_exists = 1; - break; - case ENOSYS: - /* Solaris can generate this error if you try to mkdir - * a path which is already a mount point. In that case, - * the path does already exist; but it's not implied by - * the definition of the error, so let's recheck */ - if (git_path_isdir(make_path.ptr)) { - already_exists = 1; - break; - } - - /* Fall through */ - errno = ENOSYS; - default: - giterr_set(GITERR_OS, "Failed to make directory '%s'", - make_path.ptr); - goto fail; + int tmp_errno = errno; + + /* ignore error if directory already exists */ + if (p_stat(make_path.ptr, &st) < 0 || !S_ISDIR(st.st_mode)) { + errno = tmp_errno; + giterr_set(GITERR_OS, "Failed to make directory '%s'", make_path.ptr); + goto done; } - if (already_exists && (flags & GIT_MKDIR_EXCL) != 0) { - giterr_set(GITERR_OS, "Directory already exists '%s'", - make_path.ptr); + /* with exclusive create, existing dir is an error */ + if ((flags & GIT_MKDIR_EXCL) != 0) { + giterr_set(GITERR_OS, "Directory already exists '%s'", make_path.ptr); error = GIT_EEXISTS; - goto fail; + goto done; } } - /* chmod if requested */ - if ((flags & GIT_MKDIR_CHMOD_PATH) != 0 || - ((flags & GIT_MKDIR_CHMOD) != 0 && lastch == '\0')) - { - if (p_chmod(make_path.ptr, mode) < 0) { - giterr_set(GITERR_OS, "Failed to set permissions on '%s'", - make_path.ptr); - goto fail; - } + /* chmod if requested and necessary */ + if (((flags & GIT_MKDIR_CHMOD_PATH) != 0 || + (lastch == '\0' && (flags & GIT_MKDIR_CHMOD) != 0)) && + st.st_mode != mode && + (error = p_chmod(make_path.ptr, mode)) < 0) { + giterr_set(GITERR_OS, "Failed to set permissions on '%s'", make_path.ptr); + goto done; } - - *tail = lastch; } - git_buf_free(&make_path); - return 0; + error = 0; + + /* check that full path really is a directory if requested & needed */ + if ((flags & GIT_MKDIR_VERIFY_DIR) != 0 && + lastch != '\0' && + (p_stat(make_path.ptr, &st) < 0 || !S_ISDIR(st.st_mode))) { + giterr_set(GITERR_OS, "Path is not a directory '%s'", make_path.ptr); + error = GIT_ENOTFOUND; + } -fail: +done: git_buf_free(&make_path); return error; } @@ -444,7 +467,7 @@ static int futils__rmdir_recurs_foreach(void *opaque, git_buf *path) if (data->error < 0) { if ((data->flags & GIT_RMDIR_SKIP_NONEMPTY) != 0 && - (errno == ENOTEMPTY || errno == EEXIST)) + (errno == ENOTEMPTY || errno == EEXIST || errno == EBUSY)) data->error = 0; else futils__error_cannot_rmdir(path->ptr, NULL); @@ -480,7 +503,7 @@ static int futils__rmdir_empty_parent(void *opaque, git_buf *path) if (en == ENOENT || en == ENOTDIR) { giterr_clear(); error = 0; - } else if (en == ENOTEMPTY || en == EEXIST) { + } else if (en == ENOTEMPTY || en == EEXIST || en == EBUSY) { giterr_clear(); error = GIT_ITEROVER; } else { @@ -605,6 +628,18 @@ static git_futils_dirs_guess_cb git_futils__dir_guess[GIT_FUTILS_DIR__MAX] = { git_futils_guess_xdg_dirs, }; +int git_futils_dirs_global_init(void) +{ + git_futils_dir_t i; + const git_buf *path; + int error = 0; + + for (i = 0; !error && i < GIT_FUTILS_DIR__MAX; i++) + error = git_futils_dirs_get(&path, i); + + return error; +} + static int git_futils_check_selector(git_futils_dir_t which) { if (which < GIT_FUTILS_DIR__MAX) @@ -988,8 +1023,10 @@ int git_futils_filestamp_check( if (stamp == NULL) return 1; - if (p_stat(path, &st) < 0) + if (p_stat(path, &st) < 0) { + giterr_set(GITERR_OS, "Could not stat '%s'", path); return GIT_ENOTFOUND; + } if (stamp->mtime == (git_time_t)st.st_mtime && stamp->size == (git_off_t)st.st_size && diff --git a/src/fileops.h b/src/fileops.h index 627a6923d..5adedfc57 100644 --- a/src/fileops.h +++ b/src/fileops.h @@ -22,6 +22,9 @@ extern int git_futils_readbuffer_updated( git_buf *obj, const char *path, time_t *mtime, size_t *size, int *updated); extern int git_futils_readbuffer_fd(git_buf *obj, git_file fd, size_t len); +extern int git_futils_writebuffer( + const git_buf *buf, const char *path, int open_flags, mode_t mode); + /** * File utils * @@ -223,6 +226,7 @@ extern git_off_t git_futils_filesize(git_file fd); #define GIT_MODE_PERMS_MASK 0777 #define GIT_CANONICAL_PERMS(MODE) (((MODE) & 0100) ? 0755 : 0644) #define GIT_MODE_TYPE(MODE) ((MODE) & ~GIT_MODE_PERMS_MASK) +#define GIT_MODE_ISBLOB(MODE) (GIT_MODE_TYPE(MODE) == GIT_MODE_TYPE(GIT_FILEMODE_BLOB)) /** * Convert a mode_t from the OS to a legal git mode_t value. @@ -240,7 +244,7 @@ extern mode_t git_futils_canonical_mode(mode_t raw_mode); * @param out buffer to populate with the mapping information. * @param fd open descriptor to configure the mapping from. * @param begin first byte to map, this should be page aligned. - * @param end number of bytes to map. + * @param len number of bytes to map. * @return * - 0 on success; * - -1 on error. @@ -274,7 +278,7 @@ extern void git_futils_mmap_free(git_map *map); /** * Find a "global" file (i.e. one in a user's home directory). * - * @param pathbuf buffer to write the full path into + * @param path buffer to write the full path into * @param filename name of file to find in the home directory * @return 0 if found, GIT_ENOTFOUND if not found, or -1 on other OS error */ @@ -283,7 +287,7 @@ extern int git_futils_find_global_file(git_buf *path, const char *filename); /** * Find an "XDG" file (i.e. one in user's XDG config path). * - * @param pathbuf buffer to write the full path into + * @param path buffer to write the full path into * @param filename name of file to find in the home directory * @return 0 if found, GIT_ENOTFOUND if not found, or -1 on other OS error */ @@ -292,7 +296,7 @@ extern int git_futils_find_xdg_file(git_buf *path, const char *filename); /** * Find a "system" file (i.e. one shared for all users of the system). * - * @param pathbuf buffer to write the full path into + * @param path buffer to write the full path into * @param filename name of file to find in the home directory * @return 0 if found, GIT_ENOTFOUND if not found, or -1 on other OS error */ @@ -306,6 +310,13 @@ typedef enum { } git_futils_dir_t; /** + * Configures global data for configuration file search paths. + * + * @return 0 on success, <0 on failure + */ +extern int git_futils_dirs_global_init(void); + +/** * Get the search path for global/system/xdg files * * @param out pointer to git_buf containing search path diff --git a/src/global.c b/src/global.c index b7fd8e257..a06d0c81f 100644 --- a/src/global.c +++ b/src/global.c @@ -61,13 +61,12 @@ int git_threads_init(void) return 0; _tls_index = TlsAlloc(); - git_mutex_init(&git__mwindow_mutex); + if (git_mutex_init(&git__mwindow_mutex)) + return -1; /* Initialize any other subsystems that have global state */ - if ((error = git_hash_global_init()) >= 0) - _tls_init = 1; - - if (error == 0) + if ((error = git_hash_global_init()) >= 0 && + (error = git_futils_dirs_global_init()) >= 0) _tls_init = 1; GIT_MEMORY_BARRIER; @@ -121,11 +120,13 @@ int git_threads_init(void) if (_tls_init) return 0; - git_mutex_init(&git__mwindow_mutex); + if (git_mutex_init(&git__mwindow_mutex)) + return -1; pthread_key_create(&_tls_key, &cb__free_status); /* Initialize any other subsystems that have global state */ - if ((error = git_hash_global_init()) >= 0) + if ((error = git_hash_global_init()) >= 0 && + (error = git_futils_dirs_global_init()) >= 0) _tls_init = 1; GIT_MEMORY_BARRIER; @@ -135,6 +136,12 @@ int git_threads_init(void) void git_threads_shutdown(void) { + if (_tls_init) { + void *ptr = pthread_getspecific(_tls_key); + pthread_setspecific(_tls_key, NULL); + git__free(ptr); + } + pthread_key_delete(_tls_key); _tls_init = 0; git_mutex_free(&git__mwindow_mutex); diff --git a/src/global.h b/src/global.h index f0ad1df29..badbc0883 100644 --- a/src/global.h +++ b/src/global.h @@ -10,14 +10,6 @@ #include "mwindow.h" #include "hash.h" -#if defined(GIT_THREADS) && defined(_MSC_VER) -# define GIT_MEMORY_BARRIER MemoryBarrier() -#elif defined(GIT_THREADS) -# define GIT_MEMORY_BARRIER __sync_synchronize() -#else -# define GIT_MEMORY_BARRIER /* noop */ -#endif - typedef struct { git_error *last_error; git_error error_t; diff --git a/src/hash/hash_generic.h b/src/hash/hash_generic.h index b731de8b3..6b60c98c4 100644 --- a/src/hash/hash_generic.h +++ b/src/hash/hash_generic.h @@ -11,9 +11,9 @@ #include "hash.h" struct git_hash_ctx { - unsigned long long size; - unsigned int H[5]; - unsigned int W[16]; + unsigned long long size; + unsigned int H[5]; + unsigned int W[16]; }; #define git_hash_global_init() 0 diff --git a/src/hash/hash_win32.h b/src/hash/hash_win32.h index daa769b59..2eee5ca79 100644 --- a/src/hash/hash_win32.h +++ b/src/hash/hash_win32.h @@ -48,10 +48,10 @@ struct hash_cryptoapi_prov { /* Function declarations for CNG */ typedef NTSTATUS (WINAPI *hash_win32_cng_open_algorithm_provider_fn)( - HANDLE /* BCRYPT_ALG_HANDLE */ *phAlgorithm, - LPCWSTR pszAlgId, - LPCWSTR pszImplementation, - DWORD dwFlags); + HANDLE /* BCRYPT_ALG_HANDLE */ *phAlgorithm, + LPCWSTR pszAlgId, + LPCWSTR pszImplementation, + DWORD dwFlags); typedef NTSTATUS (WINAPI *hash_win32_cng_get_property_fn)( HANDLE /* BCRYPT_HANDLE */ hObject, diff --git a/src/hashsig.c b/src/hashsig.c index 3a75aaaed..109f966ba 100644 --- a/src/hashsig.c +++ b/src/hashsig.c @@ -13,12 +13,15 @@ typedef uint64_t hashsig_state; #define HASHSIG_SCALE 100 -#define HASHSIG_HASH_WINDOW 32 -#define HASHSIG_HASH_START 0 +#define HASHSIG_MAX_RUN 80 +#define HASHSIG_HASH_START 0x012345678ABCDEF0LL #define HASHSIG_HASH_SHIFT 5 -#define HASHSIG_HASH_MASK 0x7FFFFFFF + +#define HASHSIG_HASH_MIX(S,CH) \ + (S) = ((S) << HASHSIG_HASH_SHIFT) - (S) + (hashsig_state)(CH) #define HASHSIG_HEAP_SIZE ((1 << 7) - 1) +#define HASHSIG_HEAP_MIN_SIZE 4 typedef int (*hashsig_cmp)(const void *a, const void *b, void *); @@ -28,14 +31,6 @@ typedef struct { hashsig_t values[HASHSIG_HEAP_SIZE]; } hashsig_heap; -typedef struct { - hashsig_state state, shift_n; - char window[HASHSIG_HASH_WINDOW]; - int win_len, win_pos, saw_lf; -} hashsig_in_progress; - -#define HASHSIG_IN_PROGRESS_INIT { HASHSIG_HASH_START, 1, {0}, 0, 0, 1 } - struct git_hashsig { hashsig_heap mins; hashsig_heap maxs; @@ -43,8 +38,8 @@ struct git_hashsig { int considered; }; -#define HEAP_LCHILD_OF(I) (((I)*2)+1) -#define HEAP_RCHILD_OF(I) (((I)*2)+2) +#define HEAP_LCHILD_OF(I) (((I)<<1)+1) +#define HEAP_RCHILD_OF(I) (((I)<<1)+2) #define HEAP_PARENT_OF(I) (((I)-1)>>1) static void hashsig_heap_init(hashsig_heap *h, hashsig_cmp cmp) @@ -115,134 +110,109 @@ static void hashsig_heap_sort(hashsig_heap *h) static void hashsig_heap_insert(hashsig_heap *h, hashsig_t val) { - /* if heap is full, pop top if new element should replace it */ - if (h->size == h->asize && h->cmp(&val, &h->values[0], NULL) > 0) { - h->size--; - h->values[0] = h->values[h->size]; - hashsig_heap_down(h, 0); - } - /* if heap is not full, insert new element */ if (h->size < h->asize) { h->values[h->size++] = val; hashsig_heap_up(h, h->size - 1); } -} - -GIT_INLINE(bool) hashsig_include_char( - char ch, git_hashsig_option_t opt, int *saw_lf) -{ - if ((opt & GIT_HASHSIG_IGNORE_WHITESPACE) && git__isspace(ch)) - return false; - - if (opt & GIT_HASHSIG_SMART_WHITESPACE) { - if (ch == '\r' || (*saw_lf && git__isspace(ch))) - return false; - *saw_lf = (ch == '\n'); + /* if heap is full, pop top if new element should replace it */ + else if (h->cmp(&val, &h->values[0], NULL) > 0) { + h->size--; + h->values[0] = h->values[h->size]; + hashsig_heap_down(h, 0); } - return true; } -static void hashsig_initial_window( - git_hashsig *sig, - const char **data, - size_t size, - hashsig_in_progress *prog) -{ - hashsig_state state, shift_n; - int win_len; - const char *scan, *end; - - /* init until we have processed at least HASHSIG_HASH_WINDOW data */ - - if (prog->win_len >= HASHSIG_HASH_WINDOW) - return; - - state = prog->state; - win_len = prog->win_len; - shift_n = prog->shift_n; - - scan = *data; - end = scan + size; - - while (scan < end && win_len < HASHSIG_HASH_WINDOW) { - char ch = *scan++; - - if (!hashsig_include_char(ch, sig->opt, &prog->saw_lf)) - continue; - - state = (state * HASHSIG_HASH_SHIFT + ch) & HASHSIG_HASH_MASK; - - if (!win_len) - shift_n = 1; - else - shift_n = (shift_n * HASHSIG_HASH_SHIFT) & HASHSIG_HASH_MASK; - - prog->window[win_len++] = ch; - } - - /* insert initial hash if we just finished */ +typedef struct { + int use_ignores; + uint8_t ignore_ch[256]; +} hashsig_in_progress; - if (win_len == HASHSIG_HASH_WINDOW) { - hashsig_heap_insert(&sig->mins, (hashsig_t)state); - hashsig_heap_insert(&sig->maxs, (hashsig_t)state); - sig->considered = 1; +static void hashsig_in_progress_init( + hashsig_in_progress *prog, git_hashsig *sig) +{ + int i; + + switch (sig->opt) { + case GIT_HASHSIG_IGNORE_WHITESPACE: + for (i = 0; i < 256; ++i) + prog->ignore_ch[i] = git__isspace_nonlf(i); + prog->use_ignores = 1; + break; + case GIT_HASHSIG_SMART_WHITESPACE: + for (i = 0; i < 256; ++i) + prog->ignore_ch[i] = git__isspace(i); + prog->use_ignores = 1; + break; + default: + memset(prog, 0, sizeof(*prog)); + break; } - - prog->state = state; - prog->win_len = win_len; - prog->shift_n = shift_n; - - *data = scan; } +#define HASHSIG_IN_PROGRESS_INIT { 1 } + static int hashsig_add_hashes( git_hashsig *sig, - const char *data, + const uint8_t *data, size_t size, hashsig_in_progress *prog) { - const char *scan = data, *end = data + size; - hashsig_state state, shift_n, rmv; - - if (prog->win_len < HASHSIG_HASH_WINDOW) - hashsig_initial_window(sig, &scan, size, prog); - - state = prog->state; - shift_n = prog->shift_n; - - /* advance window, adding new chars and removing old */ - - for (; scan < end; ++scan) { - char ch = *scan; - - if (!hashsig_include_char(ch, sig->opt, &prog->saw_lf)) - continue; - - rmv = shift_n * prog->window[prog->win_pos]; + const uint8_t *scan = data, *end = data + size; + hashsig_state state = HASHSIG_HASH_START; + int use_ignores = prog->use_ignores, len; + uint8_t ch; + + while (scan < end) { + state = HASHSIG_HASH_START; + + for (len = 0; scan < end && len < HASHSIG_MAX_RUN; ) { + ch = *scan; + + if (use_ignores) + for (; scan < end && git__isspace_nonlf(ch); ch = *scan) + ++scan; + else if (sig->opt != GIT_HASHSIG_NORMAL) + for (; scan < end && ch == '\r'; ch = *scan) + ++scan; + + /* peek at next character to decide what to do next */ + if (sig->opt == GIT_HASHSIG_SMART_WHITESPACE) + use_ignores = (ch == '\n'); + + if (scan >= end) + break; + ++scan; + + /* check run terminator */ + if (ch == '\n' || ch == '\0') + break; + + ++len; + HASHSIG_HASH_MIX(state, ch); + } - state = (state - rmv) & HASHSIG_HASH_MASK; - state = (state * HASHSIG_HASH_SHIFT) & HASHSIG_HASH_MASK; - state = (state + ch) & HASHSIG_HASH_MASK; + if (len > 0) { + hashsig_heap_insert(&sig->mins, (hashsig_t)state); + hashsig_heap_insert(&sig->maxs, (hashsig_t)state); - hashsig_heap_insert(&sig->mins, (hashsig_t)state); - hashsig_heap_insert(&sig->maxs, (hashsig_t)state); - sig->considered++; + sig->considered++; - prog->window[prog->win_pos] = ch; - prog->win_pos = (prog->win_pos + 1) % HASHSIG_HASH_WINDOW; + while (scan < end && (*scan == '\n' || !*scan)) + ++scan; + } } - prog->state = state; + prog->use_ignores = use_ignores; return 0; } static int hashsig_finalize_hashes(git_hashsig *sig) { - if (sig->mins.size < HASHSIG_HEAP_SIZE) { + if (sig->mins.size < HASHSIG_HEAP_MIN_SIZE) { giterr_set(GITERR_INVALID, "File too small for similarity signature calculation"); return GIT_EBUFS; @@ -274,11 +244,13 @@ int git_hashsig_create( git_hashsig_option_t opts) { int error; - hashsig_in_progress prog = HASHSIG_IN_PROGRESS_INIT; + hashsig_in_progress prog; git_hashsig *sig = hashsig_alloc(opts); GITERR_CHECK_ALLOC(sig); - error = hashsig_add_hashes(sig, buf, buflen, &prog); + hashsig_in_progress_init(&prog, sig); + + error = hashsig_add_hashes(sig, (const uint8_t *)buf, buflen, &prog); if (!error) error = hashsig_finalize_hashes(sig); @@ -296,10 +268,10 @@ int git_hashsig_create_fromfile( const char *path, git_hashsig_option_t opts) { - char buf[4096]; + uint8_t buf[0x1000]; ssize_t buflen = 0; int error = 0, fd; - hashsig_in_progress prog = HASHSIG_IN_PROGRESS_INIT; + hashsig_in_progress prog; git_hashsig *sig = hashsig_alloc(opts); GITERR_CHECK_ALLOC(sig); @@ -308,6 +280,8 @@ int git_hashsig_create_fromfile( return fd; } + hashsig_in_progress_init(&prog, sig); + while (!error) { if ((buflen = p_read(fd, buf, sizeof(buf))) <= 0) { if ((error = (int)buflen) < 0) @@ -362,7 +336,12 @@ static int hashsig_heap_compare(const hashsig_heap *a, const hashsig_heap *b) int git_hashsig_compare(const git_hashsig *a, const git_hashsig *b) { - return (hashsig_heap_compare(&a->mins, &b->mins) + - hashsig_heap_compare(&a->maxs, &b->maxs)) / 2; + /* if we have fewer than the maximum number of elements, then just use + * one array since the two arrays will be the same + */ + if (a->mins.size < HASHSIG_HEAP_SIZE) + return hashsig_heap_compare(&a->mins, &b->mins); + else + return (hashsig_heap_compare(&a->mins, &b->mins) + + hashsig_heap_compare(&a->maxs, &b->maxs)) / 2; } - diff --git a/src/ignore.c b/src/ignore.c index dae974b6e..7d8280403 100644 --- a/src/ignore.c +++ b/src/ignore.c @@ -15,24 +15,14 @@ static int parse_ignore_file( git_attr_fnmatch *match = NULL; const char *scan = NULL; char *context = NULL; - bool ignore_case = false; - git_config *cfg = NULL; - int val; - - /* Prefer to have the caller pass in a git_ignores as the parsedata object. - * If they did not, then we can (much more slowly) find the value of - * ignore_case by using the repository object. */ - if (parsedata != NULL) { - ignore_case = ((git_ignores *)parsedata)->ignore_case; - } else { - if ((error = git_repository_config(&cfg, repo)) < 0) - return error; - - if (git_config_get_bool(&val, cfg, "core.ignorecase") == 0) - ignore_case = (val != 0); + int ignore_case = false; - git_config_free(cfg); - } + /* Prefer to have the caller pass in a git_ignores as the parsedata + * object. If they did not, then look up the value of ignore_case */ + if (parsedata != NULL) + ignore_case = ((git_ignores *)parsedata)->ignore_case; + else if (git_repository__cvar(&ignore_case, repo, GIT_CVAR_IGNORECASE) < 0) + return error; if (ignores->key && git__suffixcmp(ignores->key, "/" GIT_IGNORE_FILE) == 0) { context = ignores->key + 2; @@ -109,8 +99,6 @@ int git_ignore__for_path( { int error = 0; const char *workdir = git_repository_workdir(repo); - git_config *cfg = NULL; - int val; assert(ignores); @@ -118,17 +106,11 @@ int git_ignore__for_path( git_buf_init(&ignores->dir, 0); ignores->ign_internal = NULL; - /* Set the ignore_case flag appropriately */ - if ((error = git_repository_config(&cfg, repo)) < 0) + /* Read the ignore_case flag */ + if ((error = git_repository__cvar( + &ignores->ignore_case, repo, GIT_CVAR_IGNORECASE)) < 0) goto cleanup; - if (git_config_get_bool(&val, cfg, "core.ignorecase") == 0) - ignores->ignore_case = (val != 0); - else - ignores->ignore_case = 0; - - git_config_free(cfg); - if ((error = git_vector_init(&ignores->ign_path, 8, NULL)) < 0 || (error = git_vector_init(&ignores->ign_global, 2, NULL)) < 0 || (error = git_attr_cache__init(repo)) < 0) @@ -358,3 +340,61 @@ cleanup: return error; } + +int git_ignore__check_pathspec_for_exact_ignores( + git_repository *repo, + git_vector *vspec, + bool no_fnmatch) +{ + int error = 0; + size_t i; + git_attr_fnmatch *match; + int ignored; + git_buf path = GIT_BUF_INIT; + const char *wd, *filename; + git_index *idx; + + if ((error = git_repository__ensure_not_bare( + repo, "validate pathspec")) < 0 || + (error = git_repository_index(&idx, repo)) < 0) + return error; + + wd = git_repository_workdir(repo); + + git_vector_foreach(vspec, i, match) { + /* skip wildcard matches (if they are being used) */ + if ((match->flags & GIT_ATTR_FNMATCH_HASWILD) != 0 && + !no_fnmatch) + continue; + + filename = match->pattern; + + /* if file is already in the index, it's fine */ + if (git_index_get_bypath(idx, filename, 0) != NULL) + continue; + + if ((error = git_buf_joinpath(&path, wd, filename)) < 0) + break; + + /* is there a file on disk that matches this exactly? */ + if (!git_path_isfile(path.ptr)) + continue; + + /* is that file ignored? */ + if ((error = git_ignore_path_is_ignored(&ignored, repo, filename)) < 0) + break; + + if (ignored) { + giterr_set(GITERR_INVALID, "pathspec contains ignored file '%s'", + filename); + error = GIT_EINVALIDSPEC; + break; + } + } + + git_index_free(idx); + git_buf_free(&path); + + return error; +} + diff --git a/src/ignore.h b/src/ignore.h index 5af8e8e7d..cc114b001 100644 --- a/src/ignore.h +++ b/src/ignore.h @@ -28,7 +28,7 @@ typedef struct { git_attr_file *ign_internal; git_vector ign_path; git_vector ign_global; - unsigned int ignore_case:1; + int ignore_case; } git_ignores; extern int git_ignore__for_path(git_repository *repo, const char *path, git_ignores *ign); @@ -41,4 +41,13 @@ extern void git_ignore__free(git_ignores *ign); extern int git_ignore__lookup(git_ignores *ign, const char *path, int *ignored); +/* command line Git sometimes generates an error message if given a + * pathspec that contains an exact match to an ignored file (provided + * --force isn't also given). This makes it easy to check it that has + * happened. Returns GIT_EINVALIDSPEC if the pathspec contains ignored + * exact matches (that are not already present in the index). + */ +extern int git_ignore__check_pathspec_for_exact_ignores( + git_repository *repo, git_vector *pathspec, bool no_fnmatch); + #endif diff --git a/src/index.c b/src/index.c index 6290ec4e8..cbdd43bdc 100644 --- a/src/index.c +++ b/src/index.c @@ -15,10 +15,14 @@ #include "hash.h" #include "iterator.h" #include "pathspec.h" +#include "ignore.h" +#include "blob.h" + #include "git2/odb.h" #include "git2/oid.h" #include "git2/blob.h" #include "git2/config.h" +#include "git2/sys/index.h" #define entry_size(type,len) ((offsetof(type, path) + (len) + 8) & ~7) #define short_entry_size(len) entry_size(struct entry_short, len) @@ -35,6 +39,7 @@ static const unsigned int INDEX_VERSION_NUMBER_EXT = 3; static const unsigned int INDEX_HEADER_SIG = 0x44495243; static const char INDEX_EXT_TREECACHE_SIG[] = {'T', 'R', 'E', 'E'}; static const char INDEX_EXT_UNMERGED_SIG[] = {'R', 'E', 'U', 'C'}; +static const char INDEX_EXT_CONFLICT_NAME_SIG[] = {'N', 'A', 'M', 'E'}; #define INDEX_OWNER(idx) ((git_repository *)(GIT_REFCOUNT_OWNER(idx))) @@ -97,16 +102,9 @@ 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); -GIT_INLINE(int) index_entry_stage(const git_index_entry *entry) -{ - return (entry->flags & GIT_IDXENTRY_STAGEMASK) >> GIT_IDXENTRY_STAGESHIFT; -} - static int index_srch(const void *key, const void *array_member) { const struct entry_srch_key *srch_key = key; @@ -115,8 +113,8 @@ static int index_srch(const void *key, const void *array_member) ret = strcmp(srch_key->path, entry->path); - if (ret == 0) - ret = srch_key->stage - index_entry_stage(entry); + if (ret == 0 && srch_key->stage != GIT_INDEX_STAGE_ANY) + ret = srch_key->stage - GIT_IDXENTRY_STAGE(entry); return ret; } @@ -129,8 +127,8 @@ static int index_isrch(const void *key, const void *array_member) ret = strcasecmp(srch_key->path, entry->path); - if (ret == 0) - ret = srch_key->stage - index_entry_stage(entry); + if (ret == 0 && srch_key->stage != GIT_INDEX_STAGE_ANY) + ret = srch_key->stage - GIT_IDXENTRY_STAGE(entry); return ret; } @@ -168,7 +166,7 @@ static int index_cmp(const void *a, const void *b) diff = strcmp(entry_a->path, entry_b->path); if (diff == 0) - diff = (index_entry_stage(entry_a) - index_entry_stage(entry_b)); + diff = (GIT_IDXENTRY_STAGE(entry_a) - GIT_IDXENTRY_STAGE(entry_b)); return diff; } @@ -182,11 +180,56 @@ static int index_icmp(const void *a, const void *b) diff = strcasecmp(entry_a->path, entry_b->path); if (diff == 0) - diff = (index_entry_stage(entry_a) - index_entry_stage(entry_b)); + diff = (GIT_IDXENTRY_STAGE(entry_a) - GIT_IDXENTRY_STAGE(entry_b)); return diff; } +static int conflict_name_cmp(const void *a, const void *b) +{ + const git_index_name_entry *name_a = a; + const git_index_name_entry *name_b = b; + + if (name_a->ancestor && !name_b->ancestor) + return 1; + + if (!name_a->ancestor && name_b->ancestor) + return -1; + + if (name_a->ancestor) + return strcmp(name_a->ancestor, name_b->ancestor); + + if (!name_a->ours || !name_b->ours) + return 0; + + return strcmp(name_a->ours, name_b->ours); +} + +/** + * TODO: enable this when resolving case insensitive conflicts + */ +#if 0 +static int conflict_name_icmp(const void *a, const void *b) +{ + const git_index_name_entry *name_a = a; + const git_index_name_entry *name_b = b; + + if (name_a->ancestor && !name_b->ancestor) + return 1; + + if (!name_a->ancestor && name_b->ancestor) + return -1; + + if (name_a->ancestor) + return strcasecmp(name_a->ancestor, name_b->ancestor); + + if (!name_a->ours || !name_b->ours) + return 0; + + return strcasecmp(name_a->ours, name_b->ours); +} +#endif + static int reuc_srch(const void *key, const void *array_member) { const git_index_reuc_entry *reuc = array_member; @@ -217,6 +260,22 @@ static int reuc_icmp(const void *a, const void *b) return strcasecmp(info_a->path, info_b->path); } +static void index_entry_reuc_free(git_index_reuc_entry *reuc) +{ + if (!reuc) + return; + git__free(reuc->path); + git__free(reuc); +} + +static void index_entry_free(git_index_entry *entry) +{ + if (!entry) + return; + git__free(entry->path); + git__free(entry); +} + static unsigned int index_create_mode(unsigned int mode) { if (S_ISLNK(mode)) @@ -246,16 +305,16 @@ void git_index__set_ignore_case(git_index *index, bool ignore_case) { index->ignore_case = ignore_case; - index->entries._cmp = ignore_case ? index_icmp : index_cmp; index->entries_cmp_path = ignore_case ? index_icmp_path : index_cmp_path; index->entries_search = ignore_case ? index_isrch : index_srch; index->entries_search_path = ignore_case ? index_isrch_path : index_srch_path; - index->entries.sorted = 0; + + git_vector_set_cmp(&index->entries, ignore_case ? index_icmp : index_cmp); git_vector_sort(&index->entries); - index->reuc._cmp = ignore_case ? reuc_icmp : reuc_cmp; index->reuc_search = ignore_case ? reuc_isrch : reuc_srch; - index->reuc.sorted = 0; + + git_vector_set_cmp(&index->reuc, ignore_case ? reuc_icmp : reuc_cmp); git_vector_sort(&index->reuc); } @@ -278,6 +337,7 @@ int git_index_open(git_index **index_out, const char *index_path) } if (git_vector_init(&index->entries, 32, index_cmp) < 0 || + git_vector_init(&index->names, 32, conflict_name_cmp) < 0 || git_vector_init(&index->reuc, 32, reuc_cmp) < 0) return -1; @@ -301,9 +361,12 @@ static void index_free(git_index *index) { git_index_clear(index); git_vector_free(&index->entries); + git_vector_free(&index->names); git_vector_free(&index->reuc); git__free(index->index_file_path); + + git__memzero(index, sizeof(*index)); git__free(index); } @@ -315,22 +378,24 @@ void git_index_free(git_index *index) GIT_REFCOUNT_DEC(index, index_free); } -void git_index_clear(git_index *index) +static void index_entries_free(git_vector *entries) { size_t i; - assert(index); + for (i = 0; i < entries->length; ++i) + index_entry_free(git__swap(entries->contents[i], NULL)); - for (i = 0; i < index->entries.length; ++i) { - git_index_entry *e; - e = git_vector_get(&index->entries, i); - git__free(e->path); - git__free(e); - } - git_vector_clear(&index->entries); + git_vector_clear(entries); +} + +void git_index_clear(git_index *index) +{ + assert(index); + index_entries_free(&index->entries); git_index_reuc_clear(index); - + git_index_name_clear(index); + git_futils_filestamp_set(&index->stamp, NULL); git_tree_cache_free(index->tree); @@ -352,19 +417,18 @@ int git_index_set_caps(git_index *index, unsigned int caps) old_ignore_case = index->ignore_case; if (caps == GIT_INDEXCAP_FROM_OWNER) { - git_config *cfg; + git_repository *repo = INDEX_OWNER(index); int val; - if (INDEX_OWNER(index) == NULL || - git_repository_config__weakptr(&cfg, INDEX_OWNER(index)) < 0) - return create_index_error(-1, - "Cannot get repository config to set index caps"); + if (!repo) + return create_index_error( + -1, "Cannot access repository to set index caps"); - if (git_config_get_bool(&val, cfg, "core.ignorecase") == 0) + if (!git_repository__cvar(&val, repo, GIT_CVAR_IGNORECASE)) index->ignore_case = (val != 0); - if (git_config_get_bool(&val, cfg, "core.filemode") == 0) + if (!git_repository__cvar(&val, repo, GIT_CVAR_FILEMODE)) index->distrust_filemode = (val == 0); - if (git_config_get_bool(&val, cfg, "core.symlinks") == 0) + if (!git_repository__cvar(&val, repo, GIT_CVAR_SYMLINKS)) index->no_symlinks = (val == 0); } else { @@ -453,6 +517,12 @@ int git_index_write(git_index *index) return 0; } +const char * git_index_path(git_index *index) +{ + assert(index); + return index->index_file_path; +} + int git_index_write_tree(git_oid *oid, git_index *index) { git_repository *repo; @@ -497,8 +567,10 @@ 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; + } return git_index_get_byindex(index, pos); } @@ -533,42 +605,23 @@ int git_index_entry__cmp_icase(const void *a, const void *b) return strcasecmp(entry_a->path, entry_b->path); } -static int index_entry_init(git_index_entry **entry_out, git_index *index, const char *rel_path) +static int index_entry_init( + git_index_entry **entry_out, git_index *index, const char *rel_path) { + int error = 0; git_index_entry *entry = NULL; struct stat st; git_oid oid; - const char *workdir; - git_buf full_path = GIT_BUF_INIT; - int error; if (INDEX_OWNER(index) == NULL) return create_index_error(-1, "Could not initialize index entry. " "Index is not backed up by an existing repository."); - workdir = git_repository_workdir(INDEX_OWNER(index)); - - if (!workdir) - return create_index_error(GIT_EBAREREPO, - "Could not initialize index entry. Repository is bare"); - - if ((error = git_buf_joinpath(&full_path, workdir, rel_path)) < 0) - return error; - - if ((error = git_path_lstat(full_path.ptr, &st)) < 0) { - git_buf_free(&full_path); - return error; - } - - git_buf_free(&full_path); /* done with full path */ - - /* There is no need to validate the rel_path here, since it will be - * immediately validated by the call to git_blob_create_fromfile. - */ - - /* write the blob to disk and get the oid */ - if ((error = git_blob_create_fromworkdir(&oid, INDEX_OWNER(index), rel_path)) < 0) + /* write the blob to disk and get the oid and stat info */ + error = git_blob__create_from_paths( + &oid, &st, INDEX_OWNER(index), NULL, rel_path, 0, true); + if (error < 0) return error; entry = git__calloc(1, sizeof(git_index_entry)); @@ -586,8 +639,9 @@ static int index_entry_init(git_index_entry **entry_out, git_index *index, const static int index_entry_reuc_init(git_index_reuc_entry **reuc_out, const char *path, - int ancestor_mode, git_oid *ancestor_oid, - int our_mode, git_oid *our_oid, int their_mode, git_oid *their_oid) + int ancestor_mode, const git_oid *ancestor_oid, + int our_mode, const git_oid *our_oid, + int their_mode, const git_oid *their_oid) { git_index_reuc_entry *reuc = NULL; @@ -615,15 +669,6 @@ static int index_entry_reuc_init(git_index_reuc_entry **reuc_out, return 0; } -static void index_entry_reuc_free(git_index_reuc_entry *reuc) -{ - if (!reuc) - return; - - git__free(reuc->path); - git__free(reuc); -} - static git_index_entry *index_entry_dup(const git_index_entry *source_entry) { git_index_entry *entry; @@ -642,14 +687,6 @@ static git_index_entry *index_entry_dup(const git_index_entry *source_entry) return entry; } -static void index_entry_free(git_index_entry *entry) -{ - if (!entry) - return; - git__free(entry->path); - git__free(entry); -} - static int index_insert(git_index *index, git_index_entry *entry, int replace) { size_t path_length, position; @@ -668,7 +705,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, index_entry_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 */ @@ -681,8 +719,9 @@ static int index_insert(git_index *index, git_index_entry *entry, int replace) if (!replace || !existing) return git_vector_insert(&index->entries, entry); - /* exists, replace it */ - git__free((*existing)->path); + /* exists, replace it (preserving name from existing entry) */ + git__free(entry->path); + entry->path = (*existing)->path; git__free(*existing); *existing = entry; @@ -691,9 +730,9 @@ static int index_insert(git_index *index, git_index_entry *entry, int replace) static int index_conflict_to_reuc(git_index *index, const char *path) { - git_index_entry *conflict_entries[3]; + const git_index_entry *conflict_entries[3]; int ancestor_mode, our_mode, their_mode; - git_oid *ancestor_oid, *our_oid, *their_oid; + git_oid const *ancestor_oid, *our_oid, *their_oid; int ret; if ((ret = git_index_conflict_get(&conflict_entries[0], @@ -779,8 +818,11 @@ 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; + } entry = git_vector_get(&index->entries, position); if (entry != NULL) @@ -813,7 +855,7 @@ int git_index_remove_directory(git_index *index, const char *dir, int stage) if (!entry || git__prefixcmp(entry->path, pfx.ptr) != 0) break; - if (index_entry_stage(entry) != stage) { + if (GIT_IDXENTRY_STAGE(entry) != stage) { ++pos; continue; } @@ -832,7 +874,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; @@ -841,7 +884,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) @@ -927,53 +971,80 @@ on_error: return ret; } -int git_index_conflict_get(git_index_entry **ancestor_out, - git_index_entry **our_out, - git_index_entry **their_out, - git_index *index, const char *path) +static int index_conflict__get_byindex( + const git_index_entry **ancestor_out, + const git_index_entry **our_out, + const git_index_entry **their_out, + git_index *index, + size_t n) { - size_t pos, posmax; - int stage; - git_index_entry *conflict_entry; - int error = GIT_ENOTFOUND; + const git_index_entry *conflict_entry; + const char *path = NULL; + size_t count; + int stage, len = 0; - assert(ancestor_out && our_out && their_out && index && path); + assert(ancestor_out && our_out && their_out && index); *ancestor_out = NULL; *our_out = NULL; *their_out = NULL; - if (git_index_find(&pos, index, path) < 0) - return GIT_ENOTFOUND; - - for (posmax = git_index_entrycount(index); pos < posmax; ++pos) { + for (count = git_index_entrycount(index); n < count; ++n) { + conflict_entry = git_vector_get(&index->entries, n); - conflict_entry = git_vector_get(&index->entries, pos); - - if (index->entries_cmp_path(conflict_entry->path, path) != 0) + if (path && index->entries_cmp_path(conflict_entry->path, path) != 0) break; - stage = index_entry_stage(conflict_entry); + stage = GIT_IDXENTRY_STAGE(conflict_entry); + path = conflict_entry->path; switch (stage) { case 3: *their_out = conflict_entry; - error = 0; + len++; break; case 2: *our_out = conflict_entry; - error = 0; + len++; break; case 1: *ancestor_out = conflict_entry; - error = 0; + len++; break; default: break; }; } - return error; + return len; +} + +int git_index_conflict_get( + const git_index_entry **ancestor_out, + const git_index_entry **our_out, + const git_index_entry **their_out, + git_index *index, + const char *path) +{ + size_t pos; + int len = 0; + + assert(ancestor_out && our_out && their_out && index && path); + + *ancestor_out = NULL; + *our_out = NULL; + *their_out = NULL; + + if (git_index_find(&pos, index, path) < 0) + return GIT_ENOTFOUND; + + if ((len = index_conflict__get_byindex( + ancestor_out, our_out, their_out, index, pos)) < 0) + return len; + else if (len == 0) + return GIT_ENOTFOUND; + + return 0; } int git_index_conflict_remove(git_index *index, const char *path) @@ -995,7 +1066,7 @@ int git_index_conflict_remove(git_index *index, const char *path) if (index->entries_cmp_path(conflict_entry->path, path) != 0) break; - if (index_entry_stage(conflict_entry) == 0) { + if (GIT_IDXENTRY_STAGE(conflict_entry) == 0) { pos++; continue; } @@ -1014,7 +1085,7 @@ static int index_conflicts_match(const git_vector *v, size_t idx) { git_index_entry *entry = git_vector_get(v, idx); - if (index_entry_stage(entry) > 0) { + if (GIT_IDXENTRY_STAGE(entry) > 0) { index_entry_free(entry); return 1; } @@ -1036,20 +1107,151 @@ int git_index_has_conflicts(const git_index *index) assert(index); git_vector_foreach(&index->entries, i, entry) { - if (index_entry_stage(entry) > 0) + if (GIT_IDXENTRY_STAGE(entry) > 0) return 1; } return 0; } +int git_index_conflict_iterator_new( + git_index_conflict_iterator **iterator_out, + git_index *index) +{ + git_index_conflict_iterator *it = NULL; + + assert(iterator_out && index); + + it = git__calloc(1, sizeof(git_index_conflict_iterator)); + GITERR_CHECK_ALLOC(it); + + it->index = index; + + *iterator_out = it; + return 0; +} + +int git_index_conflict_next( + const git_index_entry **ancestor_out, + const git_index_entry **our_out, + const git_index_entry **their_out, + git_index_conflict_iterator *iterator) +{ + const git_index_entry *entry; + int len; + + assert(ancestor_out && our_out && their_out && iterator); + + *ancestor_out = NULL; + *our_out = NULL; + *their_out = NULL; + + while (iterator->cur < iterator->index->entries.length) { + entry = git_index_get_byindex(iterator->index, iterator->cur); + + if (git_index_entry_stage(entry) > 0) { + if ((len = index_conflict__get_byindex( + ancestor_out, + our_out, + their_out, + iterator->index, + iterator->cur)) < 0) + return len; + + iterator->cur += len; + return 0; + } + + iterator->cur++; + } + + return GIT_ITEROVER; +} + +void git_index_conflict_iterator_free(git_index_conflict_iterator *iterator) +{ + if (iterator == NULL) + return; + + git__free(iterator); +} + +unsigned int git_index_name_entrycount(git_index *index) +{ + assert(index); + return (unsigned int)index->names.length; +} + +const git_index_name_entry *git_index_name_get_byindex( + git_index *index, size_t n) +{ + assert(index); + + git_vector_sort(&index->names); + return git_vector_get(&index->names, n); +} + +int git_index_name_add(git_index *index, + const char *ancestor, const char *ours, const char *theirs) +{ + git_index_name_entry *conflict_name; + + assert ((ancestor && ours) || (ancestor && theirs) || (ours && theirs)); + + conflict_name = git__calloc(1, sizeof(git_index_name_entry)); + GITERR_CHECK_ALLOC(conflict_name); + + if (ancestor) { + conflict_name->ancestor = git__strdup(ancestor); + GITERR_CHECK_ALLOC(conflict_name->ancestor); + } + + if (ours) { + conflict_name->ours = git__strdup(ours); + GITERR_CHECK_ALLOC(conflict_name->ours); + } + + if (theirs) { + conflict_name->theirs = git__strdup(theirs); + GITERR_CHECK_ALLOC(conflict_name->theirs); + } + + return git_vector_insert(&index->names, conflict_name); +} + +void git_index_name_clear(git_index *index) +{ + size_t i; + git_index_name_entry *conflict_name; + + assert(index); + + git_vector_foreach(&index->names, i, conflict_name) { + if (conflict_name->ancestor) + git__free(conflict_name->ancestor); + + if (conflict_name->ours) + git__free(conflict_name->ours); + + if (conflict_name->theirs) + git__free(conflict_name->theirs); + + git__free(conflict_name); + } + + git_vector_clear(&index->names); +} + unsigned int git_index_reuc_entrycount(git_index *index) { assert(index); return (unsigned int)index->reuc.length; } -static int index_reuc_insert(git_index *index, git_index_reuc_entry *reuc, int replace) +static int index_reuc_insert( + git_index *index, + git_index_reuc_entry *reuc, + int replace) { git_index_reuc_entry **existing = NULL; size_t position; @@ -1071,9 +1273,9 @@ static int index_reuc_insert(git_index *index, git_index_reuc_entry *reuc, int r } int git_index_reuc_add(git_index *index, const char *path, - int ancestor_mode, git_oid *ancestor_oid, - int our_mode, git_oid *our_oid, - int their_mode, git_oid *their_oid) + int ancestor_mode, const git_oid *ancestor_oid, + int our_mode, const git_oid *our_oid, + int their_mode, const git_oid *their_oid) { git_index_reuc_entry *reuc = NULL; int error = 0; @@ -1140,14 +1342,11 @@ int git_index_reuc_remove(git_index *index, size_t position) void git_index_reuc_clear(git_index *index) { size_t i; - git_index_reuc_entry *reuc; assert(index); - git_vector_foreach(&index->reuc, i, reuc) { - git__free(reuc->path); - git__free(reuc); - } + for (i = 0; i < index->reuc.length; ++i) + index_entry_reuc_free(git__swap(index->reuc.contents[i], NULL)); git_vector_clear(&index->reuc); } @@ -1164,8 +1363,9 @@ static int read_reuc(git_index *index, const char *buffer, size_t size) size_t len; int i; - /* This gets called multiple times, the vector might already be initialized */ - if (index->reuc._alloc_size == 0 && git_vector_init(&index->reuc, 16, reuc_cmp) < 0) + /* If called multiple times, the vector might already be initialized */ + if (index->reuc._alloc_size == 0 && + git_vector_init(&index->reuc, 16, reuc_cmp) < 0) return -1; while (size) { @@ -1175,12 +1375,9 @@ static int read_reuc(git_index *index, const char *buffer, size_t size) if (size <= len) return index_error_invalid("reading reuc entries"); - lost = git__malloc(sizeof(git_index_reuc_entry)); + lost = git__calloc(1, sizeof(git_index_reuc_entry)); GITERR_CHECK_ALLOC(lost); - if (git_vector_insert(&index->reuc, lost) < 0) - return -1; - /* read NUL-terminated pathname for entry */ lost->path = git__strdup(buffer); GITERR_CHECK_ALLOC(lost->path); @@ -1194,14 +1391,18 @@ static int read_reuc(git_index *index, const char *buffer, size_t size) if (git__strtol32(&tmp, buffer, &endptr, 8) < 0 || !endptr || endptr == buffer || *endptr || - (unsigned)tmp > UINT_MAX) + (unsigned)tmp > UINT_MAX) { + index_entry_reuc_free(lost); return index_error_invalid("reading reuc entry stage"); + } lost->mode[i] = tmp; len = (endptr + 1) - buffer; - if (size <= len) + if (size <= len) { + index_entry_reuc_free(lost); return index_error_invalid("reading reuc entry stage"); + } size -= len; buffer += len; @@ -1211,13 +1412,19 @@ static int read_reuc(git_index *index, const char *buffer, size_t size) for (i = 0; i < 3; i++) { if (!lost->mode[i]) continue; - if (size < 20) + if (size < 20) { + index_entry_reuc_free(lost); return index_error_invalid("reading reuc entry oid"); + } git_oid_fromraw(&lost->oid[i], (const unsigned char *) buffer); size -= 20; buffer += 20; } + + /* entry was read successfully - insert into reuc vector */ + if (git_vector_insert(&index->reuc, lost) < 0) + return -1; } /* entries are guaranteed to be sorted on-disk */ @@ -1226,6 +1433,52 @@ static int read_reuc(git_index *index, const char *buffer, size_t size) return 0; } + +static int read_conflict_names(git_index *index, const char *buffer, size_t size) +{ + size_t len; + + /* This gets called multiple times, the vector might already be initialized */ + if (index->names._alloc_size == 0 && + git_vector_init(&index->names, 16, conflict_name_cmp) < 0) + return -1; + +#define read_conflict_name(ptr) \ + len = strlen(buffer) + 1; \ + if (size < len) \ + return index_error_invalid("reading conflict name entries"); \ + \ + if (len == 1) \ + ptr = NULL; \ + else { \ + ptr = git__malloc(len); \ + GITERR_CHECK_ALLOC(ptr); \ + memcpy(ptr, buffer, len); \ + } \ + \ + buffer += len; \ + size -= len; + + while (size) { + git_index_name_entry *conflict_name = git__calloc(1, sizeof(git_index_name_entry)); + GITERR_CHECK_ALLOC(conflict_name); + + read_conflict_name(conflict_name->ancestor); + read_conflict_name(conflict_name->ours); + read_conflict_name(conflict_name->theirs); + + if (git_vector_insert(&index->names, conflict_name) < 0) + return -1; + } + +#undef read_conflict_name + + /* entries are guaranteed to be sorted on-disk */ + index->names.sorted = 1; + + return 0; +} + static size_t read_entry(git_index_entry *dest, const void *buffer, size_t buffer_size) { size_t path_length, entry_size; @@ -1318,7 +1571,8 @@ static size_t read_extension(git_index *index, const char *buffer, size_t buffer total_size = dest.extension_size + sizeof(struct index_extension); - if (buffer_size - total_size < INDEX_FOOTER_SIZE) + if (buffer_size < total_size || + buffer_size - total_size < INDEX_FOOTER_SIZE) return 0; /* optional extension */ @@ -1330,6 +1584,9 @@ static size_t read_extension(git_index *index, const char *buffer, size_t buffer } else if (memcmp(dest.signature, INDEX_EXT_UNMERGED_SIG, 4) == 0) { if (read_reuc(index, buffer + 8, dest.extension_size) < 0) return 0; + } else if (memcmp(dest.signature, INDEX_EXT_CONFLICT_NAME_SIG, 4) == 0) { + if (read_conflict_names(index, buffer + 8, dest.extension_size) < 0) + return 0; } /* else, unsupported extension. We cannot parse this, but we can skip * it by returning `total_size */ @@ -1345,7 +1602,7 @@ static size_t read_extension(git_index *index, const char *buffer, size_t buffer static int parse_index(git_index *index, const char *buffer, size_t buffer_size) { unsigned int i; - struct index_header header; + struct index_header header = { 0 }; git_oid checksum_calculated, checksum_expected; #define seek_forward(_increase) { \ @@ -1401,7 +1658,7 @@ static int parse_index(git_index *index, const char *buffer, size_t buffer_size) /* see if we have read any bytes from the extension */ if (extension_size == 0) - return index_error_invalid("extension size is zero"); + return index_error_invalid("extension is truncated"); seek_forward(extension_size); } @@ -1412,7 +1669,7 @@ static int parse_index(git_index *index, const char *buffer, size_t buffer_size) /* 160-bit SHA-1 over the content of the index file before this checksum. */ git_oid_fromraw(&checksum_expected, (const unsigned char *)buffer); - if (git_oid_cmp(&checksum_calculated, &checksum_expected) != 0) + if (git_oid__cmp(&checksum_calculated, &checksum_expected) != 0) return index_error_invalid("calculated checksum does not match expected"); #undef seek_forward @@ -1543,6 +1800,61 @@ static int write_extension(git_filebuf *file, struct index_extension *header, gi return error; } +static int create_name_extension_data(git_buf *name_buf, git_index_name_entry *conflict_name) +{ + int error = 0; + + if (conflict_name->ancestor == NULL) + error = git_buf_put(name_buf, "\0", 1); + else + error = git_buf_put(name_buf, conflict_name->ancestor, strlen(conflict_name->ancestor) + 1); + + if (error != 0) + goto on_error; + + if (conflict_name->ours == NULL) + error = git_buf_put(name_buf, "\0", 1); + else + error = git_buf_put(name_buf, conflict_name->ours, strlen(conflict_name->ours) + 1); + + if (error != 0) + goto on_error; + + if (conflict_name->theirs == NULL) + error = git_buf_put(name_buf, "\0", 1); + else + error = git_buf_put(name_buf, conflict_name->theirs, strlen(conflict_name->theirs) + 1); + +on_error: + return error; +} + +static int write_name_extension(git_index *index, git_filebuf *file) +{ + git_buf name_buf = GIT_BUF_INIT; + git_vector *out = &index->names; + git_index_name_entry *conflict_name; + struct index_extension extension; + size_t i; + int error = 0; + + git_vector_foreach(out, i, conflict_name) { + if ((error = create_name_extension_data(&name_buf, conflict_name)) < 0) + goto done; + } + + memset(&extension, 0x0, sizeof(struct index_extension)); + memcpy(&extension.signature, INDEX_EXT_CONFLICT_NAME_SIG, 4); + extension.extension_size = (uint32_t)name_buf.size; + + error = write_extension(file, &extension, &name_buf); + + git_buf_free(&name_buf); + +done: + return error; +} + static int create_reuc_extension_data(git_buf *reuc_buf, git_index_reuc_entry *reuc) { int i; @@ -1613,6 +1925,10 @@ static int write_index(git_index *index, git_filebuf *file) /* TODO: write tree cache extension */ + /* write the rename conflict extension */ + if (index->names.length > 0 && write_name_extension(index, file) < 0) + return -1; + /* write the reuc extension */ if (index->reuc.length > 0 && write_reuc_extension(index, file) < 0) return -1; @@ -1626,18 +1942,20 @@ static int write_index(git_index *index, git_filebuf *file) int git_index_entry_stage(const git_index_entry *entry) { - return index_entry_stage(entry); + return GIT_IDXENTRY_STAGE(entry); } typedef struct read_tree_data { - git_index *index; - git_transfer_progress *stats; + git_vector *old_entries; + git_vector *new_entries; + git_vector_cmp entries_search; } read_tree_data; -static int read_tree_cb(const char *root, const git_tree_entry *tentry, void *data) +static int read_tree_cb( + const char *root, const git_tree_entry *tentry, void *payload) { - git_index *index = (git_index *)data; - git_index_entry *entry = NULL; + read_tree_data *data = payload; + git_index_entry *entry = NULL, *old_entry; git_buf path = GIT_BUF_INIT; if (git_tree_entry__is_tree(tentry)) @@ -1652,6 +1970,25 @@ static int read_tree_cb(const char *root, const git_tree_entry *tentry, void *da entry->mode = tentry->attr; entry->oid = tentry->oid; + /* look for corresponding old entry and copy data to new entry */ + if (data->old_entries) { + size_t pos; + struct entry_srch_key skey; + + skey.path = path.ptr; + skey.stage = 0; + + if (!git_vector_bsearch2( + &pos, data->old_entries, data->entries_search, &skey) && + (old_entry = git_vector_get(data->old_entries, pos)) != NULL && + entry->mode == old_entry->mode && + git_oid_equal(&entry->oid, &old_entry->oid)) + { + memcpy(entry, old_entry, sizeof(*entry)); + entry->flags_extended = 0; + } + } + if (path.size < GIT_IDXENTRY_NAMEMASK) entry->flags = path.size & GIT_IDXENTRY_NAMEMASK; else @@ -1660,7 +1997,7 @@ static int read_tree_cb(const char *root, const git_tree_entry *tentry, void *da entry->path = git_buf_detach(&path); git_buf_free(&path); - if (git_vector_insert(&index->entries, entry) < 0) { + if (git_vector_insert(data->new_entries, entry) < 0) { index_entry_free(entry); return -1; } @@ -1670,12 +2007,247 @@ static int read_tree_cb(const char *root, const git_tree_entry *tentry, void *da int git_index_read_tree(git_index *index, const git_tree *tree) { + int error = 0; + git_vector entries = GIT_VECTOR_INIT; + read_tree_data data; + + git_vector_set_cmp(&entries, index->entries._cmp); /* match sort */ + + data.old_entries = &index->entries; + data.new_entries = &entries; + data.entries_search = index->entries_search; + + git_vector_sort(&index->entries); + + error = git_tree_walk(tree, GIT_TREEWALK_POST, read_tree_cb, &data); + + git_vector_sort(&entries); + git_index_clear(index); - return git_tree_walk(tree, GIT_TREEWALK_POST, read_tree_cb, index); + git_vector_swap(&entries, &index->entries); + git_vector_free(&entries); + + return error; } git_repository *git_index_owner(const git_index *index) { return INDEX_OWNER(index); } + +int git_index_add_all( + git_index *index, + const git_strarray *paths, + unsigned int flags, + git_index_matched_path_cb cb, + void *payload) +{ + int error; + git_repository *repo; + git_iterator *wditer = NULL; + const git_index_entry *wd = NULL; + git_index_entry *entry; + git_pathspec ps; + const char *match; + size_t existing; + bool no_fnmatch = (flags & GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH) != 0; + int ignorecase; + git_oid blobid; + + assert(index); + + if (INDEX_OWNER(index) == NULL) + return create_index_error(-1, + "Could not add paths to index. " + "Index is not backed up by an existing repository."); + + repo = INDEX_OWNER(index); + if ((error = git_repository__ensure_not_bare(repo, "index add all")) < 0) + return error; + + if (git_repository__cvar(&ignorecase, repo, GIT_CVAR_IGNORECASE) < 0) + return -1; + + if ((error = git_pathspec__init(&ps, paths)) < 0) + return error; + + /* optionally check that pathspec doesn't mention any ignored files */ + if ((flags & GIT_INDEX_ADD_CHECK_PATHSPEC) != 0 && + (flags & GIT_INDEX_ADD_FORCE) == 0 && + (error = git_ignore__check_pathspec_for_exact_ignores( + repo, &ps.pathspec, no_fnmatch)) < 0) + goto cleanup; + + if ((error = git_iterator_for_workdir( + &wditer, repo, 0, ps.prefix, ps.prefix)) < 0) + goto cleanup; + + while (!(error = git_iterator_advance(&wd, wditer))) { + + /* check if path actually matches */ + 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) && + git_index__find(&existing, index, wd->path, 0) < 0) + continue; + + /* issue notification callback if requested */ + if (cb && (error = cb(wd->path, match, payload)) != 0) { + if (error > 0) /* return > 0 means skip this one */ + continue; + if (error < 0) { /* return < 0 means abort */ + giterr_clear(); + error = GIT_EUSER; + break; + } + } + + /* TODO: Should we check if the file on disk is already an exact + * match to the file in the index and skip this work if it is? + */ + + /* write the blob to disk and get the oid */ + if ((error = git_blob_create_fromworkdir(&blobid, repo, wd->path)) < 0) + break; + + /* make the new entry to insert */ + if ((entry = index_entry_dup(wd)) == NULL) { + error = -1; + break; + } + entry->oid = blobid; + + /* add working directory item to index */ + if ((error = index_insert(index, entry, 1)) < 0) { + index_entry_free(entry); + break; + } + + git_tree_cache_invalidate_path(index->tree, wd->path); + + /* add implies conflict resolved, move conflict entries to REUC */ + if ((error = index_conflict_to_reuc(index, wd->path)) < 0) { + if (error != GIT_ENOTFOUND) + break; + giterr_clear(); + } + } + + if (error == GIT_ITEROVER) + error = 0; + +cleanup: + git_iterator_free(wditer); + git_pathspec__clear(&ps); + + return error; +} + +enum { + INDEX_ACTION_NONE = 0, + INDEX_ACTION_UPDATE = 1, + INDEX_ACTION_REMOVE = 2, +}; + +static int index_apply_to_all( + git_index *index, + int action, + const git_strarray *paths, + git_index_matched_path_cb cb, + void *payload) +{ + int error = 0; + size_t i; + git_pathspec ps; + const char *match; + git_buf path = GIT_BUF_INIT; + + assert(index); + + if ((error = git_pathspec__init(&ps, paths)) < 0) + return error; + + git_vector_sort(&index->entries); + + for (i = 0; !error && i < index->entries.length; ++i) { + git_index_entry *entry = git_vector_get(&index->entries, i); + + /* check if path actually matches */ + if (!git_pathspec__match( + &ps.pathspec, entry->path, false, index->ignore_case, + &match, NULL)) + continue; + + /* issue notification callback if requested */ + if (cb && (error = cb(entry->path, match, payload)) != 0) { + if (error > 0) { /* return > 0 means skip this one */ + error = 0; + continue; + } + if (error < 0) { /* return < 0 means abort */ + giterr_clear(); + error = GIT_EUSER; + break; + } + } + + /* index manipulation may alter entry, so don't depend on it */ + if ((error = git_buf_sets(&path, entry->path)) < 0) + break; + + switch (action) { + case INDEX_ACTION_NONE: + break; + case INDEX_ACTION_UPDATE: + error = git_index_add_bypath(index, path.ptr); + + if (error == GIT_ENOTFOUND) { + giterr_clear(); + + error = git_index_remove_bypath(index, path.ptr); + + if (!error) /* back up foreach if we removed this */ + i--; + } + break; + case INDEX_ACTION_REMOVE: + if (!(error = git_index_remove_bypath(index, path.ptr))) + i--; /* back up foreach if we removed this */ + break; + default: + giterr_set(GITERR_INVALID, "Unknown index action %d", action); + error = -1; + break; + } + } + + git_buf_free(&path); + git_pathspec__clear(&ps); + + return error; +} + +int git_index_remove_all( + git_index *index, + const git_strarray *pathspec, + git_index_matched_path_cb cb, + void *payload) +{ + return index_apply_to_all( + index, INDEX_ACTION_REMOVE, pathspec, cb, payload); +} + +int git_index_update_all( + git_index *index, + const git_strarray *pathspec, + git_index_matched_path_cb cb, + void *payload) +{ + return index_apply_to_all( + index, INDEX_ACTION_UPDATE, pathspec, cb, payload); +} diff --git a/src/index.h b/src/index.h index 9498907b6..40577e105 100644 --- a/src/index.h +++ b/src/index.h @@ -33,6 +33,7 @@ struct git_index { git_tree_cache *tree; + git_vector names; git_vector reuc; git_vector_cmp entries_cmp_path; @@ -41,13 +42,22 @@ struct git_index { git_vector_cmp reuc_search; }; -extern void git_index_entry__init_from_stat(git_index_entry *entry, struct stat *st); +struct git_index_conflict_iterator { + git_index *index; + size_t cur; +}; + +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/indexer.c b/src/indexer.c index 2cfbd3a5a..09f962934 100644 --- a/src/indexer.c +++ b/src/indexer.c @@ -9,7 +9,6 @@ #include "git2/indexer.h" #include "git2/object.h" -#include "git2/oid.h" #include "common.h" #include "pack.h" @@ -17,6 +16,7 @@ #include "posix.h" #include "pack.h" #include "filebuf.h" +#include "oid.h" #include "oidmap.h" #define UINT31_MAX (0x7FFFFFFF) @@ -60,36 +60,19 @@ const git_oid *git_indexer_stream_hash(const git_indexer_stream *idx) static int open_pack(struct git_pack_file **out, const char *filename) { - size_t namelen; struct git_pack_file *pack; - struct stat st; - int fd; - namelen = strlen(filename); - pack = git__calloc(1, sizeof(struct git_pack_file) + namelen + 1); - GITERR_CHECK_ALLOC(pack); - - memcpy(pack->pack_name, filename, namelen + 1); - - if (p_stat(filename, &st) < 0) { - giterr_set(GITERR_OS, "Failed to stat packfile."); - goto cleanup; - } + if (git_packfile_alloc(&pack, filename) < 0) + return -1; - if ((fd = p_open(pack->pack_name, O_RDONLY)) < 0) { + if ((pack->mwf.fd = p_open(pack->pack_name, O_RDONLY)) < 0) { giterr_set(GITERR_OS, "Failed to open packfile."); - goto cleanup; + git_packfile_free(pack); + return -1; } - pack->mwf.fd = fd; - pack->mwf.size = (git_off_t)st.st_size; - *out = pack; return 0; - -cleanup: - git__free(pack); - return -1; } static int parse_header(struct git_pack_header *hdr, struct git_pack_file *pack) @@ -120,7 +103,7 @@ static int objects_cmp(const void *a, const void *b) const struct entry *entrya = a; const struct entry *entryb = b; - return git_oid_cmp(&entrya->oid, &entryb->oid); + return git_oid__cmp(&entrya->oid, &entryb->oid); } int git_indexer_stream_new( @@ -276,7 +259,7 @@ static int store_object(git_indexer_stream *idx) entry = git__calloc(1, sizeof(*entry)); GITERR_CHECK_ALLOC(entry); - pentry = git__malloc(sizeof(struct git_pack_entry)); + pentry = git__calloc(1, sizeof(struct git_pack_entry)); GITERR_CHECK_ALLOC(pentry); git_hash_final(&oid, ctx); @@ -342,10 +325,10 @@ static int hash_and_save(git_indexer_stream *idx, git_rawobj *obj, git_off_t ent /* FIXME: Parse the object instead of hashing it */ if (git_odb__hashobj(&oid, obj) < 0) { giterr_set(GITERR_INDEXER, "Failed to hash object"); - return -1; + goto on_error; } - pentry = git__malloc(sizeof(struct git_pack_entry)); + pentry = git__calloc(1, sizeof(struct git_pack_entry)); GITERR_CHECK_ALLOC(pentry); git_oid_cpy(&pentry->sha1, &oid); @@ -391,7 +374,7 @@ int git_indexer_stream_add(git_indexer_stream *idx, const void *data, size_t siz { int error = -1; struct git_pack_header hdr; - size_t processed; + size_t processed; git_mwindow_file *mwf = &idx->pack->mwf; assert(idx && data && stats); @@ -404,7 +387,6 @@ int git_indexer_stream_add(git_indexer_stream *idx, const void *data, size_t siz /* Make sure we set the new size of the pack */ if (idx->opened_pack) { idx->pack->mwf.size += size; - //printf("\nadding %zu for %zu\n", size, idx->pack->mwf.size); } else { if (open_pack(&idx->pack, idx->pack_file.path_lock) < 0) return -1; @@ -620,7 +602,7 @@ int git_indexer_stream_finalize(git_indexer_stream *idx, git_transfer_progress * git_vector_sort(&idx->objects); git_buf_sets(&filename, idx->pack->pack_name); - git_buf_truncate(&filename, filename.size - strlen("pack")); + git_buf_shorten(&filename, strlen("pack")); git_buf_puts(&filename, "idx"); if (git_buf_oom(&filename)) return -1; diff --git a/src/iterator.c b/src/iterator.c index 5b5ed9525..5917f63fd 100644 --- a/src/iterator.c +++ b/src/iterator.c @@ -7,6 +7,7 @@ #include "iterator.h" #include "tree.h" +#include "index.h" #include "ignore.h" #include "buffer.h" #include "git2/submodule.h" @@ -26,8 +27,6 @@ (GIT_ITERATOR_IGNORE_CASE | GIT_ITERATOR_DONT_IGNORE_CASE) #define ITERATOR_BASE_INIT(P,NAME_LC,NAME_UC,REPO) do { \ - (P) = git__calloc(1, sizeof(NAME_LC ## _iterator)); \ - GITERR_CHECK_ALLOC(P); \ (P)->base.type = GIT_ITERATOR_TYPE_ ## NAME_UC; \ (P)->base.cb = &(P)->cb; \ ITERATOR_SET_CB(P,NAME_LC); \ @@ -48,6 +47,9 @@ #define iterator__dont_autoexpand(I) iterator__flag(I,DONT_AUTOEXPAND) #define iterator__do_autoexpand(I) !iterator__flag(I,DONT_AUTOEXPAND) +#define GIT_ITERATOR_FIRST_ACCESS (1 << 15) +#define iterator__has_been_accessed(I) iterator__flag(I,FIRST_ACCESS) + #define iterator__end(I) ((git_iterator *)(I))->end #define iterator__past_end(I,PATH) \ (iterator__end(I) && ((git_iterator *)(I))->prefixcomp((PATH),iterator__end(I)) > 0) @@ -70,6 +72,8 @@ static int iterator__reset_range( GITERR_CHECK_ALLOC(iter->end); } + iter->flags &= ~GIT_ITERATOR_FIRST_ACCESS; + return 0; } @@ -111,7 +115,7 @@ static int empty_iterator__noop(const git_index_entry **e, git_iterator *i) { GIT_UNUSED(i); iterator__clear_entry(e); - return 0; + return GIT_ITEROVER; } static int empty_iterator__seek(git_iterator *i, const char *p) @@ -148,7 +152,8 @@ int git_iterator_for_nothing( const char *start, const char *end) { - empty_iterator *i; + empty_iterator *i = git__calloc(1, sizeof(empty_iterator)); + GITERR_CHECK_ALLOC(i); #define empty_iterator__current empty_iterator__noop #define empty_iterator__advance empty_iterator__noop @@ -194,6 +199,7 @@ typedef struct { git_buf path; int path_ambiguities; bool path_has_filename; + bool entry_is_current; int (*strncomp)(const char *a, const char *b, size_t sz); } tree_iterator; @@ -267,9 +273,28 @@ static int tree_iterator__search_cmp(const void *key, const void *val, void *p) ((tree_iterator *)p)->strncomp); } +static bool tree_iterator__move_to_next( + tree_iterator *ti, tree_iterator_frame *tf) +{ + if (tf->next > tf->current + 1) + ti->path_ambiguities--; + + if (!tf->up) { /* at root */ + tf->current = tf->next; + return false; + } + + for (; tf->current < tf->next; tf->current++) { + git_tree_free(tf->entries[tf->current]->tree); + tf->entries[tf->current]->tree = NULL; + } + + return (tf->current < tf->n_entries); +} + static int tree_iterator__set_next(tree_iterator *ti, tree_iterator_frame *tf) { - int error; + int error = 0; const git_tree_entry *te, *last = NULL; tf->next = tf->current; @@ -280,18 +305,23 @@ static int tree_iterator__set_next(tree_iterator *ti, tree_iterator_frame *tf) if (last && tree_iterator__te_cmp(last, te, ti->strncomp)) break; - /* load trees for items in [current,next) range */ - if (git_tree_entry__is_tree(te) && - (error = git_tree_lookup( - &tf->entries[tf->next]->tree, ti->base.repo, &te->oid)) < 0) - return error; + /* try to load trees for items in [current,next) range */ + if (!error && git_tree_entry__is_tree(te)) + error = git_tree_lookup( + &tf->entries[tf->next]->tree, ti->base.repo, &te->oid); } if (tf->next > tf->current + 1) ti->path_ambiguities++; + /* if a tree lookup failed, advance over this span and return failure */ + if (error < 0) { + tree_iterator__move_to_next(ti, tf); + return error; + } + if (last && !tree_iterator__current_filename(ti, last)) - return -1; + return -1; /* must have been allocation failure */ return 0; } @@ -309,7 +339,7 @@ static int tree_iterator__push_frame(tree_iterator *ti) size_t i, n_entries = 0; if (head->current >= head->n_entries || !head->entries[head->current]->tree) - return 0; + return GIT_ITEROVER; for (i = head->current; i < head->next; ++i) n_entries += git_tree_entrycount(head->entries[i]->tree); @@ -360,7 +390,7 @@ static int tree_iterator__push_frame(tree_iterator *ti) } } - ti->path_has_filename = false; + ti->path_has_filename = ti->entry_is_current = false; if ((error = tree_iterator__set_next(ti, tf)) < 0) return error; @@ -372,25 +402,6 @@ static int tree_iterator__push_frame(tree_iterator *ti) return 0; } -static bool tree_iterator__move_to_next( - tree_iterator *ti, tree_iterator_frame *tf) -{ - if (tf->next > tf->current + 1) - ti->path_ambiguities--; - - if (!tf->up) { /* at root */ - tf->current = tf->next; - return false; - } - - for (; tf->current < tf->next; tf->current++) { - git_tree_free(tf->entries[tf->current]->tree); - tf->entries[tf->current]->tree = NULL; - } - - return (tf->current < tf->n_entries); -} - static bool tree_iterator__pop_frame(tree_iterator *ti, bool final) { tree_iterator_frame *tf = ti->head; @@ -413,7 +424,7 @@ static bool tree_iterator__pop_frame(tree_iterator *ti, bool final) return true; } -static int tree_iterator__pop_all(tree_iterator *ti, bool to_end, bool final) +static void tree_iterator__pop_all(tree_iterator *ti, bool to_end, bool final) { while (tree_iterator__pop_frame(ti, final)) /* pop to root */; @@ -422,22 +433,18 @@ static int tree_iterator__pop_all(tree_iterator *ti, bool to_end, bool final) ti->path_ambiguities = 0; git_buf_clear(&ti->path); } - - return 0; } -static int tree_iterator__current( - const git_index_entry **entry, git_iterator *self) +static int tree_iterator__update_entry(tree_iterator *ti) { - tree_iterator *ti = (tree_iterator *)self; - tree_iterator_frame *tf = ti->head; - const git_tree_entry *te; + tree_iterator_frame *tf; + const git_tree_entry *te; - iterator__clear_entry(entry); + if (ti->entry_is_current) + return 0; - if (tf->current >= tf->n_entries) - return 0; - te = tf->entries[tf->current]->te; + tf = ti->head; + te = tf->entries[tf->current]->te; ti->entry.mode = te->attr; git_oid_cpy(&ti->entry.oid, &te->oid); @@ -448,12 +455,36 @@ static int tree_iterator__current( if (ti->path_ambiguities > 0) tree_iterator__rewrite_filename(ti); - if (iterator__past_end(ti, ti->entry.path)) - return tree_iterator__pop_all(ti, true, false); + if (iterator__past_end(ti, ti->entry.path)) { + tree_iterator__pop_all(ti, true, false); + return GIT_ITEROVER; + } + + ti->entry_is_current = true; + + return 0; +} + +static int tree_iterator__current( + const git_index_entry **entry, git_iterator *self) +{ + int error; + tree_iterator *ti = (tree_iterator *)self; + tree_iterator_frame *tf = ti->head; + + iterator__clear_entry(entry); + + if (tf->current >= tf->n_entries) + return GIT_ITEROVER; + + if ((error = tree_iterator__update_entry(ti)) < 0) + return error; if (entry) *entry = &ti->entry; + ti->base.flags |= GIT_ITERATOR_FIRST_ACCESS; + return 0; } @@ -465,8 +496,10 @@ static int tree_iterator__advance_into( iterator__clear_entry(entry); - if (tree_iterator__at_tree(ti) && - !(error = tree_iterator__push_frame(ti))) + if (tree_iterator__at_tree(ti)) + error = tree_iterator__push_frame(ti); + + if (!error && entry) error = tree_iterator__current(entry, self); return error; @@ -481,8 +514,11 @@ static int tree_iterator__advance( iterator__clear_entry(entry); - if (tf->current > tf->n_entries) - return 0; + if (tf->current >= tf->n_entries) + return GIT_ITEROVER; + + if (!iterator__has_been_accessed(ti)) + return tree_iterator__current(entry, self); if (iterator__do_autoexpand(ti) && iterator__include_trees(ti) && tree_iterator__at_tree(ti)) @@ -490,7 +526,7 @@ static int tree_iterator__advance( if (ti->path_has_filename) { git_buf_rtruncate_at_char(&ti->path, '/'); - ti->path_has_filename = false; + ti->path_has_filename = ti->entry_is_current = false; } /* scan forward and up, advancing in frame or popping frame when done */ @@ -581,6 +617,9 @@ int git_iterator_for_tree( if ((error = git_object_dup((git_object **)&tree, (git_object *)tree)) < 0) return error; + ti = git__calloc(1, sizeof(tree_iterator)); + GITERR_CHECK_ALLOC(ti); + ITERATOR_BASE_INIT(ti, tree, TREE, git_tree_owner(tree)); if ((error = iterator__update_ignore_case((git_iterator *)ti, flags)) < 0) @@ -697,7 +736,9 @@ static int index_iterator__current( if (entry) *entry = ie; - return 0; + ii->base.flags |= GIT_ITERATOR_FIRST_ACCESS; + + return (ie != NULL) ? 0 : GIT_ITEROVER; } static int index_iterator__at_end(git_iterator *self) @@ -713,6 +754,9 @@ static int index_iterator__advance( size_t entrycount = git_index_entrycount(ii->index); const git_index_entry *ie; + if (!iterator__has_been_accessed(ii)) + return index_iterator__current(entry, self); + if (index_iterator__at_tree(ii)) { if (iterator__do_autoexpand(ii)) { ii->partial.ptr[ii->partial_pos] = ii->restore_terminator; @@ -810,7 +854,8 @@ int git_iterator_for_index( const char *start, const char *end) { - index_iterator *ii; + index_iterator *ii = git__calloc(1, sizeof(index_iterator)); + GITERR_CHECK_ALLOC(ii); ITERATOR_BASE_INIT(ii, index, INDEX, git_index_owner(index)); @@ -833,237 +878,240 @@ int git_iterator_for_index( } -#define WORKDIR_MAX_DEPTH 100 - -typedef struct workdir_iterator_frame workdir_iterator_frame; -struct workdir_iterator_frame { - workdir_iterator_frame *next; +typedef struct fs_iterator_frame fs_iterator_frame; +struct fs_iterator_frame { + fs_iterator_frame *next; git_vector entries; size_t index; }; -typedef struct { +typedef struct fs_iterator fs_iterator; +struct fs_iterator { git_iterator base; git_iterator_callbacks cb; - workdir_iterator_frame *stack; - git_ignores ignores; + fs_iterator_frame *stack; git_index_entry entry; git_buf path; size_t root_len; - int is_ignored; int depth; -} workdir_iterator; -GIT_INLINE(bool) path_is_dotgit(const git_path_with_stat *ps) -{ - if (!ps) - return false; - else { - const char *path = ps->path; - size_t len = ps->path_len; - - if (len < 4) - return false; - if (path[len - 1] == '/') - len--; - if (tolower(path[len - 1]) != 't' || - tolower(path[len - 2]) != 'i' || - tolower(path[len - 3]) != 'g' || - tolower(path[len - 4]) != '.') - return false; - return (len == 4 || path[len - 5] == '/'); - } -} + int (*enter_dir_cb)(fs_iterator *self); + int (*leave_dir_cb)(fs_iterator *self); + int (*update_entry_cb)(fs_iterator *self); +}; + +#define FS_MAX_DEPTH 100 -static workdir_iterator_frame *workdir_iterator__alloc_frame( - workdir_iterator *wi) +static fs_iterator_frame *fs_iterator__alloc_frame(fs_iterator *fi) { - workdir_iterator_frame *wf = git__calloc(1, sizeof(workdir_iterator_frame)); + fs_iterator_frame *ff = git__calloc(1, sizeof(fs_iterator_frame)); git_vector_cmp entry_compare = CASESELECT( - iterator__ignore_case(wi), + iterator__ignore_case(fi), git_path_with_stat_cmp_icase, git_path_with_stat_cmp); - if (wf == NULL) - return NULL; - - if (git_vector_init(&wf->entries, 0, entry_compare) != 0) { - git__free(wf); - return NULL; + if (ff && git_vector_init(&ff->entries, 0, entry_compare) < 0) { + git__free(ff); + ff = NULL; } - return wf; + return ff; } -static void workdir_iterator__free_frame(workdir_iterator_frame *wf) +static void fs_iterator__free_frame(fs_iterator_frame *ff) { - unsigned int i; + size_t i; git_path_with_stat *path; - git_vector_foreach(&wf->entries, i, path) + git_vector_foreach(&ff->entries, i, path) git__free(path); - git_vector_free(&wf->entries); - git__free(wf); + git_vector_free(&ff->entries); + git__free(ff); +} + +static void fs_iterator__pop_frame( + fs_iterator *fi, fs_iterator_frame *ff, bool pop_last) +{ + if (fi && fi->stack == ff) { + if (!ff->next && !pop_last) { + memset(&fi->entry, 0, sizeof(fi->entry)); + return; + } + + if (fi->leave_dir_cb) + (void)fi->leave_dir_cb(fi); + + fi->stack = ff->next; + fi->depth--; + } + + fs_iterator__free_frame(ff); } -static int workdir_iterator__update_entry(workdir_iterator *wi); +static int fs_iterator__update_entry(fs_iterator *fi); +static int fs_iterator__advance_over( + const git_index_entry **entry, git_iterator *self); -static int workdir_iterator__entry_cmp(const void *i, const void *item) +static int fs_iterator__entry_cmp(const void *i, const void *item) { - const workdir_iterator *wi = (const workdir_iterator *)i; + const fs_iterator *fi = (const fs_iterator *)i; const git_path_with_stat *ps = item; - return wi->base.prefixcomp(wi->base.start, ps->path); + return fi->base.prefixcomp(fi->base.start, ps->path); } -static void workdir_iterator__seek_frame_start( - workdir_iterator *wi, workdir_iterator_frame *wf) +static void fs_iterator__seek_frame_start( + fs_iterator *fi, fs_iterator_frame *ff) { - if (!wf) + if (!ff) return; - if (wi->base.start) + if (fi->base.start) git_vector_bsearch2( - &wf->index, &wf->entries, workdir_iterator__entry_cmp, wi); + &ff->index, &ff->entries, fs_iterator__entry_cmp, fi); else - wf->index = 0; - - if (path_is_dotgit(git_vector_get(&wf->entries, wf->index))) - wf->index++; + ff->index = 0; } -static int workdir_iterator__expand_dir(workdir_iterator *wi) +static int fs_iterator__expand_dir(fs_iterator *fi) { int error; - workdir_iterator_frame *wf; + fs_iterator_frame *ff; + + if (fi->depth > FS_MAX_DEPTH) { + giterr_set(GITERR_REPOSITORY, + "Directory nesting is too deep (%d)", fi->depth); + return -1; + } - wf = workdir_iterator__alloc_frame(wi); - GITERR_CHECK_ALLOC(wf); + ff = fs_iterator__alloc_frame(fi); + GITERR_CHECK_ALLOC(ff); error = git_path_dirload_with_stat( - wi->path.ptr, wi->root_len, iterator__ignore_case(wi), - wi->base.start, wi->base.end, &wf->entries); + fi->path.ptr, fi->root_len, iterator__ignore_case(fi), + fi->base.start, fi->base.end, &ff->entries); - if (error < 0 || wf->entries.length == 0) { - workdir_iterator__free_frame(wf); - return GIT_ENOTFOUND; + if (error < 0) { + fs_iterator__free_frame(ff); + fs_iterator__advance_over(NULL, (git_iterator *)fi); + return error; } - if (++(wi->depth) > WORKDIR_MAX_DEPTH) { - giterr_set(GITERR_REPOSITORY, - "Working directory is too deep (%d)", wi->depth); - workdir_iterator__free_frame(wf); - return -1; + if (ff->entries.length == 0) { + fs_iterator__free_frame(ff); + return GIT_ENOTFOUND; } - workdir_iterator__seek_frame_start(wi, wf); + fs_iterator__seek_frame_start(fi, ff); - /* only push new ignores if this is not top level directory */ - if (wi->stack != NULL) { - ssize_t slash_pos = git_buf_rfind_next(&wi->path, '/'); - (void)git_ignore__push_dir(&wi->ignores, &wi->path.ptr[slash_pos + 1]); - } + ff->next = fi->stack; + fi->stack = ff; + fi->depth++; - wf->next = wi->stack; - wi->stack = wf; + if (fi->enter_dir_cb && (error = fi->enter_dir_cb(fi)) < 0) + return error; - return workdir_iterator__update_entry(wi); + return fs_iterator__update_entry(fi); } -static int workdir_iterator__current( +static int fs_iterator__current( const git_index_entry **entry, git_iterator *self) { - workdir_iterator *wi = (workdir_iterator *)self; + fs_iterator *fi = (fs_iterator *)self; + const git_index_entry *fe = (fi->entry.path == NULL) ? NULL : &fi->entry; + if (entry) - *entry = (wi->entry.path == NULL) ? NULL : &wi->entry; - return 0; + *entry = fe; + + fi->base.flags |= GIT_ITERATOR_FIRST_ACCESS; + + return (fe != NULL) ? 0 : GIT_ITEROVER; } -static int workdir_iterator__at_end(git_iterator *self) +static int fs_iterator__at_end(git_iterator *self) { - return (((workdir_iterator *)self)->entry.path == NULL); + return (((fs_iterator *)self)->entry.path == NULL); } -static int workdir_iterator__advance_into( +static int fs_iterator__advance_into( const git_index_entry **entry, git_iterator *iter) { int error = 0; - workdir_iterator *wi = (workdir_iterator *)iter; + fs_iterator *fi = (fs_iterator *)iter; iterator__clear_entry(entry); - /* workdir iterator will allow you to explicitly advance into a - * commit/submodule (as well as a tree) to avoid some cases where an - * entry is mislabeled as a submodule in the working directory + /* Allow you to explicitly advance into a commit/submodule (as well as a + * tree) to avoid cases where an entry is mislabeled as a submodule in + * the working directory. The fs iterator will never have COMMMIT + * entries on it's own, but a wrapper might add them. */ - if (wi->entry.path != NULL && - (wi->entry.mode == GIT_FILEMODE_TREE || - wi->entry.mode == GIT_FILEMODE_COMMIT)) + if (fi->entry.path != NULL && + (fi->entry.mode == GIT_FILEMODE_TREE || + fi->entry.mode == GIT_FILEMODE_COMMIT)) /* returns GIT_ENOTFOUND if the directory is empty */ - error = workdir_iterator__expand_dir(wi); + error = fs_iterator__expand_dir(fi); if (!error && entry) - error = workdir_iterator__current(entry, iter); + error = fs_iterator__current(entry, iter); + + if (!error && !fi->entry.path) + error = GIT_ITEROVER; return error; } -static int workdir_iterator__advance( +static int fs_iterator__advance_over( const git_index_entry **entry, git_iterator *self) { int error = 0; - workdir_iterator *wi = (workdir_iterator *)self; - workdir_iterator_frame *wf; + fs_iterator *fi = (fs_iterator *)self; + fs_iterator_frame *ff; git_path_with_stat *next; - /* given include_trees & autoexpand, we might have to go into a tree */ - if (iterator__do_autoexpand(wi) && - wi->entry.path != NULL && - wi->entry.mode == GIT_FILEMODE_TREE) - { - error = workdir_iterator__advance_into(entry, self); - - /* continue silently past empty directories if autoexpanding */ - if (error != GIT_ENOTFOUND) - return error; - giterr_clear(); - error = 0; - } - if (entry != NULL) *entry = NULL; - while (wi->entry.path != NULL) { - wf = wi->stack; - next = git_vector_get(&wf->entries, ++wf->index); + while (fi->entry.path != NULL) { + ff = fi->stack; + next = git_vector_get(&ff->entries, ++ff->index); - if (next != NULL) { - /* match git's behavior of ignoring anything named ".git" */ - if (path_is_dotgit(next)) - continue; - /* else found a good entry */ + if (next != NULL) break; - } - /* pop stack if anything is left to pop */ - if (!wf->next) { - memset(&wi->entry, 0, sizeof(wi->entry)); - return 0; - } - - wi->stack = wf->next; - wi->depth--; - workdir_iterator__free_frame(wf); - git_ignore__pop_dir(&wi->ignores); + fs_iterator__pop_frame(fi, ff, false); } - error = workdir_iterator__update_entry(wi); + error = fs_iterator__update_entry(fi); if (!error && entry != NULL) - error = workdir_iterator__current(entry, self); + error = fs_iterator__current(entry, self); return error; } -static int workdir_iterator__seek(git_iterator *self, const char *prefix) +static int fs_iterator__advance( + const git_index_entry **entry, git_iterator *self) +{ + fs_iterator *fi = (fs_iterator *)self; + + if (!iterator__has_been_accessed(fi)) + return fs_iterator__current(entry, self); + + /* given include_trees & autoexpand, we might have to go into a tree */ + if (iterator__do_autoexpand(fi) && + fi->entry.path != NULL && + fi->entry.mode == GIT_FILEMODE_TREE) + { + int error = fs_iterator__advance_into(entry, self); + if (error != GIT_ENOTFOUND) + return error; + /* continue silently past empty directories if autoexpanding */ + giterr_clear(); + } + + return fs_iterator__advance_over(entry, self); +} + +static int fs_iterator__seek(git_iterator *self, const char *prefix) { GIT_UNUSED(self); GIT_UNUSED(prefix); @@ -1073,108 +1121,210 @@ static int workdir_iterator__seek(git_iterator *self, const char *prefix) return 0; } -static int workdir_iterator__reset( +static int fs_iterator__reset( git_iterator *self, const char *start, const char *end) { - workdir_iterator *wi = (workdir_iterator *)self; + int error; + fs_iterator *fi = (fs_iterator *)self; - while (wi->stack != NULL && wi->stack->next != NULL) { - workdir_iterator_frame *wf = wi->stack; - wi->stack = wf->next; - workdir_iterator__free_frame(wf); - git_ignore__pop_dir(&wi->ignores); - } - wi->depth = 0; + while (fi->stack != NULL && fi->stack->next != NULL) + fs_iterator__pop_frame(fi, fi->stack, false); + fi->depth = 0; - if (iterator__reset_range(self, start, end) < 0) - return -1; + if ((error = iterator__reset_range(self, start, end)) < 0) + return error; + + fs_iterator__seek_frame_start(fi, fi->stack); - workdir_iterator__seek_frame_start(wi, wi->stack); + error = fs_iterator__update_entry(fi); + if (error == GIT_ITEROVER) + error = 0; - return workdir_iterator__update_entry(wi); + return error; } -static void workdir_iterator__free(git_iterator *self) +static void fs_iterator__free(git_iterator *self) { - workdir_iterator *wi = (workdir_iterator *)self; + fs_iterator *fi = (fs_iterator *)self; - while (wi->stack != NULL) { - workdir_iterator_frame *wf = wi->stack; - wi->stack = wf->next; - workdir_iterator__free_frame(wf); - } + while (fi->stack != NULL) + fs_iterator__pop_frame(fi, fi->stack, true); - git_ignore__free(&wi->ignores); - git_buf_free(&wi->path); + git_buf_free(&fi->path); } -static int workdir_iterator__update_entry(workdir_iterator *wi) +static int fs_iterator__update_entry(fs_iterator *fi) { - int error = 0; - git_path_with_stat *ps = - git_vector_get(&wi->stack->entries, wi->stack->index); + git_path_with_stat *ps; - git_buf_truncate(&wi->path, wi->root_len); - memset(&wi->entry, 0, sizeof(wi->entry)); + memset(&fi->entry, 0, sizeof(fi->entry)); + if (!fi->stack) + return GIT_ITEROVER; + + ps = git_vector_get(&fi->stack->entries, fi->stack->index); if (!ps) - return 0; + return GIT_ITEROVER; - /* skip over .git entries */ - if (path_is_dotgit(ps)) - return workdir_iterator__advance(NULL, (git_iterator *)wi); + git_buf_truncate(&fi->path, fi->root_len); + if (git_buf_put(&fi->path, ps->path, ps->path_len) < 0) + return -1; - if (git_buf_put(&wi->path, ps->path, ps->path_len) < 0) + if (iterator__past_end(fi, fi->path.ptr + fi->root_len)) + return GIT_ITEROVER; + + fi->entry.path = ps->path; + git_index_entry__init_from_stat(&fi->entry, &ps->st); + + /* need different mode here to keep directories during iteration */ + fi->entry.mode = git_futils_canonical_mode(ps->st.st_mode); + + /* allow wrapper to check/update the entry (can force skip) */ + if (fi->update_entry_cb && + fi->update_entry_cb(fi) == GIT_ENOTFOUND) + return fs_iterator__advance_over(NULL, (git_iterator *)fi); + + /* if this is a tree and trees aren't included, then skip */ + if (fi->entry.mode == GIT_FILEMODE_TREE && !iterator__include_trees(fi)) { + int error = fs_iterator__advance_into(NULL, (git_iterator *)fi); + if (error != GIT_ENOTFOUND) + return error; + giterr_clear(); + return fs_iterator__advance_over(NULL, (git_iterator *)fi); + } + + return 0; +} + +static int fs_iterator__initialize( + git_iterator **out, fs_iterator *fi, const char *root) +{ + int error; + + if (git_buf_sets(&fi->path, root) < 0 || git_path_to_dir(&fi->path) < 0) { + git__free(fi); return -1; + } + fi->root_len = fi->path.size; - if (iterator__past_end(wi, wi->path.ptr + wi->root_len)) - return 0; + if ((error = fs_iterator__expand_dir(fi)) < 0) { + if (error == GIT_ENOTFOUND || error == GIT_ITEROVER) { + giterr_clear(); + error = 0; + } else { + git_iterator_free((git_iterator *)fi); + fi = NULL; + } + } - wi->entry.path = ps->path; + *out = (git_iterator *)fi; + return error; +} - wi->is_ignored = -1; +int git_iterator_for_filesystem( + git_iterator **out, + const char *root, + git_iterator_flag_t flags, + const char *start, + const char *end) +{ + fs_iterator *fi = git__calloc(1, sizeof(fs_iterator)); + GITERR_CHECK_ALLOC(fi); - git_index_entry__init_from_stat(&wi->entry, &ps->st); + ITERATOR_BASE_INIT(fi, fs, FS, NULL); - /* need different mode here to keep directories during iteration */ - wi->entry.mode = git_futils_canonical_mode(ps->st.st_mode); + if ((flags & GIT_ITERATOR_IGNORE_CASE) != 0) + fi->base.flags |= GIT_ITERATOR_IGNORE_CASE; - /* if this is a file type we don't handle, treat as ignored */ - if (wi->entry.mode == 0) { - wi->is_ignored = 1; - return 0; + return fs_iterator__initialize(out, fi, root); +} + + +typedef struct { + fs_iterator fi; + git_ignores ignores; + int is_ignored; +} workdir_iterator; + +GIT_INLINE(bool) workdir_path_is_dotgit(const git_buf *path) +{ + size_t len; + + if (!path || (len = path->size) < 4) + return false; + + if (path->ptr[len - 1] == '/') + len--; + + if (tolower(path->ptr[len - 1]) != 't' || + tolower(path->ptr[len - 2]) != 'i' || + tolower(path->ptr[len - 3]) != 'g' || + tolower(path->ptr[len - 4]) != '.') + return false; + + return (len == 4 || path->ptr[len - 5] == '/'); +} + +static int workdir_iterator__enter_dir(fs_iterator *fi) +{ + /* only push new ignores if this is not top level directory */ + if (fi->stack->next != NULL) { + workdir_iterator *wi = (workdir_iterator *)fi; + ssize_t slash_pos = git_buf_rfind_next(&fi->path, '/'); + + (void)git_ignore__push_dir(&wi->ignores, &fi->path.ptr[slash_pos + 1]); } - /* if this isn't a tree, then we're done */ - if (wi->entry.mode != GIT_FILEMODE_TREE) - return 0; + return 0; +} - /* detect submodules */ - error = git_submodule_lookup(NULL, wi->base.repo, wi->entry.path); - if (error == GIT_ENOTFOUND) - giterr_clear(); +static int workdir_iterator__leave_dir(fs_iterator *fi) +{ + workdir_iterator *wi = (workdir_iterator *)fi; + git_ignore__pop_dir(&wi->ignores); + return 0; +} - if (error == GIT_EEXISTS) /* if contains .git, treat as untracked submod */ - error = 0; +static int workdir_iterator__update_entry(fs_iterator *fi) +{ + int error = 0; + workdir_iterator *wi = (workdir_iterator *)fi; - /* if submodule, mark as GITLINK and remove trailing slash */ - if (!error) { - size_t len = strlen(wi->entry.path); - assert(wi->entry.path[len - 1] == '/'); - wi->entry.path[len - 1] = '\0'; - wi->entry.mode = S_IFGITLINK; + /* skip over .git entries */ + if (workdir_path_is_dotgit(&fi->path)) + return GIT_ENOTFOUND; + + /* reset is_ignored since we haven't checked yet */ + wi->is_ignored = -1; + + /* check if apparent tree entries are actually submodules */ + if (fi->entry.mode != GIT_FILEMODE_TREE) return 0; + + error = git_submodule_lookup(NULL, fi->base.repo, fi->entry.path); + if (error < 0) + giterr_clear(); + + /* mark submodule (or any dir with .git) as GITLINK and remove slash */ + if (!error || error == GIT_EEXISTS) { + fi->entry.mode = S_IFGITLINK; + fi->entry.path[strlen(fi->entry.path) - 1] = '\0'; } - if (iterator__include_trees(wi)) - return 0; + return 0; +} - return workdir_iterator__advance(NULL, (git_iterator *)wi); +static void workdir_iterator__free(git_iterator *self) +{ + workdir_iterator *wi = (workdir_iterator *)self; + fs_iterator__free(self); + git_ignore__free(&wi->ignores); } -int git_iterator_for_workdir( - git_iterator **iter, +int git_iterator_for_workdir_ext( + git_iterator **out, git_repository *repo, + const char *repo_workdir, git_iterator_flag_t flags, const char *start, const char *end) @@ -1182,38 +1332,31 @@ int git_iterator_for_workdir( int error; workdir_iterator *wi; - assert(iter && repo); - - if ((error = git_repository__ensure_not_bare( - repo, "scan working directory")) < 0) - return error; + if (!repo_workdir) { + if (git_repository__ensure_not_bare(repo, "scan working directory") < 0) + return GIT_EBAREREPO; + repo_workdir = git_repository_workdir(repo); + } - ITERATOR_BASE_INIT(wi, workdir, WORKDIR, repo); + /* initialize as an fs iterator then do overrides */ + wi = git__calloc(1, sizeof(workdir_iterator)); + GITERR_CHECK_ALLOC(wi); + ITERATOR_BASE_INIT((&wi->fi), fs, FS, repo); - if ((error = iterator__update_ignore_case((git_iterator *)wi, flags)) < 0) - goto fail; + wi->fi.base.type = GIT_ITERATOR_TYPE_WORKDIR; + wi->fi.cb.free = workdir_iterator__free; + wi->fi.enter_dir_cb = workdir_iterator__enter_dir; + wi->fi.leave_dir_cb = workdir_iterator__leave_dir; + wi->fi.update_entry_cb = workdir_iterator__update_entry; - if (git_buf_sets(&wi->path, git_repository_workdir(repo)) < 0 || - git_path_to_dir(&wi->path) < 0 || - git_ignore__for_path(repo, "", &wi->ignores) < 0) + if ((error = iterator__update_ignore_case((git_iterator *)wi, flags)) < 0 || + (error = git_ignore__for_path(repo, "", &wi->ignores)) < 0) { - git__free(wi); - return -1; - } - wi->root_len = wi->path.size; - - if ((error = workdir_iterator__expand_dir(wi)) < 0) { - if (error != GIT_ENOTFOUND) - goto fail; - giterr_clear(); + git_iterator_free((git_iterator *)wi); + return error; } - *iter = (git_iterator *)wi; - return 0; - -fail: - git_iterator_free((git_iterator *)wi); - return error; + return fs_iterator__initialize(out, &wi->fi, repo_workdir); } @@ -1315,7 +1458,8 @@ bool git_iterator_current_is_ignored(git_iterator *iter) if (wi->is_ignored != -1) return (bool)(wi->is_ignored != 0); - if (git_ignore__lookup(&wi->ignores, wi->entry.path, &wi->is_ignored) < 0) + if (git_ignore__lookup( + &wi->ignores, wi->fi.entry.path, &wi->is_ignored) < 0) wi->is_ignored = true; return (bool)wi->is_ignored; @@ -1340,10 +1484,10 @@ int git_iterator_current_workdir_path(git_buf **path, git_iterator *iter) { workdir_iterator *wi = (workdir_iterator *)iter; - if (iter->type != GIT_ITERATOR_TYPE_WORKDIR || !wi->entry.path) + if (iter->type != GIT_ITERATOR_TYPE_WORKDIR || !wi->fi.entry.path) *path = NULL; else - *path = &wi->path; + *path = &wi->fi.path; return 0; } diff --git a/src/iterator.h b/src/iterator.h index 4a4e6a9d8..ea88fa6a2 100644 --- a/src/iterator.h +++ b/src/iterator.h @@ -19,6 +19,7 @@ typedef enum { GIT_ITERATOR_TYPE_TREE = 1, GIT_ITERATOR_TYPE_INDEX = 2, GIT_ITERATOR_TYPE_WORKDIR = 3, + GIT_ITERATOR_TYPE_FS = 4, } git_iterator_type_t; typedef enum { @@ -78,14 +79,35 @@ extern int git_iterator_for_index( const char *start, const char *end); +extern int git_iterator_for_workdir_ext( + git_iterator **out, + git_repository *repo, + const char *repo_workdir, + git_iterator_flag_t flags, + const char *start, + const char *end); + /* workdir iterators will match the ignore_case value from the index of the * repository, unless you override with a non-zero flag value */ -extern int git_iterator_for_workdir( +GIT_INLINE(int) git_iterator_for_workdir( git_iterator **out, git_repository *repo, git_iterator_flag_t flags, const char *start, + const char *end) +{ + return git_iterator_for_workdir_ext(out, repo, NULL, flags, start, end); +} + +/* for filesystem iterators, you have to explicitly pass in the ignore_case + * behavior that you desire + */ +extern int git_iterator_for_filesystem( + git_iterator **out, + const char *root, + git_iterator_flag_t flags, + const char *start, const char *end); extern void git_iterator_free(git_iterator *iter); @@ -131,9 +153,9 @@ GIT_INLINE(int) git_iterator_advance( * * If the current item is not a tree, this is a no-op. * - * For working directory iterators only, a tree (i.e. directory) can be - * empty. In that case, this function returns GIT_ENOTFOUND and does not - * advance. That can't happen for tree and index iterators. + * For filesystem and working directory iterators, a tree (i.e. directory) + * can be empty. In that case, this function returns GIT_ENOTFOUND and + * does not advance. That can't happen for tree and index iterators. */ GIT_INLINE(int) git_iterator_advance_into( const git_index_entry **entry, git_iterator *iter) @@ -141,18 +163,50 @@ GIT_INLINE(int) git_iterator_advance_into( return iter->cb->advance_into(entry, iter); } +/** + * Advance into a tree or skip over it if it is empty. + * + * Because `git_iterator_advance_into` may return GIT_ENOTFOUND if the + * directory is empty (only with filesystem and working directory + * iterators) and a common response is to just call `git_iterator_advance` + * when that happens, this bundles the two into a single simple call. + */ +GIT_INLINE(int) git_iterator_advance_into_or_over( + const git_index_entry **entry, git_iterator *iter) +{ + int error = iter->cb->advance_into(entry, iter); + if (error == GIT_ENOTFOUND) { + giterr_clear(); + error = iter->cb->advance(entry, iter); + } + return error; +} + +/* Seek is currently unimplemented */ GIT_INLINE(int) git_iterator_seek( git_iterator *iter, const char *prefix) { return iter->cb->seek(iter, prefix); } +/** + * Go back to the start of the iteration. + * + * This resets the iterator to the start of the iteration. It also allows + * you to reset the `start` and `end` pathname boundaries of the iteration + * when doing so. + */ GIT_INLINE(int) git_iterator_reset( git_iterator *iter, const char *start, const char *end) { return iter->cb->reset(iter, start, end); } +/** + * Check if the iterator is at the end + * + * @return 0 if not at end, >0 if at end + */ GIT_INLINE(int) git_iterator_at_end(git_iterator *iter) { return iter->cb->at_end(iter); diff --git a/src/merge.c b/src/merge.c index e0010d6a4..2e94ce1cd 100644 --- a/src/merge.c +++ b/src/merge.c @@ -5,48 +5,58 @@ * a Linking Exception. For full terms see the included COPYING file. */ +#include "common.h" +#include "posix.h" +#include "buffer.h" #include "repository.h" #include "revwalk.h" -#include "buffer.h" +#include "commit_list.h" #include "merge.h" +#include "path.h" #include "refs.h" +#include "object.h" +#include "iterator.h" +#include "refs.h" +#include "diff.h" +#include "checkout.h" +#include "tree.h" +#include "merge_file.h" +#include "blob.h" +#include "hashsig.h" +#include "oid.h" +#include "index.h" +#include "filebuf.h" + +#include "git2/types.h" #include "git2/repository.h" +#include "git2/object.h" +#include "git2/commit.h" #include "git2/merge.h" +#include "git2/refs.h" #include "git2/reset.h" -#include "commit_list.h" - -int git_repository_merge_cleanup(git_repository *repo) -{ - int error = 0; - git_buf merge_head_path = GIT_BUF_INIT, - merge_mode_path = GIT_BUF_INIT, - merge_msg_path = GIT_BUF_INIT; - - assert(repo); +#include "git2/checkout.h" +#include "git2/signature.h" +#include "git2/config.h" +#include "git2/tree.h" +#include "git2/sys/index.h" - if (git_buf_joinpath(&merge_head_path, repo->path_repository, GIT_MERGE_HEAD_FILE) < 0 || - git_buf_joinpath(&merge_mode_path, repo->path_repository, GIT_MERGE_MODE_FILE) < 0 || - git_buf_joinpath(&merge_msg_path, repo->path_repository, GIT_MERGE_MSG_FILE) < 0) - return -1; - - if (git_path_isfile(merge_head_path.ptr)) { - if ((error = p_unlink(merge_head_path.ptr)) < 0) - goto cleanup; - } +#define GIT_MERGE_INDEX_ENTRY_EXISTS(X) ((X).mode != 0) - if (git_path_isfile(merge_mode_path.ptr)) - (void)p_unlink(merge_mode_path.ptr); +typedef enum { + TREE_IDX_ANCESTOR = 0, + TREE_IDX_OURS = 1, + TREE_IDX_THEIRS = 2 +} merge_tree_index_t; - if (git_path_isfile(merge_msg_path.ptr)) - (void)p_unlink(merge_msg_path.ptr); +/* Tracks D/F conflicts */ +struct merge_diff_df_data { + const char *df_path; + const char *prev_path; + git_merge_diff *prev_conflict; +}; -cleanup: - git_buf_free(&merge_msg_path); - git_buf_free(&merge_mode_path); - git_buf_free(&merge_head_path); - return error; -} +/* Merge base computation */ int git_merge_base_many(git_oid *out, git_repository *repo, const git_oid input_array[], size_t length) { @@ -86,6 +96,7 @@ int git_merge_base_many(git_oid *out, git_repository *repo, const git_oid input_ goto cleanup; if (!result) { + giterr_set(GITERR_MERGE, "No merge base found"); error = GIT_ENOTFOUND; goto cleanup; } @@ -131,7 +142,7 @@ int git_merge_base(git_oid *out, git_repository *repo, const git_oid *one, const if (!result) { git_revwalk_free(walk); - giterr_clear(); + giterr_set(GITERR_MERGE, "No merge base found"); return GIT_ENOTFOUND; } @@ -177,7 +188,7 @@ int git_merge__bases_many(git_commit_list **out, git_revwalk *walk, git_commit_l return -1; if (git_commit_list_parse(walk, one) < 0) - return -1; + return -1; one->flags |= PARENT1; if (git_pqueue_insert(&list, one) < 0) @@ -294,3 +305,1854 @@ cleanup: return error; } + +GIT_INLINE(int) index_entry_cmp(const git_index_entry *a, const git_index_entry *b) +{ + int value = 0; + + if (a->path == NULL) + return (b->path == NULL) ? 0 : 1; + + if ((value = a->mode - b->mode) == 0 && + (value = git_oid__cmp(&a->oid, &b->oid)) == 0) + value = strcmp(a->path, b->path); + + return value; +} + +/* Conflict resolution */ + +static int merge_conflict_resolve_trivial( + int *resolved, + git_merge_diff_list *diff_list, + const git_merge_diff *conflict) +{ + int ours_empty, theirs_empty; + int ours_changed, theirs_changed, ours_theirs_differ; + git_index_entry const *result = NULL; + int error = 0; + + assert(resolved && diff_list && conflict); + + *resolved = 0; + + if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE || + conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED) + return 0; + + if (conflict->our_status == GIT_DELTA_RENAMED || + conflict->their_status == GIT_DELTA_RENAMED) + return 0; + + ours_empty = !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry); + theirs_empty = !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry); + + ours_changed = (conflict->our_status != GIT_DELTA_UNMODIFIED); + theirs_changed = (conflict->their_status != GIT_DELTA_UNMODIFIED); + ours_theirs_differ = ours_changed && theirs_changed && + index_entry_cmp(&conflict->our_entry, &conflict->their_entry); + + /* + * Note: with only one ancestor, some cases are not distinct: + * + * 16: ancest:anc1/anc2, head:anc1, remote:anc2 = result:no merge + * 3: ancest:(empty)^, head:head, remote:(empty) = result:no merge + * 2: ancest:(empty)^, head:(empty), remote:remote = result:no merge + * + * Note that the two cases that take D/F conflicts into account + * specifically do not need to be explicitly tested, as D/F conflicts + * would fail the *empty* test: + * + * 3ALT: ancest:(empty)+, head:head, remote:*empty* = result:head + * 2ALT: ancest:(empty)+, head:*empty*, remote:remote = result:remote + * + * Note that many of these cases need not be explicitly tested, as + * they simply degrade to "all different" cases (eg, 11): + * + * 4: ancest:(empty)^, head:head, remote:remote = result:no merge + * 7: ancest:ancest+, head:(empty), remote:remote = result:no merge + * 9: ancest:ancest+, head:head, remote:(empty) = result:no merge + * 11: ancest:ancest+, head:head, remote:remote = result:no merge + */ + + /* 5ALT: ancest:*, head:head, remote:head = result:head */ + if (ours_changed && !ours_empty && !ours_theirs_differ) + result = &conflict->our_entry; + /* 6: ancest:ancest+, head:(empty), remote:(empty) = result:no merge */ + else if (ours_changed && ours_empty && theirs_empty) + *resolved = 0; + /* 8: ancest:ancest^, head:(empty), remote:ancest = result:no merge */ + else if (ours_empty && !theirs_changed) + *resolved = 0; + /* 10: ancest:ancest^, head:ancest, remote:(empty) = result:no merge */ + else if (!ours_changed && theirs_empty) + *resolved = 0; + /* 13: ancest:ancest+, head:head, remote:ancest = result:head */ + else if (ours_changed && !theirs_changed) + result = &conflict->our_entry; + /* 14: ancest:ancest+, head:ancest, remote:remote = result:remote */ + else if (!ours_changed && theirs_changed) + result = &conflict->their_entry; + else + *resolved = 0; + + if (result != NULL && + GIT_MERGE_INDEX_ENTRY_EXISTS(*result) && + (error = git_vector_insert(&diff_list->staged, (void *)result)) >= 0) + *resolved = 1; + + /* Note: trivial resolution does not update the REUC. */ + + return error; +} + +static int merge_conflict_resolve_one_removed( + int *resolved, + git_merge_diff_list *diff_list, + const git_merge_diff *conflict) +{ + int ours_empty, theirs_empty; + int ours_changed, theirs_changed; + int error = 0; + + assert(resolved && diff_list && conflict); + + *resolved = 0; + + if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE || + conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED) + return 0; + + ours_empty = !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry); + theirs_empty = !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry); + + ours_changed = (conflict->our_status != GIT_DELTA_UNMODIFIED); + theirs_changed = (conflict->their_status != GIT_DELTA_UNMODIFIED); + + /* Removed in both */ + if (ours_changed && ours_empty && theirs_empty) + *resolved = 1; + /* Removed in ours */ + else if (ours_empty && !theirs_changed) + *resolved = 1; + /* Removed in theirs */ + else if (!ours_changed && theirs_empty) + *resolved = 1; + + if (*resolved) + git_vector_insert(&diff_list->resolved, (git_merge_diff *)conflict); + + return error; +} + + +static int merge_conflict_resolve_one_renamed( + int *resolved, + git_merge_diff_list *diff_list, + const git_merge_diff *conflict) +{ + int ours_renamed, theirs_renamed; + int ours_changed, theirs_changed; + git_index_entry *merged; + int error = 0; + + assert(resolved && diff_list && conflict); + + *resolved = 0; + + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) || + !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry)) + return 0; + + ours_renamed = (conflict->our_status == GIT_DELTA_RENAMED); + theirs_renamed = (conflict->their_status == GIT_DELTA_RENAMED); + + if (!ours_renamed && !theirs_renamed) + return 0; + + /* Reject one file in a 2->1 conflict */ + if (conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1 || + conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_1_TO_2 || + conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED) + return 0; + + ours_changed = (git_oid__cmp(&conflict->ancestor_entry.oid, &conflict->our_entry.oid) != 0); + theirs_changed = (git_oid__cmp(&conflict->ancestor_entry.oid, &conflict->their_entry.oid) != 0); + + /* if both are modified (and not to a common target) require a merge */ + if (ours_changed && theirs_changed && + git_oid__cmp(&conflict->our_entry.oid, &conflict->their_entry.oid) != 0) + return 0; + + if ((merged = git_pool_malloc(&diff_list->pool, sizeof(git_index_entry))) == NULL) + return -1; + + if (ours_changed) + memcpy(merged, &conflict->our_entry, sizeof(git_index_entry)); + else + memcpy(merged, &conflict->their_entry, sizeof(git_index_entry)); + + if (ours_renamed) + merged->path = conflict->our_entry.path; + else + merged->path = conflict->their_entry.path; + + *resolved = 1; + + git_vector_insert(&diff_list->staged, merged); + git_vector_insert(&diff_list->resolved, (git_merge_diff *)conflict); + + return error; +} + +static int merge_conflict_resolve_automerge( + int *resolved, + git_merge_diff_list *diff_list, + const git_merge_diff *conflict, + unsigned int automerge_flags) +{ + git_merge_file_input ancestor = GIT_MERGE_FILE_INPUT_INIT, + ours = GIT_MERGE_FILE_INPUT_INIT, + theirs = GIT_MERGE_FILE_INPUT_INIT; + git_merge_file_result result = GIT_MERGE_FILE_RESULT_INIT; + git_index_entry *index_entry; + git_odb *odb = NULL; + git_oid automerge_oid; + int error = 0; + + assert(resolved && diff_list && conflict); + + *resolved = 0; + + if (automerge_flags == GIT_MERGE_AUTOMERGE_NONE) + return 0; + + /* Reject D/F conflicts */ + if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE) + return 0; + + /* Reject link/file conflicts. */ + if ((S_ISLNK(conflict->ancestor_entry.mode) ^ S_ISLNK(conflict->our_entry.mode)) || + (S_ISLNK(conflict->ancestor_entry.mode) ^ S_ISLNK(conflict->their_entry.mode))) + return 0; + + /* Reject name conflicts */ + if (conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1 || + conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED) + return 0; + + if ((conflict->our_status & GIT_DELTA_RENAMED) == GIT_DELTA_RENAMED && + (conflict->their_status & GIT_DELTA_RENAMED) == GIT_DELTA_RENAMED && + strcmp(conflict->ancestor_entry.path, conflict->their_entry.path) != 0) + return 0; + + if ((error = git_repository_odb(&odb, diff_list->repo)) < 0 || + (error = git_merge_file_input_from_index_entry(&ancestor, diff_list->repo, &conflict->ancestor_entry)) < 0 || + (error = git_merge_file_input_from_index_entry(&ours, diff_list->repo, &conflict->our_entry)) < 0 || + (error = git_merge_file_input_from_index_entry(&theirs, diff_list->repo, &conflict->their_entry)) < 0 || + (error = git_merge_files(&result, &ancestor, &ours, &theirs, automerge_flags)) < 0 || + !result.automergeable || + (error = git_odb_write(&automerge_oid, odb, result.data, result.len, GIT_OBJ_BLOB)) < 0) + goto done; + + if ((index_entry = git_pool_malloc(&diff_list->pool, sizeof(git_index_entry))) == NULL) + GITERR_CHECK_ALLOC(index_entry); + + index_entry->path = git_pool_strdup(&diff_list->pool, result.path); + GITERR_CHECK_ALLOC(index_entry->path); + + index_entry->file_size = result.len; + index_entry->mode = result.mode; + git_oid_cpy(&index_entry->oid, &automerge_oid); + + git_vector_insert(&diff_list->staged, index_entry); + git_vector_insert(&diff_list->resolved, (git_merge_diff *)conflict); + + *resolved = 1; + +done: + git_merge_file_input_free(&ancestor); + git_merge_file_input_free(&ours); + git_merge_file_input_free(&theirs); + git_merge_file_result_free(&result); + git_odb_free(odb); + + return error; +} + +static int merge_conflict_resolve( + int *out, + git_merge_diff_list *diff_list, + const git_merge_diff *conflict, + unsigned int automerge_flags) +{ + int resolved = 0; + int error = 0; + + *out = 0; + + if ((error = merge_conflict_resolve_trivial(&resolved, diff_list, conflict)) < 0) + goto done; + + if (automerge_flags != GIT_MERGE_AUTOMERGE_NONE) { + if (!resolved && (error = merge_conflict_resolve_one_removed(&resolved, diff_list, conflict)) < 0) + goto done; + + if (!resolved && (error = merge_conflict_resolve_one_renamed(&resolved, diff_list, conflict)) < 0) + goto done; + + if (!resolved && (error = merge_conflict_resolve_automerge(&resolved, diff_list, conflict, automerge_flags)) < 0) + goto done; + } + + *out = resolved; + +done: + return error; +} + +/* Rename detection and coalescing */ + +struct merge_diff_similarity { + unsigned char similarity; + size_t other_idx; +}; + +static int index_entry_similarity_exact( + git_repository *repo, + git_index_entry *a, + size_t a_idx, + git_index_entry *b, + size_t b_idx, + void **cache, + const git_merge_tree_opts *opts) +{ + GIT_UNUSED(repo); + GIT_UNUSED(a_idx); + GIT_UNUSED(b_idx); + GIT_UNUSED(cache); + GIT_UNUSED(opts); + + if (git_oid__cmp(&a->oid, &b->oid) == 0) + return 100; + + return 0; +} + +static int index_entry_similarity_calc( + void **out, + git_repository *repo, + git_index_entry *entry, + const git_merge_tree_opts *opts) +{ + git_blob *blob; + git_diff_file diff_file = {{{0}}}; + git_off_t blobsize; + int error; + + *out = NULL; + + if ((error = git_blob_lookup(&blob, repo, &entry->oid)) < 0) + return error; + + git_oid_cpy(&diff_file.oid, &entry->oid); + diff_file.path = entry->path; + diff_file.size = entry->file_size; + diff_file.mode = entry->mode; + diff_file.flags = 0; + + blobsize = git_blob_rawsize(blob); + + /* file too big for rename processing */ + if (!git__is_sizet(blobsize)) + return 0; + + error = opts->metric->buffer_signature(out, &diff_file, + git_blob_rawcontent(blob), (size_t)blobsize, + opts->metric->payload); + + git_blob_free(blob); + + return error; +} + +static int index_entry_similarity_inexact( + git_repository *repo, + git_index_entry *a, + size_t a_idx, + git_index_entry *b, + size_t b_idx, + void **cache, + const git_merge_tree_opts *opts) +{ + int score = 0; + int error = 0; + + if (GIT_MODE_TYPE(a->mode) != GIT_MODE_TYPE(b->mode)) + return 0; + + /* update signature cache if needed */ + if (!cache[a_idx] && (error = index_entry_similarity_calc(&cache[a_idx], repo, a, opts)) < 0) + return error; + if (!cache[b_idx] && (error = index_entry_similarity_calc(&cache[b_idx], repo, b, opts)) < 0) + return error; + + /* some metrics may not wish to process this file (too big / too small) */ + if (!cache[a_idx] || !cache[b_idx]) + return 0; + + /* compare signatures */ + if (opts->metric->similarity( + &score, cache[a_idx], cache[b_idx], opts->metric->payload) < 0) + return -1; + + /* clip score */ + if (score < 0) + score = 0; + else if (score > 100) + score = 100; + + return score; +} + +static int merge_diff_mark_similarity( + git_repository *repo, + git_merge_diff_list *diff_list, + struct merge_diff_similarity *similarity_ours, + struct merge_diff_similarity *similarity_theirs, + int (*similarity_fn)(git_repository *, git_index_entry *, size_t, git_index_entry *, size_t, void **, const git_merge_tree_opts *), + void **cache, + const git_merge_tree_opts *opts) +{ + size_t i, j; + git_merge_diff *conflict_src, *conflict_tgt; + int similarity; + + git_vector_foreach(&diff_list->conflicts, i, conflict_src) { + /* Items can be the source of a rename iff they have an item in the + * ancestor slot and lack an item in the ours or theirs slot. */ + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->ancestor_entry) || + (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->our_entry) && + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->their_entry))) + continue; + + git_vector_foreach(&diff_list->conflicts, j, conflict_tgt) { + size_t our_idx = diff_list->conflicts.length + j; + size_t their_idx = (diff_list->conflicts.length * 2) + j; + + if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_tgt->ancestor_entry)) + continue; + + if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_tgt->our_entry) && + !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->our_entry)) { + similarity = similarity_fn(repo, &conflict_src->ancestor_entry, i, &conflict_tgt->our_entry, our_idx, cache, opts); + + if (similarity == GIT_EBUFS) + continue; + else if (similarity < 0) + return similarity; + + if (similarity > similarity_ours[i].similarity && + similarity > similarity_ours[j].similarity) { + /* Clear previous best similarity */ + if (similarity_ours[i].similarity > 0) + similarity_ours[similarity_ours[i].other_idx].similarity = 0; + + if (similarity_ours[j].similarity > 0) + similarity_ours[similarity_ours[j].other_idx].similarity = 0; + + similarity_ours[i].similarity = similarity; + similarity_ours[i].other_idx = j; + + similarity_ours[j].similarity = similarity; + similarity_ours[j].other_idx = i; + } + } + + if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_tgt->their_entry) && + !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->their_entry)) { + similarity = similarity_fn(repo, &conflict_src->ancestor_entry, i, &conflict_tgt->their_entry, their_idx, cache, opts); + + if (similarity > similarity_theirs[i].similarity && + similarity > similarity_theirs[j].similarity) { + /* Clear previous best similarity */ + if (similarity_theirs[i].similarity > 0) + similarity_theirs[similarity_theirs[i].other_idx].similarity = 0; + + if (similarity_theirs[j].similarity > 0) + similarity_theirs[similarity_theirs[j].other_idx].similarity = 0; + + similarity_theirs[i].similarity = similarity; + similarity_theirs[i].other_idx = j; + + similarity_theirs[j].similarity = similarity; + similarity_theirs[j].other_idx = i; + } + } + } + } + + return 0; +} + +/* + * Rename conflicts: + * + * Ancestor Ours Theirs + * + * 0a A A A No rename + * b A A* A No rename (ours was rewritten) + * c A A A* No rename (theirs rewritten) + * 1a A A B[A] Rename or rename/edit + * b A B[A] A (automergeable) + * 2 A B[A] B[A] Both renamed (automergeable) + * 3a A B[A] Rename/delete + * b A B[A] (same) + * 4a A B[A] B Rename/add [B~ours B~theirs] + * b A B B[A] (same) + * 5 A B[A] C[A] Both renamed ("1 -> 2") + * 6 A C[A] Both renamed ("2 -> 1") + * B C[B] [C~ours C~theirs] (automergeable) + */ +static void merge_diff_mark_rename_conflict( + git_merge_diff_list *diff_list, + struct merge_diff_similarity *similarity_ours, + bool ours_renamed, + size_t ours_source_idx, + struct merge_diff_similarity *similarity_theirs, + bool theirs_renamed, + size_t theirs_source_idx, + git_merge_diff *target, + const git_merge_tree_opts *opts) +{ + git_merge_diff *ours_source = NULL, *theirs_source = NULL; + + if (ours_renamed) + ours_source = diff_list->conflicts.contents[ours_source_idx]; + + if (theirs_renamed) + theirs_source = diff_list->conflicts.contents[theirs_source_idx]; + + /* Detect 2->1 conflicts */ + if (ours_renamed && theirs_renamed) { + /* Both renamed to the same target name. */ + if (ours_source_idx == theirs_source_idx) + ours_source->type = GIT_MERGE_DIFF_BOTH_RENAMED; + else { + ours_source->type = GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1; + theirs_source->type = GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1; + } + } else if (ours_renamed) { + /* If our source was also renamed in theirs, this is a 1->2 */ + if (similarity_theirs[ours_source_idx].similarity >= opts->rename_threshold) + ours_source->type = GIT_MERGE_DIFF_BOTH_RENAMED_1_TO_2; + + else if (GIT_MERGE_INDEX_ENTRY_EXISTS(target->their_entry)) { + ours_source->type = GIT_MERGE_DIFF_RENAMED_ADDED; + target->type = GIT_MERGE_DIFF_RENAMED_ADDED; + } + + else if (!GIT_MERGE_INDEX_ENTRY_EXISTS(ours_source->their_entry)) + ours_source->type = GIT_MERGE_DIFF_RENAMED_DELETED; + + else if (ours_source->type == GIT_MERGE_DIFF_MODIFIED_DELETED) + ours_source->type = GIT_MERGE_DIFF_RENAMED_MODIFIED; + } else if (theirs_renamed) { + /* If their source was also renamed in ours, this is a 1->2 */ + if (similarity_ours[theirs_source_idx].similarity >= opts->rename_threshold) + theirs_source->type = GIT_MERGE_DIFF_BOTH_RENAMED_1_TO_2; + + else if (GIT_MERGE_INDEX_ENTRY_EXISTS(target->our_entry)) { + theirs_source->type = GIT_MERGE_DIFF_RENAMED_ADDED; + target->type = GIT_MERGE_DIFF_RENAMED_ADDED; + } + + else if (!GIT_MERGE_INDEX_ENTRY_EXISTS(theirs_source->our_entry)) + theirs_source->type = GIT_MERGE_DIFF_RENAMED_DELETED; + + else if (theirs_source->type == GIT_MERGE_DIFF_MODIFIED_DELETED) + theirs_source->type = GIT_MERGE_DIFF_RENAMED_MODIFIED; + } +} + +GIT_INLINE(void) merge_diff_coalesce_rename( + git_index_entry *source_entry, + git_delta_t *source_status, + git_index_entry *target_entry, + git_delta_t *target_status) +{ + /* Coalesce the rename target into the rename source. */ + memcpy(source_entry, target_entry, sizeof(git_index_entry)); + *source_status = GIT_DELTA_RENAMED; + + memset(target_entry, 0x0, sizeof(git_index_entry)); + *target_status = GIT_DELTA_UNMODIFIED; +} + +static void merge_diff_list_coalesce_renames( + git_merge_diff_list *diff_list, + struct merge_diff_similarity *similarity_ours, + struct merge_diff_similarity *similarity_theirs, + const git_merge_tree_opts *opts) +{ + size_t i; + bool ours_renamed = 0, theirs_renamed = 0; + size_t ours_source_idx = 0, theirs_source_idx = 0; + git_merge_diff *ours_source, *theirs_source, *target; + + for (i = 0; i < diff_list->conflicts.length; i++) { + target = diff_list->conflicts.contents[i]; + + ours_renamed = 0; + theirs_renamed = 0; + + if (GIT_MERGE_INDEX_ENTRY_EXISTS(target->our_entry) && + similarity_ours[i].similarity >= opts->rename_threshold) { + ours_source_idx = similarity_ours[i].other_idx; + + ours_source = diff_list->conflicts.contents[ours_source_idx]; + + merge_diff_coalesce_rename( + &ours_source->our_entry, + &ours_source->our_status, + &target->our_entry, + &target->our_status); + + similarity_ours[ours_source_idx].similarity = 0; + similarity_ours[i].similarity = 0; + + ours_renamed = 1; + } + + /* insufficient to determine direction */ + if (GIT_MERGE_INDEX_ENTRY_EXISTS(target->their_entry) && + similarity_theirs[i].similarity >= opts->rename_threshold) { + theirs_source_idx = similarity_theirs[i].other_idx; + + theirs_source = diff_list->conflicts.contents[theirs_source_idx]; + + merge_diff_coalesce_rename( + &theirs_source->their_entry, + &theirs_source->their_status, + &target->their_entry, + &target->their_status); + + similarity_theirs[theirs_source_idx].similarity = 0; + similarity_theirs[i].similarity = 0; + + theirs_renamed = 1; + } + + merge_diff_mark_rename_conflict(diff_list, + similarity_ours, ours_renamed, ours_source_idx, + similarity_theirs, theirs_renamed, theirs_source_idx, + target, opts); + } +} + +static int merge_diff_empty(const git_vector *conflicts, size_t idx) +{ + git_merge_diff *conflict = conflicts->contents[idx]; + + return (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) && + !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) && + !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry)); +} + +static void merge_diff_list_count_candidates( + git_merge_diff_list *diff_list, + size_t *src_count, + size_t *tgt_count) +{ + git_merge_diff *entry; + size_t i; + + *src_count = 0; + *tgt_count = 0; + + git_vector_foreach(&diff_list->conflicts, i, entry) { + if (GIT_MERGE_INDEX_ENTRY_EXISTS(entry->ancestor_entry) && + (!GIT_MERGE_INDEX_ENTRY_EXISTS(entry->our_entry) || + !GIT_MERGE_INDEX_ENTRY_EXISTS(entry->their_entry))) + src_count++; + else if (!GIT_MERGE_INDEX_ENTRY_EXISTS(entry->ancestor_entry)) + tgt_count++; + } +} + +int git_merge_diff_list__find_renames( + git_repository *repo, + git_merge_diff_list *diff_list, + const git_merge_tree_opts *opts) +{ + struct merge_diff_similarity *similarity_ours, *similarity_theirs; + void **cache = NULL; + size_t cache_size = 0; + size_t src_count, tgt_count, i; + int error = 0; + + assert(diff_list && opts); + + if ((opts->flags & GIT_MERGE_TREE_FIND_RENAMES) == 0) + return 0; + + similarity_ours = git__calloc(diff_list->conflicts.length, + sizeof(struct merge_diff_similarity)); + GITERR_CHECK_ALLOC(similarity_ours); + + similarity_theirs = git__calloc(diff_list->conflicts.length, + sizeof(struct merge_diff_similarity)); + GITERR_CHECK_ALLOC(similarity_theirs); + + /* Calculate similarity between items that were deleted from the ancestor + * and added in the other branch. + */ + if ((error = merge_diff_mark_similarity(repo, diff_list, similarity_ours, + similarity_theirs, index_entry_similarity_exact, NULL, opts)) < 0) + goto done; + + if (diff_list->conflicts.length <= opts->target_limit) { + cache_size = diff_list->conflicts.length * 3; + cache = git__calloc(cache_size, sizeof(void *)); + GITERR_CHECK_ALLOC(cache); + + merge_diff_list_count_candidates(diff_list, &src_count, &tgt_count); + + if (src_count > opts->target_limit || tgt_count > opts->target_limit) { + /* TODO: report! */ + } else { + if ((error = merge_diff_mark_similarity( + repo, diff_list, similarity_ours, similarity_theirs, + index_entry_similarity_inexact, cache, opts)) < 0) + goto done; + } + } + + /* For entries that are appropriately similar, merge the new name's entry + * into the old name. + */ + merge_diff_list_coalesce_renames(diff_list, similarity_ours, similarity_theirs, opts); + + /* And remove any entries that were merged and are now empty. */ + git_vector_remove_matching(&diff_list->conflicts, merge_diff_empty); + +done: + if (cache != NULL) { + for (i = 0; i < cache_size; ++i) { + if (cache[i] != NULL) + opts->metric->free_signature(cache[i], opts->metric->payload); + } + + git__free(cache); + } + + git__free(similarity_ours); + git__free(similarity_theirs); + + return error; +} + +/* Directory/file conflict handling */ + +GIT_INLINE(const char *) merge_diff_path( + const git_merge_diff *conflict) +{ + if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry)) + return conflict->ancestor_entry.path; + else if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry)) + return conflict->our_entry.path; + else if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry)) + return conflict->their_entry.path; + + return NULL; +} + +GIT_INLINE(bool) merge_diff_any_side_added_or_modified( + const git_merge_diff *conflict) +{ + if (conflict->our_status == GIT_DELTA_ADDED || + conflict->our_status == GIT_DELTA_MODIFIED || + conflict->their_status == GIT_DELTA_ADDED || + conflict->their_status == GIT_DELTA_MODIFIED) + return true; + + return false; +} + +GIT_INLINE(bool) path_is_prefixed(const char *parent, const char *child) +{ + size_t child_len = strlen(child); + size_t parent_len = strlen(parent); + + if (child_len < parent_len || + strncmp(parent, child, parent_len) != 0) + return 0; + + return (child[parent_len] == '/'); +} + +GIT_INLINE(int) merge_diff_detect_df_conflict( + struct merge_diff_df_data *df_data, + git_merge_diff *conflict) +{ + const char *cur_path = merge_diff_path(conflict); + + /* Determine if this is a D/F conflict or the child of one */ + if (df_data->df_path && + path_is_prefixed(df_data->df_path, cur_path)) + conflict->type = GIT_MERGE_DIFF_DF_CHILD; + else if(df_data->df_path) + df_data->df_path = NULL; + else if (df_data->prev_path && + merge_diff_any_side_added_or_modified(df_data->prev_conflict) && + merge_diff_any_side_added_or_modified(conflict) && + path_is_prefixed(df_data->prev_path, cur_path)) { + conflict->type = GIT_MERGE_DIFF_DF_CHILD; + + df_data->prev_conflict->type = GIT_MERGE_DIFF_DIRECTORY_FILE; + df_data->df_path = df_data->prev_path; + } + + df_data->prev_path = cur_path; + df_data->prev_conflict = conflict; + + return 0; +} + +/* Conflict handling */ + +GIT_INLINE(int) merge_diff_detect_type( + git_merge_diff *conflict) +{ + if (conflict->our_status == GIT_DELTA_ADDED && + conflict->their_status == GIT_DELTA_ADDED) + conflict->type = GIT_MERGE_DIFF_BOTH_ADDED; + else if (conflict->our_status == GIT_DELTA_MODIFIED && + conflict->their_status == GIT_DELTA_MODIFIED) + conflict->type = GIT_MERGE_DIFF_BOTH_MODIFIED; + else if (conflict->our_status == GIT_DELTA_DELETED && + conflict->their_status == GIT_DELTA_DELETED) + conflict->type = GIT_MERGE_DIFF_BOTH_DELETED; + else if (conflict->our_status == GIT_DELTA_MODIFIED && + conflict->their_status == GIT_DELTA_DELETED) + conflict->type = GIT_MERGE_DIFF_MODIFIED_DELETED; + else if (conflict->our_status == GIT_DELTA_DELETED && + conflict->their_status == GIT_DELTA_MODIFIED) + conflict->type = GIT_MERGE_DIFF_MODIFIED_DELETED; + else + conflict->type = GIT_MERGE_DIFF_NONE; + + return 0; +} + +GIT_INLINE(int) index_entry_dup( + git_index_entry *out, + git_pool *pool, + const git_index_entry *src) +{ + if (src != NULL) { + memcpy(out, src, sizeof(git_index_entry)); + + if ((out->path = git_pool_strdup(pool, src->path)) == NULL) + return -1; + } + + return 0; +} + +GIT_INLINE(int) merge_delta_type_from_index_entries( + const git_index_entry *ancestor, + const git_index_entry *other) +{ + if (ancestor == NULL && other == NULL) + return GIT_DELTA_UNMODIFIED; + else if (ancestor == NULL && other != NULL) + return GIT_DELTA_ADDED; + else if (ancestor != NULL && other == NULL) + return GIT_DELTA_DELETED; + else if (S_ISDIR(ancestor->mode) ^ S_ISDIR(other->mode)) + return GIT_DELTA_TYPECHANGE; + else if(S_ISLNK(ancestor->mode) ^ S_ISLNK(other->mode)) + return GIT_DELTA_TYPECHANGE; + else if (git_oid__cmp(&ancestor->oid, &other->oid) || + ancestor->mode != other->mode) + return GIT_DELTA_MODIFIED; + + return GIT_DELTA_UNMODIFIED; +} + +static git_merge_diff *merge_diff_from_index_entries( + git_merge_diff_list *diff_list, + const git_index_entry **entries) +{ + git_merge_diff *conflict; + git_pool *pool = &diff_list->pool; + + if ((conflict = git_pool_malloc(pool, sizeof(git_merge_diff))) == NULL) + return NULL; + + if (index_entry_dup(&conflict->ancestor_entry, pool, entries[TREE_IDX_ANCESTOR]) < 0 || + index_entry_dup(&conflict->our_entry, pool, entries[TREE_IDX_OURS]) < 0 || + index_entry_dup(&conflict->their_entry, pool, entries[TREE_IDX_THEIRS]) < 0) + return NULL; + + conflict->our_status = merge_delta_type_from_index_entries( + entries[TREE_IDX_ANCESTOR], entries[TREE_IDX_OURS]); + conflict->their_status = merge_delta_type_from_index_entries( + entries[TREE_IDX_ANCESTOR], entries[TREE_IDX_THEIRS]); + + return conflict; +} + +/* Merge trees */ + +static int merge_index_insert_conflict( + git_merge_diff_list *diff_list, + struct merge_diff_df_data *merge_df_data, + const git_index_entry *tree_items[3]) +{ + git_merge_diff *conflict; + + if ((conflict = merge_diff_from_index_entries(diff_list, tree_items)) == NULL || + merge_diff_detect_type(conflict) < 0 || + merge_diff_detect_df_conflict(merge_df_data, conflict) < 0 || + git_vector_insert(&diff_list->conflicts, conflict) < 0) + return -1; + + return 0; +} + +static int merge_index_insert_unmodified( + git_merge_diff_list *diff_list, + const git_index_entry *tree_items[3]) +{ + int error = 0; + git_index_entry *entry; + + entry = git_pool_malloc(&diff_list->pool, sizeof(git_index_entry)); + GITERR_CHECK_ALLOC(entry); + + if ((error = index_entry_dup(entry, &diff_list->pool, tree_items[0])) >= 0) + error = git_vector_insert(&diff_list->staged, entry); + + return error; +} + +int git_merge_diff_list__find_differences( + git_merge_diff_list *diff_list, + const git_tree *ancestor_tree, + const git_tree *our_tree, + const git_tree *their_tree) +{ + git_iterator *iterators[3] = {0}; + const git_index_entry *items[3] = {0}, *best_cur_item, *cur_items[3]; + git_vector_cmp entry_compare = git_index_entry__cmp; + struct merge_diff_df_data df_data = {0}; + int cur_item_modified; + size_t i, j; + int error = 0; + + assert(diff_list && our_tree && their_tree); + + if ((error = git_iterator_for_tree(&iterators[TREE_IDX_ANCESTOR], (git_tree *)ancestor_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || + (error = git_iterator_for_tree(&iterators[TREE_IDX_OURS], (git_tree *)our_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || + (error = git_iterator_for_tree(&iterators[TREE_IDX_THEIRS], (git_tree *)their_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0) + goto done; + + /* Set up the iterators */ + for (i = 0; i < 3; i++) { + error = git_iterator_current(&items[i], iterators[i]); + if (error < 0 && error != GIT_ITEROVER) + goto done; + } + + while (true) { + for (i = 0; i < 3; i++) + cur_items[i] = NULL; + + best_cur_item = NULL; + cur_item_modified = 0; + + /* Find the next path(s) to consume from each iterator */ + for (i = 0; i < 3; i++) { + if (items[i] == NULL) { + cur_item_modified = 1; + continue; + } + + if (best_cur_item == NULL) { + best_cur_item = items[i]; + cur_items[i] = items[i]; + } else { + int path_diff = entry_compare(items[i], best_cur_item); + + if (path_diff < 0) { + /* + * Found an item that sorts before our current item, make + * our current item this one. + */ + for (j = 0; j < i; j++) + cur_items[j] = NULL; + + cur_item_modified = 1; + best_cur_item = items[i]; + cur_items[i] = items[i]; + } else if (path_diff > 0) { + /* No entry for the current item, this is modified */ + cur_item_modified = 1; + } else if (path_diff == 0) { + cur_items[i] = items[i]; + + if (!cur_item_modified) + cur_item_modified = index_entry_cmp(best_cur_item, items[i]); + } + } + } + + if (best_cur_item == NULL) + break; + + if (cur_item_modified) + error = merge_index_insert_conflict(diff_list, &df_data, cur_items); + else + error = merge_index_insert_unmodified(diff_list, cur_items); + if (error < 0) + goto done; + + /* Advance each iterator that participated */ + for (i = 0; i < 3; i++) { + if (cur_items[i] == NULL) + continue; + + error = git_iterator_advance(&items[i], iterators[i]); + if (error < 0 && error != GIT_ITEROVER) + goto done; + } + } + +done: + for (i = 0; i < 3; i++) + git_iterator_free(iterators[i]); + + if (error == GIT_ITEROVER) + error = 0; + + return error; +} + +git_merge_diff_list *git_merge_diff_list__alloc(git_repository *repo) +{ + git_merge_diff_list *diff_list = git__calloc(1, sizeof(git_merge_diff_list)); + + if (diff_list == NULL) + return NULL; + + diff_list->repo = repo; + + if (git_vector_init(&diff_list->staged, 0, NULL) < 0 || + git_vector_init(&diff_list->conflicts, 0, NULL) < 0 || + git_vector_init(&diff_list->resolved, 0, NULL) < 0 || + git_pool_init(&diff_list->pool, 1, 0) < 0) + return NULL; + + return diff_list; +} + +void git_merge_diff_list__free(git_merge_diff_list *diff_list) +{ + if (!diff_list) + return; + + git_vector_free(&diff_list->staged); + git_vector_free(&diff_list->conflicts); + git_vector_free(&diff_list->resolved); + git_pool_clear(&diff_list->pool); + git__free(diff_list); +} + +static int merge_tree_normalize_opts( + git_repository *repo, + git_merge_tree_opts *opts, + const git_merge_tree_opts *given) +{ + git_config *cfg = NULL; + int error = 0; + + assert(repo && opts); + + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + return error; + + if (given != NULL) + memcpy(opts, given, sizeof(git_merge_tree_opts)); + else { + git_merge_tree_opts init = GIT_MERGE_TREE_OPTS_INIT; + memcpy(opts, &init, sizeof(init)); + + opts->flags = GIT_MERGE_TREE_FIND_RENAMES; + opts->rename_threshold = GIT_MERGE_TREE_RENAME_THRESHOLD; + } + + if (!opts->target_limit) { + int32_t limit = 0; + + opts->target_limit = GIT_MERGE_TREE_TARGET_LIMIT; + + if (git_config_get_int32(&limit, cfg, "merge.renameLimit") < 0) { + giterr_clear(); + + if (git_config_get_int32(&limit, cfg, "diff.renameLimit") < 0) + giterr_clear(); + } + + if (limit > 0) + opts->target_limit = limit; + } + + /* assign the internal metric with whitespace flag as payload */ + if (!opts->metric) { + opts->metric = git__malloc(sizeof(git_diff_similarity_metric)); + GITERR_CHECK_ALLOC(opts->metric); + + opts->metric->file_signature = git_diff_find_similar__hashsig_for_file; + opts->metric->buffer_signature = git_diff_find_similar__hashsig_for_buf; + opts->metric->free_signature = git_diff_find_similar__hashsig_free; + opts->metric->similarity = git_diff_find_similar__calc_similarity; + + if (opts->flags & GIT_DIFF_FIND_IGNORE_WHITESPACE) + opts->metric->payload = (void *)GIT_HASHSIG_IGNORE_WHITESPACE; + else if (opts->flags & GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE) + opts->metric->payload = (void *)GIT_HASHSIG_NORMAL; + else + opts->metric->payload = (void *)GIT_HASHSIG_SMART_WHITESPACE; + } + + return 0; +} + + +static int merge_index_insert_reuc( + git_index *index, + size_t idx, + const git_index_entry *entry) +{ + const git_index_reuc_entry *reuc; + int mode[3] = { 0, 0, 0 }; + git_oid const *oid[3] = { NULL, NULL, NULL }; + size_t i; + + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(*entry)) + return 0; + + if ((reuc = git_index_reuc_get_bypath(index, entry->path)) != NULL) { + for (i = 0; i < 3; i++) { + mode[i] = reuc->mode[i]; + oid[i] = &reuc->oid[i]; + } + } + + mode[idx] = entry->mode; + oid[idx] = &entry->oid; + + return git_index_reuc_add(index, entry->path, + mode[0], oid[0], mode[1], oid[1], mode[2], oid[2]); +} + +int index_from_diff_list(git_index **out, git_merge_diff_list *diff_list) +{ + git_index *index; + size_t i; + git_index_entry *entry; + git_merge_diff *conflict; + int error = 0; + + *out = NULL; + + if ((error = git_index_new(&index)) < 0) + return error; + + git_vector_foreach(&diff_list->staged, i, entry) { + if ((error = git_index_add(index, entry)) < 0) + goto on_error; + } + + git_vector_foreach(&diff_list->conflicts, i, conflict) { + const git_index_entry *ancestor = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ? + &conflict->ancestor_entry : NULL; + + const git_index_entry *ours = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ? + &conflict->our_entry : NULL; + + const git_index_entry *theirs = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ? + &conflict->their_entry : NULL; + + if ((error = git_index_conflict_add(index, ancestor, ours, theirs)) < 0) + goto on_error; + } + + /* Add each rename entry to the rename portion of the index. */ + git_vector_foreach(&diff_list->conflicts, i, conflict) { + const char *ancestor_path, *our_path, *their_path; + + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry)) + continue; + + ancestor_path = conflict->ancestor_entry.path; + + our_path = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ? + conflict->our_entry.path : NULL; + + their_path = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ? + conflict->their_entry.path : NULL; + + if ((our_path && strcmp(ancestor_path, our_path) != 0) || + (their_path && strcmp(ancestor_path, their_path) != 0)) { + if ((error = git_index_name_add(index, ancestor_path, our_path, their_path)) < 0) + goto on_error; + } + } + + /* Add each entry in the resolved conflict to the REUC independently, since + * the paths may differ due to renames. */ + git_vector_foreach(&diff_list->resolved, i, conflict) { + const git_index_entry *ancestor = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ? + &conflict->ancestor_entry : NULL; + + const git_index_entry *ours = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ? + &conflict->our_entry : NULL; + + const git_index_entry *theirs = + GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ? + &conflict->their_entry : NULL; + + if (ancestor != NULL && + (error = merge_index_insert_reuc(index, TREE_IDX_ANCESTOR, ancestor)) < 0) + goto on_error; + + if (ours != NULL && + (error = merge_index_insert_reuc(index, TREE_IDX_OURS, ours)) < 0) + goto on_error; + + if (theirs != NULL && + (error = merge_index_insert_reuc(index, TREE_IDX_THEIRS, theirs)) < 0) + goto on_error; + } + + *out = index; + return 0; + +on_error: + git_index_free(index); + + return error; +} + +int git_merge_trees( + git_index **out, + git_repository *repo, + const git_tree *ancestor_tree, + const git_tree *our_tree, + const git_tree *their_tree, + const git_merge_tree_opts *given_opts) +{ + git_merge_diff_list *diff_list; + git_merge_tree_opts opts; + git_merge_diff *conflict; + git_vector changes; + size_t i; + int error = 0; + + assert(out && repo && our_tree && their_tree); + + *out = NULL; + + if ((error = merge_tree_normalize_opts(repo, &opts, given_opts)) < 0) + return error; + + diff_list = git_merge_diff_list__alloc(repo); + GITERR_CHECK_ALLOC(diff_list); + + if ((error = git_merge_diff_list__find_differences(diff_list, ancestor_tree, our_tree, their_tree)) < 0 || + (error = git_merge_diff_list__find_renames(repo, diff_list, &opts)) < 0) + goto done; + + memcpy(&changes, &diff_list->conflicts, sizeof(git_vector)); + git_vector_clear(&diff_list->conflicts); + + git_vector_foreach(&changes, i, conflict) { + int resolved = 0; + + if ((error = merge_conflict_resolve(&resolved, diff_list, conflict, opts.automerge_flags)) < 0) + goto done; + + if (!resolved) + git_vector_insert(&diff_list->conflicts, conflict); + } + + if (!given_opts || !given_opts->metric) + git__free(opts.metric); + + error = index_from_diff_list(out, diff_list); + +done: + git_merge_diff_list__free(diff_list); + + return error; +} + +/* Merge setup / cleanup */ + +static int write_orig_head( + git_repository *repo, + const git_merge_head *our_head) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_buf file_path = GIT_BUF_INIT; + char orig_oid_str[GIT_OID_HEXSZ + 1]; + int error = 0; + + assert(repo && our_head); + + git_oid_tostr(orig_oid_str, GIT_OID_HEXSZ+1, &our_head->oid); + + if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_ORIG_HEAD_FILE)) == 0 && + (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE)) == 0 && + (error = git_filebuf_printf(&file, "%s\n", orig_oid_str)) == 0) + error = git_filebuf_commit(&file, 0666); + + if (error < 0) + git_filebuf_cleanup(&file); + + git_buf_free(&file_path); + + return error; +} + +static int write_merge_head( + git_repository *repo, + const git_merge_head *heads[], + size_t heads_len) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_buf file_path = GIT_BUF_INIT; + char merge_oid_str[GIT_OID_HEXSZ + 1]; + size_t i; + int error = 0; + + assert(repo && heads); + + if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_MERGE_HEAD_FILE)) < 0 || + (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE)) < 0) + goto cleanup; + + for (i = 0; i < heads_len; i++) { + git_oid_tostr(merge_oid_str, GIT_OID_HEXSZ+1, &heads[i]->oid); + + if ((error = git_filebuf_printf(&file, "%s\n", merge_oid_str)) < 0) + goto cleanup; + } + + error = git_filebuf_commit(&file, 0666); + +cleanup: + if (error < 0) + git_filebuf_cleanup(&file); + + git_buf_free(&file_path); + + return error; +} + +static int write_merge_mode(git_repository *repo, unsigned int flags) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_buf file_path = GIT_BUF_INIT; + int error = 0; + + /* For future expansion */ + GIT_UNUSED(flags); + + assert(repo); + + if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_MERGE_MODE_FILE)) < 0 || + (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE)) < 0) + goto cleanup; + + error = git_filebuf_commit(&file, 0666); + +cleanup: + if (error < 0) + git_filebuf_cleanup(&file); + + git_buf_free(&file_path); + + return error; +} + +struct merge_msg_entry { + const git_merge_head *merge_head; + bool written; +}; + +static int msg_entry_is_branch( + const struct merge_msg_entry *entry, + git_vector *entries) +{ + GIT_UNUSED(entries); + + return (entry->written == 0 && + entry->merge_head->remote_url == NULL && + entry->merge_head->ref_name != NULL && + git__strncmp(GIT_REFS_HEADS_DIR, entry->merge_head->ref_name, strlen(GIT_REFS_HEADS_DIR)) == 0); +} + +static int msg_entry_is_tracking( + const struct merge_msg_entry *entry, + git_vector *entries) +{ + GIT_UNUSED(entries); + + return (entry->written == 0 && + entry->merge_head->remote_url == NULL && + entry->merge_head->ref_name != NULL && + git__strncmp(GIT_REFS_REMOTES_DIR, entry->merge_head->ref_name, strlen(GIT_REFS_REMOTES_DIR)) == 0); +} + +static int msg_entry_is_tag( + const struct merge_msg_entry *entry, + git_vector *entries) +{ + GIT_UNUSED(entries); + + return (entry->written == 0 && + entry->merge_head->remote_url == NULL && + entry->merge_head->ref_name != NULL && + git__strncmp(GIT_REFS_TAGS_DIR, entry->merge_head->ref_name, strlen(GIT_REFS_TAGS_DIR)) == 0); +} + +static int msg_entry_is_remote( + const struct merge_msg_entry *entry, + git_vector *entries) +{ + if (entry->written == 0 && + entry->merge_head->remote_url != NULL && + entry->merge_head->ref_name != NULL && + git__strncmp(GIT_REFS_HEADS_DIR, entry->merge_head->ref_name, strlen(GIT_REFS_HEADS_DIR)) == 0) + { + struct merge_msg_entry *existing; + + /* Match only branches from the same remote */ + if (entries->length == 0) + return 1; + + existing = git_vector_get(entries, 0); + + return (git__strcmp(existing->merge_head->remote_url, + entry->merge_head->remote_url) == 0); + } + + return 0; +} + +static int msg_entry_is_oid( + const struct merge_msg_entry *merge_msg_entry) +{ + return (merge_msg_entry->written == 0 && + merge_msg_entry->merge_head->ref_name == NULL && + merge_msg_entry->merge_head->remote_url == NULL); +} + +static int merge_msg_entry_written( + const struct merge_msg_entry *merge_msg_entry) +{ + return (merge_msg_entry->written == 1); +} + +static int merge_msg_entries( + git_vector *v, + const struct merge_msg_entry *entries, + size_t len, + int (*match)(const struct merge_msg_entry *entry, git_vector *entries)) +{ + size_t i; + int matches, total = 0; + + git_vector_clear(v); + + for (i = 0; i < len; i++) { + if ((matches = match(&entries[i], v)) < 0) + return matches; + else if (!matches) + continue; + + git_vector_insert(v, (struct merge_msg_entry *)&entries[i]); + total++; + } + + return total; +} + +static int merge_msg_write_entries( + git_filebuf *file, + git_vector *entries, + const char *item_name, + const char *item_plural_name, + size_t ref_name_skip, + const char *source, + char sep) +{ + struct merge_msg_entry *entry; + size_t i; + int error = 0; + + if (entries->length == 0) + return 0; + + if (sep && (error = git_filebuf_printf(file, "%c ", sep)) < 0) + goto done; + + if ((error = git_filebuf_printf(file, "%s ", + (entries->length == 1) ? item_name : item_plural_name)) < 0) + goto done; + + git_vector_foreach(entries, i, entry) { + if (i > 0 && + (error = git_filebuf_printf(file, "%s", (i == entries->length - 1) ? " and " : ", ")) < 0) + goto done; + + if ((error = git_filebuf_printf(file, "'%s'", entry->merge_head->ref_name + ref_name_skip)) < 0) + goto done; + + entry->written = 1; + } + + if (source) + error = git_filebuf_printf(file, " of %s", source); + +done: + return error; +} + +static int merge_msg_write_branches( + git_filebuf *file, + git_vector *entries, + char sep) +{ + return merge_msg_write_entries(file, entries, + "branch", "branches", strlen(GIT_REFS_HEADS_DIR), NULL, sep); +} + +static int merge_msg_write_tracking( + git_filebuf *file, + git_vector *entries, + char sep) +{ + return merge_msg_write_entries(file, entries, + "remote-tracking branch", "remote-tracking branches", 0, NULL, sep); +} + +static int merge_msg_write_tags( + git_filebuf *file, + git_vector *entries, + char sep) +{ + return merge_msg_write_entries(file, entries, + "tag", "tags", strlen(GIT_REFS_TAGS_DIR), NULL, sep); +} + +static int merge_msg_write_remotes( + git_filebuf *file, + git_vector *entries, + char sep) +{ + const char *source; + + if (entries->length == 0) + return 0; + + source = ((struct merge_msg_entry *)entries->contents[0])->merge_head->remote_url; + + return merge_msg_write_entries(file, entries, + "branch", "branches", strlen(GIT_REFS_HEADS_DIR), source, sep); +} + +static int write_merge_msg( + git_repository *repo, + const git_merge_head *heads[], + size_t heads_len) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_buf file_path = GIT_BUF_INIT; + char oid_str[GIT_OID_HEXSZ + 1]; + struct merge_msg_entry *entries; + git_vector matching = GIT_VECTOR_INIT; + size_t i; + char sep = 0; + int error = 0; + + assert(repo && heads); + + entries = git__calloc(heads_len, sizeof(struct merge_msg_entry)); + GITERR_CHECK_ALLOC(entries); + + if (git_vector_init(&matching, heads_len, NULL) < 0) { + git__free(entries); + return -1; + } + + for (i = 0; i < heads_len; i++) + entries[i].merge_head = heads[i]; + + if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_MERGE_MSG_FILE)) < 0 || + (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE)) < 0 || + (error = git_filebuf_write(&file, "Merge ", 6)) < 0) + goto cleanup; + + /* + * This is to emulate the format of MERGE_MSG by core git. + * + * Core git will write all the commits specified by OID, in the order + * provided, until the first named branch or tag is reached, at which + * point all branches will be written in the order provided, then all + * tags, then all remote tracking branches and finally all commits that + * were specified by OID that were not already written. + * + * Yes. Really. + */ + for (i = 0; i < heads_len; i++) { + if (!msg_entry_is_oid(&entries[i])) + break; + + git_oid_fmt(oid_str, &entries[i].merge_head->oid); + oid_str[GIT_OID_HEXSZ] = '\0'; + + if ((error = git_filebuf_printf(&file, "%scommit '%s'", (i > 0) ? "; " : "", oid_str)) < 0) + goto cleanup; + + entries[i].written = 1; + } + + if (i) + sep = ';'; + + if ((error = merge_msg_entries(&matching, entries, heads_len, msg_entry_is_branch)) < 0 || + (error = merge_msg_write_branches(&file, &matching, sep)) < 0) + goto cleanup; + + if (matching.length) + sep =','; + + if ((error = merge_msg_entries(&matching, entries, heads_len, msg_entry_is_tracking)) < 0 || + (error = merge_msg_write_tracking(&file, &matching, sep)) < 0) + goto cleanup; + + if (matching.length) + sep =','; + + if ((error = merge_msg_entries(&matching, entries, heads_len, msg_entry_is_tag)) < 0 || + (error = merge_msg_write_tags(&file, &matching, sep)) < 0) + goto cleanup; + + if (matching.length) + sep =','; + + /* We should never be called with multiple remote branches, but handle + * it in case we are... */ + while ((error = merge_msg_entries(&matching, entries, heads_len, msg_entry_is_remote)) > 0) { + if ((error = merge_msg_write_remotes(&file, &matching, sep)) < 0) + goto cleanup; + + if (matching.length) + sep =','; + } + + if (error < 0) + goto cleanup; + + for (i = 0; i < heads_len; i++) { + if (merge_msg_entry_written(&entries[i])) + continue; + + git_oid_fmt(oid_str, &entries[i].merge_head->oid); + oid_str[GIT_OID_HEXSZ] = '\0'; + + if ((error = git_filebuf_printf(&file, "; commit '%s'", oid_str)) < 0) + goto cleanup; + } + + if ((error = git_filebuf_printf(&file, "\n")) < 0 || + (error = git_filebuf_commit(&file, 0666)) < 0) + goto cleanup; + +cleanup: + if (error < 0) + git_filebuf_cleanup(&file); + + git_buf_free(&file_path); + + git_vector_free(&matching); + git__free(entries); + + return error; +} + +int git_merge__setup( + git_repository *repo, + const git_merge_head *our_head, + const git_merge_head *heads[], + size_t heads_len, + unsigned int flags) +{ + int error = 0; + + assert (repo && our_head && heads); + + if ((error = write_orig_head(repo, our_head)) == 0 && + (error = write_merge_head(repo, heads, heads_len)) == 0 && + (error = write_merge_mode(repo, flags)) == 0) { + error = write_merge_msg(repo, heads, heads_len); + } + + return error; +} + +int git_repository_merge_cleanup(git_repository *repo) +{ + int error = 0; + git_buf merge_head_path = GIT_BUF_INIT, + merge_mode_path = GIT_BUF_INIT, + merge_msg_path = GIT_BUF_INIT; + + assert(repo); + + if (git_buf_joinpath(&merge_head_path, repo->path_repository, GIT_MERGE_HEAD_FILE) < 0 || + git_buf_joinpath(&merge_mode_path, repo->path_repository, GIT_MERGE_MODE_FILE) < 0 || + git_buf_joinpath(&merge_msg_path, repo->path_repository, GIT_MERGE_MSG_FILE) < 0) + return -1; + + if (git_path_isfile(merge_head_path.ptr)) { + if ((error = p_unlink(merge_head_path.ptr)) < 0) + goto cleanup; + } + + if (git_path_isfile(merge_mode_path.ptr)) + (void)p_unlink(merge_mode_path.ptr); + + if (git_path_isfile(merge_msg_path.ptr)) + (void)p_unlink(merge_msg_path.ptr); + +cleanup: + git_buf_free(&merge_msg_path); + git_buf_free(&merge_mode_path); + git_buf_free(&merge_head_path); + + return error; +} + +/* Merge heads are the input to merge */ + +static int merge_head_init( + git_merge_head **out, + git_repository *repo, + const char *ref_name, + const char *remote_url, + const git_oid *oid) +{ + git_merge_head *head; + int error = 0; + + assert(out && oid); + + *out = NULL; + + head = git__calloc(1, sizeof(git_merge_head)); + GITERR_CHECK_ALLOC(head); + + if (ref_name) { + head->ref_name = git__strdup(ref_name); + GITERR_CHECK_ALLOC(head->ref_name); + } + + if (remote_url) { + head->remote_url = git__strdup(remote_url); + GITERR_CHECK_ALLOC(head->remote_url); + } + + git_oid_cpy(&head->oid, oid); + + if ((error = git_commit_lookup(&head->commit, repo, &head->oid)) < 0) { + git_merge_head_free(head); + return error; + } + + *out = head; + return error; +} + +int git_merge_head_from_ref( + git_merge_head **out, + git_repository *repo, + git_reference *ref) +{ + git_reference *resolved; + int error = 0; + + assert(out && repo && ref); + + *out = NULL; + + if ((error = git_reference_resolve(&resolved, ref)) < 0) + return error; + + error = merge_head_init(out, repo, git_reference_name(ref), NULL, + git_reference_target(resolved)); + + git_reference_free(resolved); + return error; +} + +int git_merge_head_from_oid( + git_merge_head **out, + git_repository *repo, + const git_oid *oid) +{ + assert(out && repo && oid); + + return merge_head_init(out, repo, NULL, NULL, oid); +} + +int git_merge_head_from_fetchhead( + git_merge_head **out, + git_repository *repo, + const char *branch_name, + const char *remote_url, + const git_oid *oid) +{ + assert(repo && branch_name && remote_url && oid); + + return merge_head_init(out, repo, branch_name, remote_url, oid); +} + +void git_merge_head_free(git_merge_head *head) +{ + if (head == NULL) + return; + + if (head->commit != NULL) + git_object_free((git_object *)head->commit); + + if (head->ref_name != NULL) + git__free(head->ref_name); + + if (head->remote_url != NULL) + git__free(head->remote_url); + + git__free(head); +} diff --git a/src/merge.h b/src/merge.h index 22c644270..ba6725de9 100644 --- a/src/merge.h +++ b/src/merge.h @@ -7,16 +7,143 @@ #ifndef INCLUDE_merge_h__ #define INCLUDE_merge_h__ -#include "git2/types.h" -#include "git2/merge.h" -#include "commit_list.h" #include "vector.h" +#include "commit_list.h" +#include "pool.h" + +#include "git2/merge.h" +#include "git2/types.h" #define GIT_MERGE_MSG_FILE "MERGE_MSG" #define GIT_MERGE_MODE_FILE "MERGE_MODE" -#define MERGE_CONFIG_FILE_MODE 0666 +#define GIT_MERGE_TREE_RENAME_THRESHOLD 50 +#define GIT_MERGE_TREE_TARGET_LIMIT 1000 + +/** Types of changes when files are merged from branch to branch. */ +typedef enum { + /* No conflict - a change only occurs in one branch. */ + GIT_MERGE_DIFF_NONE = 0, + + /* Occurs when a file is modified in both branches. */ + GIT_MERGE_DIFF_BOTH_MODIFIED = (1 << 0), + + /* Occurs when a file is added in both branches. */ + GIT_MERGE_DIFF_BOTH_ADDED = (1 << 1), + + /* Occurs when a file is deleted in both branches. */ + GIT_MERGE_DIFF_BOTH_DELETED = (1 << 2), + + /* Occurs when a file is modified in one branch and deleted in the other. */ + GIT_MERGE_DIFF_MODIFIED_DELETED = (1 << 3), + + /* Occurs when a file is renamed in one branch and modified in the other. */ + GIT_MERGE_DIFF_RENAMED_MODIFIED = (1 << 4), + + /* Occurs when a file is renamed in one branch and deleted in the other. */ + GIT_MERGE_DIFF_RENAMED_DELETED = (1 << 5), + + /* Occurs when a file is renamed in one branch and a file with the same + * name is added in the other. Eg, A->B and new file B. Core git calls + * this a "rename/delete". */ + GIT_MERGE_DIFF_RENAMED_ADDED = (1 << 6), + + /* Occurs when both a file is renamed to the same name in the ours and + * theirs branches. Eg, A->B and A->B in both. Automergeable. */ + GIT_MERGE_DIFF_BOTH_RENAMED = (1 << 7), + + /* Occurs when a file is renamed to different names in the ours and theirs + * branches. Eg, A->B and A->C. */ + GIT_MERGE_DIFF_BOTH_RENAMED_1_TO_2 = (1 << 8), + + /* Occurs when two files are renamed to the same name in the ours and + * theirs branches. Eg, A->C and B->C. */ + GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1 = (1 << 9), + + /* Occurs when an item at a path in one branch is a directory, and an + * item at the same path in a different branch is a file. */ + GIT_MERGE_DIFF_DIRECTORY_FILE = (1 << 10), + + /* The child of a folder that is in a directory/file conflict. */ + GIT_MERGE_DIFF_DF_CHILD = (1 << 11), +} git_merge_diff_type_t; + + +typedef struct { + git_repository *repo; + git_pool pool; + + /* Vector of git_index_entry that represent the merged items that + * have been staged, either because only one side changed, or because + * the two changes were non-conflicting and mergeable. These items + * will be written as staged entries in the main index. + */ + git_vector staged; + + /* Vector of git_merge_diff entries that represent the conflicts that + * have not been automerged. These items will be written to high-stage + * entries in the main index. + */ + git_vector conflicts; + + /* Vector of git_merge_diff that have been automerged. These items + * will be written to the REUC when the index is produced. + */ + git_vector resolved; +} git_merge_diff_list; + +/** + * Description of changes to one file across three trees. + */ +typedef struct { + git_merge_diff_type_t type; + + git_index_entry ancestor_entry; + + git_index_entry our_entry; + git_delta_t our_status; + + git_index_entry their_entry; + git_delta_t their_status; +} git_merge_diff; + +/** Internal structure for merge inputs */ +struct git_merge_head { + char *ref_name; + char *remote_url; + + git_oid oid; + git_commit *commit; +}; + +int git_merge__bases_many( + git_commit_list **out, + git_revwalk *walk, + git_commit_list_node *one, + git_vector *twos); + +/* + * Three-way tree differencing + */ + +git_merge_diff_list *git_merge_diff_list__alloc(git_repository *repo); + +int git_merge_diff_list__find_differences(git_merge_diff_list *merge_diff_list, + const git_tree *ancestor_tree, + const git_tree *ours_tree, + const git_tree *theirs_tree); + +int git_merge_diff_list__find_renames(git_repository *repo, git_merge_diff_list *merge_diff_list, const git_merge_tree_opts *opts); + +void git_merge_diff_list__free(git_merge_diff_list *diff_list); + +/* Merge metadata setup */ -int git_merge__bases_many(git_commit_list **out, git_revwalk *walk, git_commit_list_node *one, git_vector *twos); +int git_merge__setup( + git_repository *repo, + const git_merge_head *our_head, + const git_merge_head *their_heads[], + size_t their_heads_len, + unsigned int flags); #endif diff --git a/src/merge_file.c b/src/merge_file.c new file mode 100644 index 000000000..c3477ccb9 --- /dev/null +++ b/src/merge_file.c @@ -0,0 +1,174 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" +#include "repository.h" +#include "merge_file.h" + +#include "git2/repository.h" +#include "git2/object.h" +#include "git2/index.h" + +#include "xdiff/xdiff.h" + +#define GIT_MERGE_FILE_SIDE_EXISTS(X) ((X)->mode != 0) + +GIT_INLINE(const char *) merge_file_best_path( + const git_merge_file_input *ancestor, + const git_merge_file_input *ours, + const git_merge_file_input *theirs) +{ + if (!GIT_MERGE_FILE_SIDE_EXISTS(ancestor)) { + if (strcmp(ours->path, theirs->path) == 0) + return ours->path; + + return NULL; + } + + if (strcmp(ancestor->path, ours->path) == 0) + return theirs->path; + else if(strcmp(ancestor->path, theirs->path) == 0) + return ours->path; + + return NULL; +} + +GIT_INLINE(int) merge_file_best_mode( + const git_merge_file_input *ancestor, + const git_merge_file_input *ours, + const git_merge_file_input *theirs) +{ + /* + * If ancestor didn't exist and either ours or theirs is executable, + * assume executable. Otherwise, if any mode changed from the ancestor, + * use that one. + */ + if (GIT_MERGE_FILE_SIDE_EXISTS(ancestor)) { + if (ours->mode == GIT_FILEMODE_BLOB_EXECUTABLE || + theirs->mode == GIT_FILEMODE_BLOB_EXECUTABLE) + return GIT_FILEMODE_BLOB_EXECUTABLE; + + return GIT_FILEMODE_BLOB; + } + + if (ancestor->mode == ours->mode) + return theirs->mode; + else if(ancestor->mode == theirs->mode) + return ours->mode; + + return 0; +} + +int git_merge_file_input_from_index_entry( + git_merge_file_input *input, + git_repository *repo, + const git_index_entry *entry) +{ + git_odb *odb = NULL; + int error = 0; + + assert(input && repo && entry); + + if (entry->mode == 0) + return 0; + + if ((error = git_repository_odb(&odb, repo)) < 0 || + (error = git_odb_read(&input->odb_object, odb, &entry->oid)) < 0) + goto done; + + input->mode = entry->mode; + input->path = git__strdup(entry->path); + input->mmfile.size = git_odb_object_size(input->odb_object); + input->mmfile.ptr = (char *)git_odb_object_data(input->odb_object); + + if (input->label == NULL) + input->label = entry->path; + +done: + git_odb_free(odb); + + return error; +} + +int git_merge_file_input_from_diff_file( + git_merge_file_input *input, + git_repository *repo, + const git_diff_file *file) +{ + git_odb *odb = NULL; + int error = 0; + + assert(input && repo && file); + + if (file->mode == 0) + return 0; + + if ((error = git_repository_odb(&odb, repo)) < 0 || + (error = git_odb_read(&input->odb_object, odb, &file->oid)) < 0) + goto done; + + input->mode = file->mode; + input->path = git__strdup(file->path); + input->mmfile.size = git_odb_object_size(input->odb_object); + input->mmfile.ptr = (char *)git_odb_object_data(input->odb_object); + + if (input->label == NULL) + input->label = file->path; + +done: + git_odb_free(odb); + + return error; +} + +int git_merge_files( + git_merge_file_result *out, + git_merge_file_input *ancestor, + git_merge_file_input *ours, + git_merge_file_input *theirs, + git_merge_automerge_flags flags) +{ + xmparam_t xmparam; + mmbuffer_t mmbuffer; + int xdl_result; + int error = 0; + + assert(out && ancestor && ours && theirs); + + memset(out, 0x0, sizeof(git_merge_file_result)); + + if (!GIT_MERGE_FILE_SIDE_EXISTS(ours) || !GIT_MERGE_FILE_SIDE_EXISTS(theirs)) + return 0; + + memset(&xmparam, 0x0, sizeof(xmparam_t)); + xmparam.ancestor = ancestor->label; + xmparam.file1 = ours->label; + xmparam.file2 = theirs->label; + + out->path = merge_file_best_path(ancestor, ours, theirs); + out->mode = merge_file_best_mode(ancestor, ours, theirs); + + if (flags == GIT_MERGE_AUTOMERGE_FAVOR_OURS) + xmparam.favor = XDL_MERGE_FAVOR_OURS; + + if (flags == GIT_MERGE_AUTOMERGE_FAVOR_THEIRS) + xmparam.favor = XDL_MERGE_FAVOR_THEIRS; + + if ((xdl_result = xdl_merge(&ancestor->mmfile, &ours->mmfile, + &theirs->mmfile, &xmparam, &mmbuffer)) < 0) { + giterr_set(GITERR_MERGE, "Failed to merge files."); + error = -1; + goto done; + } + + out->automergeable = (xdl_result == 0); + out->data = (unsigned char *)mmbuffer.ptr; + out->len = mmbuffer.size; + +done: + return error; +} diff --git a/src/merge_file.h b/src/merge_file.h new file mode 100644 index 000000000..0af2f0a57 --- /dev/null +++ b/src/merge_file.h @@ -0,0 +1,71 @@ +/* + * 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_filediff_h__ +#define INCLUDE_filediff_h__ + +#include "xdiff/xdiff.h" + +#include "git2/merge.h" + +typedef struct { + const char *label; + char *path; + unsigned int mode; + mmfile_t mmfile; + + git_odb_object *odb_object; +} git_merge_file_input; + +#define GIT_MERGE_FILE_INPUT_INIT {0} + +typedef struct { + bool automergeable; + + const char *path; + int mode; + + unsigned char *data; + size_t len; +} git_merge_file_result; + +#define GIT_MERGE_FILE_RESULT_INIT {0} + +int git_merge_file_input_from_index_entry( + git_merge_file_input *input, + git_repository *repo, + const git_index_entry *entry); + +int git_merge_file_input_from_diff_file( + git_merge_file_input *input, + git_repository *repo, + const git_diff_file *file); + +int git_merge_files( + git_merge_file_result *out, + git_merge_file_input *ancestor, + git_merge_file_input *ours, + git_merge_file_input *theirs, + git_merge_automerge_flags flags); + +GIT_INLINE(void) git_merge_file_input_free(git_merge_file_input *input) +{ + assert(input); + git__free(input->path); + git_odb_object_free(input->odb_object); +} + +GIT_INLINE(void) git_merge_file_result_free(git_merge_file_result *filediff) +{ + if (filediff == NULL) + return; + + /* xdiff uses malloc() not git_malloc, so we use free(), not git_free() */ + if (filediff->data != NULL) + free(filediff->data); +} + +#endif diff --git a/src/mwindow.c b/src/mwindow.c index b35503d46..7e5fcdfbc 100644 --- a/src/mwindow.c +++ b/src/mwindow.c @@ -162,7 +162,7 @@ static git_mwindow *new_window( git_mwindow *w; w = git__malloc(sizeof(*w)); - + if (w == NULL) return NULL; diff --git a/src/notes.c b/src/notes.c index ef48ac88e..beace1b50 100644 --- a/src/notes.c +++ b/src/notes.c @@ -13,6 +13,12 @@ #include "iterator.h" #include "signature.h" +static int note_error_notfound(void) +{ + giterr_set(GITERR_INVALID, "Note could not be found"); + return GIT_ENOTFOUND; +} + static int find_subtree_in_current_level( git_tree **out, git_repository *repo, @@ -26,7 +32,7 @@ static int find_subtree_in_current_level( *out = NULL; if (parent == NULL) - return GIT_ENOTFOUND; + return note_error_notfound(); for (i = 0; i < git_tree_entrycount(parent); i++) { entry = git_tree_entry_byindex(parent, i); @@ -44,7 +50,7 @@ static int find_subtree_in_current_level( return GIT_EEXISTS; } - return GIT_ENOTFOUND; + return note_error_notfound(); } static int find_subtree_r(git_tree **out, git_tree *root, @@ -56,9 +62,8 @@ static int find_subtree_r(git_tree **out, git_tree *root, *out = NULL; error = find_subtree_in_current_level(&subtree, repo, root, target, *fanout); - if (error == GIT_EEXISTS) { + if (error == GIT_EEXISTS) return git_tree_lookup(out, repo, git_tree_id(root)); - } if (error < 0) return error; @@ -85,7 +90,8 @@ static int find_blob(git_oid *blob, git_tree *tree, const char *target) return 0; } } - return GIT_ENOTFOUND; + + return note_error_notfound(); } static int tree_write( @@ -316,8 +322,8 @@ static int note_new(git_note **out, git_oid *note_oid, git_blob *blob) return 0; } -static int note_lookup(git_note **out, git_repository *repo, - git_tree *tree, const char *target) +static int note_lookup( + git_note **out, git_repository *repo, git_tree *tree, const char *target) { int error, fanout = 0; git_oid oid; @@ -382,6 +388,7 @@ static int note_get_default_ref(const char **out, git_repository *repo) ret = git_config_get_string(out, cfg, "core.notesRef"); if (ret == GIT_ENOTFOUND) { + giterr_clear(); *out = GIT_NOTES_DEFAULT_REF; return 0; } @@ -432,12 +439,10 @@ int git_note_read(git_note **out, git_repository *repo, target = git_oid_allocfmt(oid); GITERR_CHECK_ALLOC(target); - if ((error = retrieve_note_tree_and_commit(&tree, &commit, repo, ¬es_ref)) < 0) - goto cleanup; - - error = note_lookup(out, repo, tree, target); + if (!(error = retrieve_note_tree_and_commit( + &tree, &commit, repo, ¬es_ref))) + error = note_lookup(out, repo, tree, target); -cleanup: git__free(target); git_tree_free(tree); git_commit_free(commit); @@ -489,13 +494,11 @@ int git_note_remove(git_repository *repo, const char *notes_ref, target = git_oid_allocfmt(oid); GITERR_CHECK_ALLOC(target); - if ((error = retrieve_note_tree_and_commit(&tree, &commit, repo, ¬es_ref)) < 0) - goto cleanup; + if (!(error = retrieve_note_tree_and_commit( + &tree, &commit, repo, ¬es_ref))) + error = note_remove( + repo, author, committer, notes_ref, tree, target, &commit); - error = note_remove(repo, author, committer, notes_ref, - tree, target, &commit); - -cleanup: git__free(target); git_commit_free(commit); git_tree_free(tree); @@ -533,7 +536,7 @@ static int process_entry_path( const char* entry_path, git_oid *annotated_object_id) { - int error = -1; + int error = 0; size_t i = 0, j = 0, len; git_buf buf = GIT_BUF_INIT; @@ -576,30 +579,30 @@ cleanup: } int git_note_foreach( - git_repository *repo, - const char *notes_ref, - git_note_foreach_cb note_cb, - void *payload) + git_repository *repo, + const char *notes_ref, + git_note_foreach_cb note_cb, + void *payload) { - int error; - git_note_iterator *iter = NULL; - git_oid note_id, annotated_id; + int error; + git_note_iterator *iter = NULL; + git_oid note_id, annotated_id; - if ((error = git_note_iterator_new(&iter, repo, notes_ref)) < 0) - return error; + if ((error = git_note_iterator_new(&iter, repo, notes_ref)) < 0) + return error; - while (!(error = git_note_next(¬e_id, &annotated_id, iter))) { - if (note_cb(¬e_id, &annotated_id, payload)) { - error = GIT_EUSER; - break; - } - } + while (!(error = git_note_next(¬e_id, &annotated_id, iter))) { + if (note_cb(¬e_id, &annotated_id, payload)) { + error = GIT_EUSER; + break; + } + } - if (error == GIT_ITEROVER) - error = 0; + if (error == GIT_ITEROVER) + error = 0; - git_note_iterator_free(iter); - return error; + git_note_iterator_free(iter); + return error; } @@ -644,18 +647,12 @@ int git_note_next( const git_index_entry *item; if ((error = git_iterator_current(&item, it)) < 0) - goto exit; + return error; - if (item != NULL) { - git_oid_cpy(note_id, &item->oid); - error = process_entry_path(item->path, annotated_id); + git_oid_cpy(note_id, &item->oid); - if (error >= 0) - error = git_iterator_advance(NULL, it); - } else { - error = GIT_ITEROVER; - } + if (!(error = process_entry_path(item->path, annotated_id))) + git_iterator_advance(NULL, it); -exit: return error; } diff --git a/src/object.c b/src/object.c index 80fe51152..9b8ccdd3e 100644 --- a/src/object.c +++ b/src/object.c @@ -18,65 +18,38 @@ static const int OBJECT_BASE_SIZE = 4096; -static struct { +typedef struct { const char *str; /* type name string */ - int loose; /* valid loose object type flag */ size_t size; /* size in bytes of the object structure */ -} git_objects_table[] = { + + int (*parse)(void *self, git_odb_object *obj); + void (*free)(void *self); +} git_object_def; + +static git_object_def git_objects_table[] = { /* 0 = GIT_OBJ__EXT1 */ - { "", 0, 0}, + { "", 0, NULL, NULL }, /* 1 = GIT_OBJ_COMMIT */ - { "commit", 1, sizeof(struct git_commit)}, + { "commit", sizeof(git_commit), git_commit__parse, git_commit__free }, /* 2 = GIT_OBJ_TREE */ - { "tree", 1, sizeof(struct git_tree) }, + { "tree", sizeof(git_tree), git_tree__parse, git_tree__free }, /* 3 = GIT_OBJ_BLOB */ - { "blob", 1, sizeof(struct git_blob) }, + { "blob", sizeof(git_blob), git_blob__parse, git_blob__free }, /* 4 = GIT_OBJ_TAG */ - { "tag", 1, sizeof(struct git_tag) }, + { "tag", sizeof(git_tag), git_tag__parse, git_tag__free }, /* 5 = GIT_OBJ__EXT2 */ - { "", 0, 0 }, - + { "", 0, NULL, NULL }, /* 6 = GIT_OBJ_OFS_DELTA */ - { "OFS_DELTA", 0, 0 }, - + { "OFS_DELTA", 0, NULL, NULL }, /* 7 = GIT_OBJ_REF_DELTA */ - { "REF_DELTA", 0, 0 } + { "REF_DELTA", 0, NULL, NULL }, }; -static int create_object(git_object **object_out, git_otype type) -{ - git_object *object = NULL; - - assert(object_out); - - *object_out = NULL; - - switch (type) { - case GIT_OBJ_COMMIT: - case GIT_OBJ_TAG: - case GIT_OBJ_BLOB: - case GIT_OBJ_TREE: - object = git__malloc(git_object__size(type)); - GITERR_CHECK_ALLOC(object); - memset(object, 0x0, git_object__size(type)); - break; - - default: - giterr_set(GITERR_INVALID, "The given type is invalid"); - return -1; - } - - object->type = type; - - *object_out = object; - return 0; -} - int git_object__from_odb_object( git_object **object_out, git_repository *repo, @@ -84,49 +57,55 @@ int git_object__from_odb_object( git_otype type) { int error; + size_t object_size; + git_object_def *def; git_object *object = NULL; - if (type != GIT_OBJ_ANY && type != odb_obj->raw.type) { - giterr_set(GITERR_INVALID, "The requested type does not match the type in the ODB"); + assert(object_out); + *object_out = NULL; + + /* Validate type match */ + if (type != GIT_OBJ_ANY && type != odb_obj->cached.type) { + giterr_set(GITERR_INVALID, + "The requested type does not match the type in the ODB"); return GIT_ENOTFOUND; } - type = odb_obj->raw.type; + if ((object_size = git_object__size(odb_obj->cached.type)) == 0) { + giterr_set(GITERR_INVALID, "The requested type is invalid"); + return GIT_ENOTFOUND; + } - if ((error = create_object(&object, type)) < 0) - return error; + /* Allocate and initialize base object */ + object = git__calloc(1, object_size); + GITERR_CHECK_ALLOC(object); - /* Initialize parent object */ git_oid_cpy(&object->cached.oid, &odb_obj->cached.oid); + object->cached.type = odb_obj->cached.type; + object->cached.size = odb_obj->cached.size; object->repo = repo; - switch (type) { - case GIT_OBJ_COMMIT: - error = git_commit__parse((git_commit *)object, odb_obj); - break; - - case GIT_OBJ_TREE: - error = git_tree__parse((git_tree *)object, odb_obj); - break; + /* Parse raw object data */ + def = &git_objects_table[odb_obj->cached.type]; + assert(def->free && def->parse); - case GIT_OBJ_TAG: - error = git_tag__parse((git_tag *)object, odb_obj); - break; + if ((error = def->parse(object, odb_obj)) < 0) + def->free(object); + else + *object_out = git_cache_store_parsed(&repo->objects, object); - case GIT_OBJ_BLOB: - error = git_blob__parse((git_blob *)object, odb_obj); - break; + return error; +} - default: - break; - } +void git_object__free(void *obj) +{ + git_otype type = ((git_object *)obj)->cached.type; - if (error < 0) - git_object__free(object); + if (type < 0 || ((size_t)type) >= ARRAY_SIZE(git_objects_table) || + !git_objects_table[type].free) + git__free(obj); else - *object_out = git_cache_try_store(&repo->objects, object); - - return error; + git_objects_table[type].free(obj); } int git_object_lookup_prefix( @@ -138,13 +117,15 @@ int git_object_lookup_prefix( { git_object *object = NULL; git_odb *odb = NULL; - git_odb_object *odb_obj; + git_odb_object *odb_obj = NULL; int error = 0; assert(repo && object_out && id); - if (len < GIT_OID_MINPREFIXLEN) + if (len < GIT_OID_MINPREFIXLEN) { + giterr_set(GITERR_OBJECT, "Ambiguous lookup - OID prefix is too short"); return GIT_EAMBIGUOUS; + } error = git_repository_odb__weakptr(&odb, repo); if (error < 0) @@ -154,27 +135,38 @@ int git_object_lookup_prefix( len = GIT_OID_HEXSZ; if (len == GIT_OID_HEXSZ) { + git_cached_obj *cached = NULL; + /* We want to match the full id : we can first look up in the cache, * since there is no need to check for non ambiguousity */ - object = git_cache_get(&repo->objects, id); - if (object != NULL) { - if (type != GIT_OBJ_ANY && type != object->type) { - git_object_free(object); - giterr_set(GITERR_INVALID, "The requested type does not match the type in ODB"); - return GIT_ENOTFOUND; + cached = git_cache_get_any(&repo->objects, id); + if (cached != NULL) { + if (cached->flags == GIT_CACHE_STORE_PARSED) { + object = (git_object *)cached; + + if (type != GIT_OBJ_ANY && type != object->cached.type) { + git_object_free(object); + giterr_set(GITERR_INVALID, + "The requested type does not match the type in ODB"); + return GIT_ENOTFOUND; + } + + *object_out = object; + return 0; + } else if (cached->flags == GIT_CACHE_STORE_RAW) { + odb_obj = (git_odb_object *)cached; + } else { + assert(!"Wrong caching type in the global object cache"); } - - *object_out = object; - return 0; + } else { + /* Object was not found in the cache, let's explore the backends. + * We could just use git_odb_read_unique_short_oid, + * it is the same cost for packed and loose object backends, + * but it may be much more costly for sqlite and hiredis. + */ + error = git_odb_read(&odb_obj, odb, id); } - - /* Object was not found in the cache, let's explore the backends. - * We could just use git_odb_read_unique_short_oid, - * it is the same cost for packed and loose object backends, - * but it may be much more costly for sqlite and hiredis. - */ - error = git_odb_read(&odb_obj, odb, id); } else { git_oid short_oid; @@ -211,41 +203,12 @@ int git_object_lookup(git_object **object_out, git_repository *repo, const git_o return git_object_lookup_prefix(object_out, repo, id, GIT_OID_HEXSZ, type); } -void git_object__free(void *_obj) -{ - git_object *object = (git_object *)_obj; - - assert(object); - - switch (object->type) { - case GIT_OBJ_COMMIT: - git_commit__free((git_commit *)object); - break; - - case GIT_OBJ_TREE: - git_tree__free((git_tree *)object); - break; - - case GIT_OBJ_TAG: - git_tag__free((git_tag *)object); - break; - - case GIT_OBJ_BLOB: - git_blob__free((git_blob *)object); - break; - - default: - git__free(object); - break; - } -} - void git_object_free(git_object *object) { if (object == NULL) return; - git_cached_obj_decref((git_cached_obj *)object, git_object__free); + git_cached_obj_decref(object); } const git_oid *git_object_id(const git_object *obj) @@ -257,7 +220,7 @@ const git_oid *git_object_id(const git_object *obj) git_otype git_object_type(const git_object *obj) { assert(obj); - return obj->type; + return obj->cached.type; } git_repository *git_object_owner(const git_object *obj) @@ -293,7 +256,7 @@ int git_object_typeisloose(git_otype type) if (type < 0 || ((size_t) type) >= ARRAY_SIZE(git_objects_table)) return 0; - return git_objects_table[type].loose; + return (git_objects_table[type].size > 0) ? 1 : 0; } size_t git_object__size(git_otype type) @@ -350,18 +313,17 @@ int git_object_peel( git_object *source, *deref = NULL; int error; - if (target_type != GIT_OBJ_TAG && - target_type != GIT_OBJ_COMMIT && - target_type != GIT_OBJ_TREE && - target_type != GIT_OBJ_BLOB && - target_type != GIT_OBJ_ANY) - return GIT_EINVALIDSPEC; - assert(object && peeled); if (git_object_type(object) == target_type) return git_object_dup(peeled, (git_object *)object); + assert(target_type == GIT_OBJ_TAG || + target_type == GIT_OBJ_COMMIT || + target_type == GIT_OBJ_TREE || + target_type == GIT_OBJ_BLOB || + target_type == GIT_OBJ_ANY); + source = (git_object *)object; while (!(error = dereference_object(&deref, source))) { diff --git a/src/object.h b/src/object.h index c1e50593c..d187c55b7 100644 --- a/src/object.h +++ b/src/object.h @@ -11,7 +11,6 @@ struct git_object { git_cached_obj cached; git_repository *repo; - git_otype type; }; /* fully free the object; internal method, DO NOT EXPORT */ diff --git a/src/object_api.c b/src/object_api.c new file mode 100644 index 000000000..838bba323 --- /dev/null +++ b/src/object_api.c @@ -0,0 +1,129 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#include "git2/object.h" + +#include "common.h" +#include "repository.h" + +#include "commit.h" +#include "tree.h" +#include "blob.h" +#include "tag.h" + +/** + * Blob + */ +int git_commit_lookup(git_commit **out, git_repository *repo, const git_oid *id) +{ + return git_object_lookup((git_object **)out, repo, id, GIT_OBJ_COMMIT); +} + +int git_commit_lookup_prefix(git_commit **out, git_repository *repo, const git_oid *id, size_t len) +{ + return git_object_lookup_prefix((git_object **)out, repo, id, len, GIT_OBJ_COMMIT); +} + +void git_commit_free(git_commit *obj) +{ + git_object_free((git_object *)obj); +} + +const git_oid *git_commit_id(const git_commit *obj) +{ + return git_object_id((const git_object *)obj); +} + +git_repository *git_commit_owner(const git_commit *obj) +{ + return git_object_owner((const git_object *)obj); +} + + +/** + * Tree + */ +int git_tree_lookup(git_tree **out, git_repository *repo, const git_oid *id) +{ + return git_object_lookup((git_object **)out, repo, id, GIT_OBJ_TREE); +} + +int git_tree_lookup_prefix(git_tree **out, git_repository *repo, const git_oid *id, size_t len) +{ + return git_object_lookup_prefix((git_object **)out, repo, id, len, GIT_OBJ_TREE); +} + +void git_tree_free(git_tree *obj) +{ + git_object_free((git_object *)obj); +} + +const git_oid *git_tree_id(const git_tree *obj) +{ + return git_object_id((const git_object *)obj); +} + +git_repository *git_tree_owner(const git_tree *obj) +{ + return git_object_owner((const git_object *)obj); +} + + +/** + * Tag + */ +int git_tag_lookup(git_tag **out, git_repository *repo, const git_oid *id) +{ + return git_object_lookup((git_object **)out, repo, id, GIT_OBJ_TAG); +} + +int git_tag_lookup_prefix(git_tag **out, git_repository *repo, const git_oid *id, size_t len) +{ + return git_object_lookup_prefix((git_object **)out, repo, id, len, GIT_OBJ_TAG); +} + +void git_tag_free(git_tag *obj) +{ + git_object_free((git_object *)obj); +} + +const git_oid *git_tag_id(const git_tag *obj) +{ + return git_object_id((const git_object *)obj); +} + +git_repository *git_tag_owner(const git_tag *obj) +{ + return git_object_owner((const git_object *)obj); +} + +/** + * Blob + */ +int git_blob_lookup(git_blob **out, git_repository *repo, const git_oid *id) +{ + return git_object_lookup((git_object **)out, repo, id, GIT_OBJ_BLOB); +} + +int git_blob_lookup_prefix(git_blob **out, git_repository *repo, const git_oid *id, size_t len) +{ + return git_object_lookup_prefix((git_object **)out, repo, id, len, GIT_OBJ_BLOB); +} + +void git_blob_free(git_blob *obj) +{ + git_object_free((git_object *)obj); +} + +const git_oid *git_blob_id(const git_blob *obj) +{ + return git_object_id((const git_object *)obj); +} + +git_repository *git_blob_owner(const git_blob *obj) +{ + return git_object_owner((const git_object *)obj); +} @@ -8,11 +8,13 @@ #include "common.h" #include <zlib.h> #include "git2/object.h" +#include "git2/sys/odb_backend.h" #include "fileops.h" #include "hash.h" #include "odb.h" #include "delta-apply.h" #include "filter.h" +#include "repository.h" #include "git2/odb_backend.h" #include "git2/oid.h" @@ -29,10 +31,19 @@ typedef struct { git_odb_backend *backend; int priority; - int is_alternate; + bool is_alternate; + ino_t disk_inode; } backend_internal; -size_t git_odb__cache_size = GIT_DEFAULT_CACHE_SIZE; +static git_cache *odb_cache(git_odb *odb) +{ + if (odb->rc.owner != NULL) { + git_repository *owner = odb->rc.owner; + return &owner->objects; + } + + return &odb->own_cache; +} static int load_alternates(git_odb *odb, const char *objects_dir, int alternate_depth); @@ -54,6 +65,7 @@ int git_odb__hashobj(git_oid *id, git_rawobj *obj) if (!git_object_typeisloose(obj->type)) return -1; + if (!obj->data && obj->len != 0) return -1; @@ -70,23 +82,24 @@ int git_odb__hashobj(git_oid *id, git_rawobj *obj) } -static git_odb_object *new_odb_object(const git_oid *oid, git_rawobj *source) +static git_odb_object *odb_object__alloc(const git_oid *oid, git_rawobj *source) { - git_odb_object *object = git__malloc(sizeof(git_odb_object)); - memset(object, 0x0, sizeof(git_odb_object)); + git_odb_object *object = git__calloc(1, sizeof(git_odb_object)); - git_oid_cpy(&object->cached.oid, oid); - memcpy(&object->raw, source, sizeof(git_rawobj)); + if (object != NULL) { + git_oid_cpy(&object->cached.oid, oid); + object->cached.type = source->type; + object->cached.size = source->len; + object->buffer = source->data; + } return object; } -static void free_odb_object(void *o) +void git_odb_object__free(void *object) { - git_odb_object *object = (git_odb_object *)o; - if (object != NULL) { - git__free(object->raw.data); + git__free(((git_odb_object *)object)->buffer); git__free(object); } } @@ -98,17 +111,17 @@ const git_oid *git_odb_object_id(git_odb_object *object) const void *git_odb_object_data(git_odb_object *object) { - return object->raw.data; + return object->buffer; } size_t git_odb_object_size(git_odb_object *object) { - return object->raw.len; + return object->cached.size; } git_otype git_odb_object_type(git_odb_object *object) { - return object->raw.type; + return object->cached.type; } void git_odb_object_free(git_odb_object *object) @@ -116,7 +129,7 @@ void git_odb_object_free(git_odb_object *object) if (object == NULL) return; - git_cached_obj_decref((git_cached_obj *)object, &free_odb_object); + git_cached_obj_decref(object); } int git_odb__hashfd(git_oid *out, git_file fd, size_t size, git_otype type) @@ -219,6 +232,7 @@ int git_odb__hashlink(git_oid *out, const char *path) link_data[size] = '\0'; if (read_len != (ssize_t)size) { giterr_set(GITERR_OS, "Failed to read symlink data for '%s'", path); + git__free(link_data); return -1; } @@ -353,9 +367,8 @@ int git_odb_new(git_odb **out) git_odb *db = git__calloc(1, sizeof(*db)); GITERR_CHECK_ALLOC(db); - if (git_cache_init(&db->cache, git_odb__cache_size, &free_odb_object) < 0 || - git_vector_init(&db->backends, 4, backend_sort_cmp) < 0) - { + if (git_cache_init(&db->own_cache) < 0 || + git_vector_init(&db->backends, 4, backend_sort_cmp) < 0) { git__free(db); return -1; } @@ -365,7 +378,9 @@ int git_odb_new(git_odb **out) return 0; } -static int add_backend_internal(git_odb *odb, git_odb_backend *backend, int priority, int is_alternate) +static int add_backend_internal( + git_odb *odb, git_odb_backend *backend, + int priority, bool is_alternate, ino_t disk_inode) { backend_internal *internal; @@ -382,6 +397,7 @@ static int add_backend_internal(git_odb *odb, git_odb_backend *backend, int prio internal->backend = backend; internal->priority = priority; internal->is_alternate = is_alternate; + internal->disk_inode = disk_inode; if (git_vector_insert(&odb->backends, internal) < 0) { git__free(internal); @@ -395,26 +411,86 @@ static int add_backend_internal(git_odb *odb, git_odb_backend *backend, int prio int git_odb_add_backend(git_odb *odb, git_odb_backend *backend, int priority) { - return add_backend_internal(odb, backend, priority, 0); + return add_backend_internal(odb, backend, priority, false, 0); } int git_odb_add_alternate(git_odb *odb, git_odb_backend *backend, int priority) { - return add_backend_internal(odb, backend, priority, 1); + return add_backend_internal(odb, backend, priority, true, 0); +} + +size_t git_odb_num_backends(git_odb *odb) +{ + assert(odb); + return odb->backends.length; +} + +static int git_odb__error_unsupported_in_backend(const char *action) +{ + giterr_set(GITERR_ODB, + "Cannot %s - unsupported in the loaded odb backends", action); + return -1; +} + + +int git_odb_get_backend(git_odb_backend **out, git_odb *odb, size_t pos) +{ + backend_internal *internal; + + assert(odb && odb); + internal = git_vector_get(&odb->backends, pos); + + if (internal && internal->backend) { + *out = internal->backend; + return 0; + } + + giterr_set(GITERR_ODB, "No ODB backend loaded at index " PRIuZ, pos); + return GIT_ENOTFOUND; } -static int add_default_backends(git_odb *db, const char *objects_dir, int as_alternates, int alternate_depth) +static int add_default_backends( + git_odb *db, const char *objects_dir, + bool as_alternates, int alternate_depth) { + size_t i; + struct stat st; + ino_t inode; git_odb_backend *loose, *packed; + /* TODO: inodes are not really relevant on Win32, so we need to find + * a cross-platform workaround for this */ +#ifdef GIT_WIN32 + GIT_UNUSED(i); + GIT_UNUSED(st); + + inode = 0; +#else + if (p_stat(objects_dir, &st) < 0) { + if (as_alternates) + return 0; + + giterr_set(GITERR_ODB, "Failed to load object database in '%s'", objects_dir); + return -1; + } + + inode = st.st_ino; + + for (i = 0; i < db->backends.length; ++i) { + backend_internal *backend = git_vector_get(&db->backends, i); + if (backend->disk_inode == inode) + return 0; + } +#endif + /* add the loose object backend */ if (git_odb_backend_loose(&loose, objects_dir, -1, 0) < 0 || - add_backend_internal(db, loose, GIT_LOOSE_PRIORITY, as_alternates) < 0) + add_backend_internal(db, loose, GIT_LOOSE_PRIORITY, as_alternates, inode) < 0) return -1; /* add the packed file backend */ if (git_odb_backend_pack(&packed, objects_dir) < 0 || - add_backend_internal(db, packed, GIT_PACKED_PRIORITY, as_alternates) < 0) + add_backend_internal(db, packed, GIT_PACKED_PRIORITY, as_alternates, inode) < 0) return -1; return load_alternates(db, objects_dir, alternate_depth); @@ -429,9 +505,8 @@ static int load_alternates(git_odb *odb, const char *objects_dir, int alternate_ int result = 0; /* Git reports an error, we just ignore anything deeper */ - if (alternate_depth > GIT_ALTERNATES_MAX_DEPTH) { + if (alternate_depth > GIT_ALTERNATES_MAX_DEPTH) return 0; - } if (git_buf_joinpath(&alternates_path, objects_dir, GIT_ALTERNATES_FILE) < 0) return -1; @@ -464,7 +539,7 @@ static int load_alternates(git_odb *odb, const char *objects_dir, int alternate_ alternate = git_buf_cstr(&alternates_path); } - if ((result = add_default_backends(odb, alternate, 1, alternate_depth + 1)) < 0) + if ((result = add_default_backends(odb, alternate, true, alternate_depth + 1)) < 0) break; } @@ -476,7 +551,7 @@ static int load_alternates(git_odb *odb, const char *objects_dir, int alternate_ int git_odb_add_disk_alternate(git_odb *odb, const char *path) { - return add_default_backends(odb, path, 1, 0); + return add_default_backends(odb, path, true, 0); } int git_odb_open(git_odb **out, const char *objects_dir) @@ -514,7 +589,9 @@ static void odb_free(git_odb *db) } git_vector_free(&db->backends); - git_cache_free(&db->cache); + git_cache_free(&db->own_cache); + + git__memzero(db, sizeof(*db)); git__free(db); } @@ -535,7 +612,7 @@ int git_odb_exists(git_odb *db, const git_oid *id) assert(db && id); - if ((object = git_cache_get(&db->cache, id)) != NULL) { + if ((object = git_cache_get_raw(odb_cache(db), id)) != NULL) { git_odb_object_free(object); return (int)true; } @@ -585,9 +662,9 @@ int git_odb__read_header_or_object( assert(db && id && out && len_p && type_p); - if ((object = git_cache_get(&db->cache, id)) != NULL) { - *len_p = object->raw.len; - *type_p = object->raw.type; + if ((object = git_cache_get_raw(odb_cache(db), id)) != NULL) { + *len_p = object->cached.size; + *type_p = object->cached.type; *out = object; return 0; } @@ -612,8 +689,8 @@ int git_odb__read_header_or_object( if ((error = git_odb_read(&object, db, id)) < 0) return error; /* error already set - pass along */ - *len_p = object->raw.len; - *type_p = object->raw.type; + *len_p = object->cached.size; + *type_p = object->cached.type; *out = object; return 0; @@ -621,19 +698,15 @@ int git_odb__read_header_or_object( int git_odb_read(git_odb_object **out, git_odb *db, const git_oid *id) { - size_t i; + size_t i, reads = 0; int error; bool refreshed = false; git_rawobj raw; + git_odb_object *object; assert(out && db && id); - if (db->backends.length == 0) { - giterr_set(GITERR_ODB, "Failed to lookup object: no backends loaded"); - return GIT_ENOTFOUND; - } - - *out = git_cache_get(&db->cache, id); + *out = git_cache_get_raw(odb_cache(db), id); if (*out != NULL) return 0; @@ -644,8 +717,10 @@ attempt_lookup: backend_internal *internal = git_vector_get(&db->backends, i); git_odb_backend *b = internal->backend; - if (b->read != NULL) + if (b->read != NULL) { + ++reads; error = b->read(&raw.data, &raw.len, &raw.type, b, id); + } } if (error == GIT_ENOTFOUND && !refreshed) { @@ -656,10 +731,16 @@ attempt_lookup: goto attempt_lookup; } - if (error && error != GIT_PASSTHROUGH) + if (error && error != GIT_PASSTHROUGH) { + if (!reads) + return git_odb__error_notfound("no match for id", id); return error; + } + + if ((object = odb_object__alloc(id, &raw)) == NULL) + return -1; - *out = git_cache_try_store(&db->cache, new_odb_object(id, &raw)); + *out = git_cache_store_raw(odb_cache(db), object); return 0; } @@ -672,6 +753,7 @@ int git_odb_read_prefix( git_rawobj raw; void *data = NULL; bool found = false, refreshed = false; + git_odb_object *object; assert(out && db); @@ -682,7 +764,7 @@ int git_odb_read_prefix( len = GIT_OID_HEXSZ; if (len == GIT_OID_HEXSZ) { - *out = git_cache_get(&db->cache, short_id); + *out = git_cache_get_raw(odb_cache(db), short_id); if (*out != NULL) return 0; } @@ -704,7 +786,7 @@ attempt_lookup: git__free(data); data = raw.data; - if (found && git_oid_cmp(&full_oid, &found_full_oid)) + if (found && git_oid__cmp(&full_oid, &found_full_oid)) return git_odb__error_ambiguous("multiple matches for prefix"); found_full_oid = full_oid; @@ -723,7 +805,10 @@ attempt_lookup: if (!found) return git_odb__error_notfound("no match for prefix", short_id); - *out = git_cache_try_store(&db->cache, new_odb_object(&found_full_oid, &raw)); + if ((object = odb_object__alloc(&found_full_oid, &raw)) == NULL) + return -1; + + *out = git_cache_store_raw(odb_cache(db), object); return 0; } @@ -770,10 +855,10 @@ int git_odb_write( if (!error || error == GIT_PASSTHROUGH) return 0; - /* if no backends were able to write the object directly, we try a streaming - * write to the backends; just write the whole object into the stream in one - * push */ - + /* if no backends were able to write the object directly, we try a + * streaming write to the backends; just write the whole object into the + * stream in one push + */ if ((error = git_odb_open_wstream(&stream, db, len, type)) != 0) return error; @@ -787,7 +872,7 @@ int git_odb_write( int git_odb_open_wstream( git_odb_stream **stream, git_odb *db, size_t size, git_otype type) { - size_t i; + size_t i, writes = 0; int error = GIT_ERROR; assert(stream && db); @@ -800,21 +885,26 @@ int git_odb_open_wstream( if (internal->is_alternate) continue; - if (b->writestream != NULL) + if (b->writestream != NULL) { + ++writes; error = b->writestream(stream, b, size, type); - else if (b->write != NULL) + } else if (b->write != NULL) { + ++writes; error = init_fake_wstream(stream, b, size, type); + } } if (error == GIT_PASSTHROUGH) error = 0; + if (error < 0 && !writes) + error = git_odb__error_unsupported_in_backend("write object"); return error; } int git_odb_open_rstream(git_odb_stream **stream, git_odb *db, const git_oid *oid) { - size_t i; + size_t i, reads = 0; int error = GIT_ERROR; assert(stream && db); @@ -823,19 +913,23 @@ int git_odb_open_rstream(git_odb_stream **stream, git_odb *db, const git_oid *oi backend_internal *internal = git_vector_get(&db->backends, i); git_odb_backend *b = internal->backend; - if (b->readstream != NULL) + if (b->readstream != NULL) { + ++reads; error = b->readstream(stream, b, oid); + } } if (error == GIT_PASSTHROUGH) error = 0; + if (error < 0 && !reads) + error = git_odb__error_unsupported_in_backend("read object streamed"); return error; } int git_odb_write_pack(struct git_odb_writepack **out, git_odb *db, git_transfer_progress_callback progress_cb, void *progress_payload) { - size_t i; + size_t i, writes = 0; int error = GIT_ERROR; assert(out && db); @@ -848,12 +942,16 @@ int git_odb_write_pack(struct git_odb_writepack **out, git_odb *db, git_transfer if (internal->is_alternate) continue; - if (b->writepack != NULL) + if (b->writepack != NULL) { + ++writes; error = b->writepack(out, b, progress_cb, progress_payload); + } } if (error == GIT_PASSTHROUGH) error = 0; + if (error < 0 && !writes) + error = git_odb__error_unsupported_in_backend("write pack"); return error; } @@ -29,14 +29,14 @@ typedef struct { /* EXPORT */ struct git_odb_object { git_cached_obj cached; - git_rawobj raw; + void *buffer; }; /* EXPORT */ struct git_odb { git_refcount rc; git_vector backends; - git_cache cache; + git_cache own_cache; }; /* @@ -96,4 +96,7 @@ int git_odb__read_header_or_object( git_odb_object **out, size_t *len_p, git_otype *type_p, git_odb *db, const git_oid *id); +/* fully free the object; internal method, DO NOT EXPORT */ +void git_odb_object__free(void *object); + #endif diff --git a/src/odb_loose.c b/src/odb_loose.c index 68083f7fd..76ed8e232 100644 --- a/src/odb_loose.c +++ b/src/odb_loose.c @@ -8,7 +8,7 @@ #include "common.h" #include <zlib.h> #include "git2/object.h" -#include "git2/oid.h" +#include "git2/sys/odb_backend.h" #include "fileops.h" #include "hash.h" #include "odb.h" @@ -33,7 +33,9 @@ typedef struct loose_backend { int object_zlib_level; /** loose object zlib compression level. */ int fsync_object_files; /** loose object file fsync flag. */ - char *objects_dir; + + size_t objects_dirlen; + char objects_dir[GIT_FLEX_ARRAY]; } loose_backend; /* State structure for exploring directories, @@ -56,24 +58,30 @@ typedef struct { * ***********************************************************/ -static int object_file_name(git_buf *name, const char *dir, const git_oid *id) +static int object_file_name( + git_buf *name, const loose_backend *be, const git_oid *id) { - git_buf_sets(name, dir); - - /* expand length for 40 hex sha1 chars + 2 * '/' + '\0' */ - if (git_buf_grow(name, git_buf_len(name) + GIT_OID_HEXSZ + 3) < 0) + /* expand length for object root + 40 hex sha1 chars + 2 * '/' + '\0' */ + if (git_buf_grow(name, be->objects_dirlen + GIT_OID_HEXSZ + 3) < 0) return -1; + git_buf_set(name, be->objects_dir, be->objects_dirlen); git_path_to_dir(name); /* loose object filename: aa/aaa... (41 bytes) */ - git_oid_pathfmt(name->ptr + git_buf_len(name), id); + git_oid_pathfmt(name->ptr + name->size, id); name->size += GIT_OID_HEXSZ + 1; name->ptr[name->size] = '\0'; return 0; } +static int object_mkdir(const git_buf *name, const loose_backend *be) +{ + return git_futils_mkdir( + name->ptr + be->objects_dirlen, be->objects_dir, GIT_OBJECT_DIR_MODE, + GIT_MKDIR_PATH | GIT_MKDIR_SKIP_LAST | GIT_MKDIR_VERIFY_DIR); +} static size_t get_binary_object_header(obj_hdr *hdr, git_buf *obj) { @@ -457,7 +465,7 @@ static int locate_object( loose_backend *backend, const git_oid *oid) { - int error = object_file_name(object_location, backend->objects_dir, oid); + int error = object_file_name(object_location, backend, oid); if (!error && !git_path_exists(object_location->ptr)) return GIT_ENOTFOUND; @@ -769,8 +777,8 @@ static int loose_backend__stream_fwrite(git_oid *oid, git_odb_stream *_stream) int error = 0; if (git_filebuf_hash(oid, &stream->fbuf) < 0 || - object_file_name(&final_path, backend->objects_dir, oid) < 0 || - git_futils_mkpath2file(final_path.ptr, GIT_OBJECT_DIR_MODE) < 0) + object_file_name(&final_path, backend, oid) < 0 || + object_mkdir(&final_path, backend) < 0) error = -1; /* * Don't try to add an existing object to the repository. This @@ -880,8 +888,8 @@ static int loose_backend__write(git_oid *oid, git_odb_backend *_backend, const v git_filebuf_write(&fbuf, header, header_len); git_filebuf_write(&fbuf, data, len); - if (object_file_name(&final_path, backend->objects_dir, oid) < 0 || - git_futils_mkpath2file(final_path.ptr, GIT_OBJECT_DIR_MODE) < 0 || + if (object_file_name(&final_path, backend, oid) < 0 || + object_mkdir(&final_path, backend) < 0 || git_filebuf_commit_at(&fbuf, final_path.ptr, GIT_OBJECT_FILE_MODE) < 0) error = -1; @@ -898,7 +906,6 @@ static void loose_backend__free(git_odb_backend *_backend) assert(_backend); backend = (loose_backend *)_backend; - git__free(backend->objects_dir); git__free(backend); } @@ -909,13 +916,20 @@ int git_odb_backend_loose( int do_fsync) { loose_backend *backend; + size_t objects_dirlen; + + assert(backend_out && objects_dir); + + objects_dirlen = strlen(objects_dir); - backend = git__calloc(1, sizeof(loose_backend)); + backend = git__calloc(1, sizeof(loose_backend) + objects_dirlen + 2); GITERR_CHECK_ALLOC(backend); backend->parent.version = GIT_ODB_BACKEND_VERSION; - backend->objects_dir = git__strdup(objects_dir); - GITERR_CHECK_ALLOC(backend->objects_dir); + backend->objects_dirlen = objects_dirlen; + memcpy(backend->objects_dir, objects_dir, objects_dirlen); + if (backend->objects_dir[backend->objects_dirlen - 1] != '/') + backend->objects_dir[backend->objects_dirlen++] = '/'; if (compression_level < 0) compression_level = Z_BEST_SPEED; diff --git a/src/odb_pack.c b/src/odb_pack.c index 7240a4ac7..43880612a 100644 --- a/src/odb_pack.c +++ b/src/odb_pack.c @@ -8,7 +8,8 @@ #include "common.h" #include <zlib.h> #include "git2/repository.h" -#include "git2/oid.h" +#include "git2/indexer.h" +#include "git2/sys/odb_backend.h" #include "fileops.h" #include "hash.h" #include "odb.h" @@ -206,7 +207,7 @@ static int packfile_load__cb(void *_data, git_buf *path) return 0; } - error = git_packfile_check(&pack, path->ptr); + error = git_packfile_alloc(&pack, path->ptr); if (error == GIT_ENOTFOUND) /* ignore missing .pack file as git does */ return 0; @@ -258,23 +259,26 @@ static int pack_entry_find(struct git_pack_entry *e, struct pack_backend *backen return git_odb__error_notfound("failed to find pack entry", oid); } -static unsigned pack_entry_find_prefix_inner( - struct git_pack_entry *e, - struct pack_backend *backend, - const git_oid *short_oid, - size_t len, - struct git_pack_file *last_found) +static int pack_entry_find_prefix( + struct git_pack_entry *e, + struct pack_backend *backend, + const git_oid *short_oid, + size_t len) { int error; size_t i; - unsigned found = 0; + git_oid found_full_oid = {{0}}; + bool found = false; + struct git_pack_file *last_found = backend->last_found; if (last_found) { error = git_pack_entry_find(e, last_found, short_oid, len); if (error == GIT_EAMBIGUOUS) return error; - if (!error) - found = 1; + if (!error) { + git_oid_cpy(&found_full_oid, &e->sha1); + found = true; + } } for (i = 0; i < backend->packs.length; ++i) { @@ -288,28 +292,16 @@ static unsigned pack_entry_find_prefix_inner( if (error == GIT_EAMBIGUOUS) return error; if (!error) { - if (++found > 1) - break; + if (found && git_oid_cmp(&e->sha1, &found_full_oid)) + return git_odb__error_ambiguous("found multiple pack entries"); + git_oid_cpy(&found_full_oid, &e->sha1); + found = true; backend->last_found = p; } } - return found; -} - -static int pack_entry_find_prefix( - struct git_pack_entry *e, - struct pack_backend *backend, - const git_oid *short_oid, - size_t len) -{ - struct git_pack_file *last_found = backend->last_found; - unsigned int found = pack_entry_find_prefix_inner(e, backend, short_oid, len, last_found); - if (!found) return git_odb__error_notfound("no matching pack entry for prefix", short_oid); - else if (found > 1) - return git_odb__error_ambiguous("found multiple pack entries"); else return 0; } @@ -526,80 +518,75 @@ static void pack_backend__free(git_odb_backend *_backend) git__free(backend); } -int git_odb_backend_one_pack(git_odb_backend **backend_out, const char *idx) +static int pack_backend__alloc(struct pack_backend **out, size_t initial_size) { - struct pack_backend *backend = NULL; - struct git_pack_file *packfile = NULL; + struct pack_backend *backend = git__calloc(1, sizeof(struct pack_backend)); + GITERR_CHECK_ALLOC(backend); - if (git_packfile_check(&packfile, idx) < 0) + if (git_vector_init(&backend->packs, initial_size, packfile_sort__cb) < 0) { + git__free(backend); return -1; + } - backend = git__calloc(1, sizeof(struct pack_backend)); - GITERR_CHECK_ALLOC(backend); backend->parent.version = GIT_ODB_BACKEND_VERSION; - if (git_vector_init(&backend->packs, 1, NULL) < 0) - goto on_error; - - if (git_vector_insert(&backend->packs, packfile) < 0) - goto on_error; - backend->parent.read = &pack_backend__read; backend->parent.read_prefix = &pack_backend__read_prefix; backend->parent.read_header = &pack_backend__read_header; backend->parent.exists = &pack_backend__exists; backend->parent.refresh = &pack_backend__refresh; backend->parent.foreach = &pack_backend__foreach; + backend->parent.writepack = &pack_backend__writepack; backend->parent.free = &pack_backend__free; - *backend_out = (git_odb_backend *)backend; - + *out = backend; return 0; - -on_error: - git_vector_free(&backend->packs); - git__free(backend); - git__free(packfile); - return -1; } -int git_odb_backend_pack(git_odb_backend **backend_out, const char *objects_dir) +int git_odb_backend_one_pack(git_odb_backend **backend_out, const char *idx) { struct pack_backend *backend = NULL; - git_buf path = GIT_BUF_INIT; + struct git_pack_file *packfile = NULL; - backend = git__calloc(1, sizeof(struct pack_backend)); - GITERR_CHECK_ALLOC(backend); - backend->parent.version = GIT_ODB_BACKEND_VERSION; + if (pack_backend__alloc(&backend, 1) < 0) + return -1; - if (git_vector_init(&backend->packs, 8, packfile_sort__cb) < 0 || - git_buf_joinpath(&path, objects_dir, "pack") < 0) + if (git_packfile_alloc(&packfile, idx) < 0 || + git_vector_insert(&backend->packs, packfile) < 0) { - git__free(backend); + pack_backend__free((git_odb_backend *)backend); return -1; } - if (git_path_isdir(git_buf_cstr(&path)) == true) { - int error; + *backend_out = (git_odb_backend *)backend; + return 0; +} + +int git_odb_backend_pack(git_odb_backend **backend_out, const char *objects_dir) +{ + int error = 0; + struct pack_backend *backend = NULL; + git_buf path = GIT_BUF_INIT; + + if (pack_backend__alloc(&backend, 8) < 0) + return -1; + if (!(error = git_buf_joinpath(&path, objects_dir, "pack")) && + git_path_isdir(git_buf_cstr(&path))) + { backend->pack_folder = git_buf_detach(&path); + error = pack_backend__refresh((git_odb_backend *)backend); - if (error < 0) - return error; } - backend->parent.read = &pack_backend__read; - backend->parent.read_prefix = &pack_backend__read_prefix; - backend->parent.read_header = &pack_backend__read_header; - backend->parent.exists = &pack_backend__exists; - backend->parent.refresh = &pack_backend__refresh; - backend->parent.foreach = &pack_backend__foreach; - backend->parent.writepack = &pack_backend__writepack; - backend->parent.free = &pack_backend__free; + if (error < 0) { + pack_backend__free((git_odb_backend *)backend); + backend = NULL; + } *backend_out = (git_odb_backend *)backend; git_buf_free(&path); - return 0; + return error; } @@ -68,12 +68,31 @@ GIT_INLINE(char) *fmt_one(char *str, unsigned int val) return str; } -void git_oid_fmt(char *str, const git_oid *oid) +void git_oid_nfmt(char *str, size_t n, const git_oid *oid) { - size_t i; + size_t i, max_i; + + if (!oid) { + memset(str, 0, n); + return; + } + if (n > GIT_OID_HEXSZ) { + memset(&str[GIT_OID_HEXSZ], 0, n - GIT_OID_HEXSZ); + n = GIT_OID_HEXSZ; + } + + max_i = n / 2; - for (i = 0; i < sizeof(oid->id); i++) + for (i = 0; i < max_i; i++) str = fmt_one(str, oid->id[i]); + + if (n & 1) + *str++ = to_hex[oid->id[i] >> 4]; +} + +void git_oid_fmt(char *str, const git_oid *oid) +{ + git_oid_nfmt(str, GIT_OID_HEXSZ, oid); } void git_oid_pathfmt(char *str, const git_oid *oid) @@ -91,31 +110,20 @@ char *git_oid_allocfmt(const git_oid *oid) char *str = git__malloc(GIT_OID_HEXSZ + 1); if (!str) return NULL; - git_oid_fmt(str, oid); - str[GIT_OID_HEXSZ] = '\0'; + git_oid_nfmt(str, GIT_OID_HEXSZ + 1, oid); return str; } char *git_oid_tostr(char *out, size_t n, const git_oid *oid) { - char str[GIT_OID_HEXSZ]; - if (!out || n == 0) return ""; - n--; /* allow room for terminating NUL */ - - if (oid == NULL) - n = 0; - - if (n > 0) { - git_oid_fmt(str, oid); - if (n > GIT_OID_HEXSZ) - n = GIT_OID_HEXSZ; - memcpy(out, str, n); - } + if (n > GIT_OID_HEXSZ + 1) + n = GIT_OID_HEXSZ + 1; - out[n] = '\0'; + git_oid_nfmt(out, n - 1, oid); /* allow room for terminating NUL */ + out[n - 1] = '\0'; return out; } @@ -166,18 +174,26 @@ void git_oid_cpy(git_oid *out, const git_oid *src) memcpy(out->id, src->id, sizeof(out->id)); } +int git_oid_cmp(const git_oid *a, const git_oid *b) +{ + return git_oid__cmp(a, b); +} + int git_oid_ncmp(const git_oid *oid_a, const git_oid *oid_b, size_t len) { const unsigned char *a = oid_a->id; const unsigned char *b = oid_b->id; - do { + if (len > GIT_OID_HEXSZ) + len = GIT_OID_HEXSZ; + + while (len > 1) { if (*a != *b) return 1; a++; b++; len -= 2; - } while (len > 1); + }; if (len) if ((*a ^ *b) & 0xf0) @@ -186,14 +202,31 @@ int git_oid_ncmp(const git_oid *oid_a, const git_oid *oid_b, size_t len) return 0; } -int git_oid_streq(const git_oid *a, const char *str) +int git_oid_strcmp(const git_oid *oid_a, const char *str) { - git_oid id; + const unsigned char *a = oid_a->id; + unsigned char strval; + int hexval; - if (git_oid_fromstr(&id, str) < 0) - return -1; + for (a = oid_a->id; *str && (a - oid_a->id) < GIT_OID_RAWSZ; ++a) { + if ((hexval = git__fromhex(*str++)) < 0) + return -1; + strval = hexval << 4; + if (*str) { + if ((hexval = git__fromhex(*str++)) < 0) + return -1; + strval |= hexval; + } + if (*a != strval) + return (*a - strval); + } - return git_oid_cmp(a, &id) == 0 ? 0 : -1; + return 0; +} + +int git_oid_streq(const git_oid *oid_a, const char *str) +{ + return git_oid_strcmp(oid_a, str) == 0 ? 0 : -1; } int git_oid_iszero(const git_oid *oid_a) @@ -244,8 +277,10 @@ static trie_node *push_leaf(git_oid_shorten *os, node_index idx, int push_at, co idx_leaf = (node_index)os->node_count++; - if (os->node_count == SHRT_MAX) + if (os->node_count == SHRT_MAX) { os->full = 1; + return NULL; + } node = &os->nodes[idx]; node->children[push_at] = -idx_leaf; diff --git a/src/oid.h b/src/oid.h new file mode 100644 index 000000000..077d0a4c8 --- /dev/null +++ b/src/oid.h @@ -0,0 +1,33 @@ +/* + * 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_oid_h__ +#define INCLUDE_oid_h__ + +#include "git2/oid.h" + +/* + * Compare two oid structures. + * + * @param a first oid structure. + * @param b second oid structure. + * @return <0, 0, >0 if a < b, a == b, a > b. + */ +GIT_INLINE(int) git_oid__cmp(const git_oid *a, const git_oid *b) +{ + const unsigned char *sha1 = a->id; + const unsigned char *sha2 = b->id; + int i; + + for (i = 0; i < GIT_OID_RAWSZ; i++, sha1++, sha2++) { + if (*sha1 != *sha2) + return *sha1 - *sha2; + } + + return 0; +} + +#endif diff --git a/src/oidmap.h b/src/oidmap.h index 40274cd19..a29c7cd35 100644 --- a/src/oidmap.h +++ b/src/oidmap.h @@ -19,17 +19,15 @@ __KHASH_TYPE(oid, const git_oid *, void *); typedef khash_t(oid) git_oidmap; -GIT_INLINE(khint_t) hash_git_oid(const git_oid *oid) +GIT_INLINE(khint_t) git_oidmap_hash(const git_oid *oid) { - int i; - khint_t h = 0; - for (i = 0; i < 20; ++i) - h = (h << 5) - h + oid->id[i]; + khint_t h; + memcpy(&h, oid, sizeof(khint_t)); return h; } #define GIT__USE_OIDMAP \ - __KHASH_IMPL(oid, static kh_inline, const git_oid *, void *, 1, hash_git_oid, git_oid_equal) + __KHASH_IMPL(oid, static kh_inline, const git_oid *, void *, 1, git_oidmap_hash, git_oid_equal) #define git_oidmap_alloc() kh_init(oid) #define git_oidmap_free(h) kh_destroy(oid,h), h = NULL diff --git a/src/pack-objects.c b/src/pack-objects.c index 459201f58..7f427e3bd 100644 --- a/src/pack-objects.c +++ b/src/pack-objects.c @@ -33,6 +33,11 @@ struct tree_walk_context { git_buf buf; }; +struct pack_write_context { + git_indexer_stream *indexer; + git_transfer_progress *stats; +}; + #ifdef GIT_THREADS #define GIT_PACKBUILDER__MUTEX_OP(pb, mtx, op) do { \ @@ -127,7 +132,10 @@ int git_packbuilder_new(git_packbuilder **out, git_repository *repo) if (git_mutex_init(&pb->cache_mutex) || git_mutex_init(&pb->progress_mutex) || git_cond_init(&pb->progress_cond)) + { + giterr_set(GITERR_OS, "Failed to initialize packbuilder mutex"); goto on_error; + } #endif @@ -497,8 +505,10 @@ static git_pobject **compute_write_order(git_packbuilder *pb) /* * Mark objects that are at the tip of tags. */ - if (git_tag_foreach(pb->repo, &cb_tag_foreach, pb) < 0) + if (git_tag_foreach(pb->repo, &cb_tag_foreach, pb) < 0) { + git__free(wo); return NULL; + } /* * Give the objects in the original recency order until @@ -620,26 +630,6 @@ static int write_pack_buf(void *buf, size_t size, void *data) return git_buf_put(b, buf, size); } -static int write_pack_to_file(void *buf, size_t size, void *data) -{ - git_filebuf *file = (git_filebuf *)data; - return git_filebuf_write(file, buf, size); -} - -static int write_pack_file(git_packbuilder *pb, const char *path) -{ - git_filebuf file = GIT_FILEBUF_INIT; - - if (git_filebuf_open(&file, path, 0) < 0 || - write_pack(pb, &write_pack_to_file, &file) < 0 || - git_filebuf_commit(&file, GIT_PACK_FILE_MODE) < 0) { - git_filebuf_cleanup(&file); - return -1; - } - - return 0; -} - static int type_size_sort(const void *_a, const void *_b) { const git_pobject *a = (git_pobject *)_a; @@ -1259,10 +1249,39 @@ int git_packbuilder_write_buf(git_buf *buf, git_packbuilder *pb) return write_pack(pb, &write_pack_buf, buf); } -int git_packbuilder_write(git_packbuilder *pb, const char *path) +static int write_cb(void *buf, size_t len, void *payload) +{ + struct pack_write_context *ctx = payload; + return git_indexer_stream_add(ctx->indexer, buf, len, ctx->stats); +} + +int git_packbuilder_write( + git_packbuilder *pb, + const char *path, + git_transfer_progress_callback progress_cb, + void *progress_cb_payload) { + git_indexer_stream *indexer; + git_transfer_progress stats; + struct pack_write_context ctx; + PREPARE_PACK; - return write_pack_file(pb, path); + + if (git_indexer_stream_new( + &indexer, path, progress_cb, progress_cb_payload) < 0) + return -1; + + ctx.indexer = indexer; + ctx.stats = &stats; + + if (git_packbuilder_foreach(pb, write_cb, &ctx) < 0 || + git_indexer_stream_finalize(indexer, &stats) < 0) { + git_indexer_stream_free(indexer); + return -1; + } + + git_indexer_stream_free(indexer); + return 0; } #undef PREPARE_PACK @@ -1284,6 +1303,21 @@ static int cb_tree_walk(const char *root, const git_tree_entry *entry, void *pay git_buf_cstr(&ctx->buf)); } +int git_packbuilder_insert_commit(git_packbuilder *pb, const git_oid *oid) +{ + git_commit *commit; + + if (git_commit_lookup(&commit, pb->repo, oid) < 0 || + git_packbuilder_insert(pb, oid, NULL) < 0) + return -1; + + if (git_packbuilder_insert_tree(pb, git_commit_tree_id(commit)) < 0) + return -1; + + git_commit_free(commit); + return 0; +} + int git_packbuilder_insert_tree(git_packbuilder *pb, const git_oid *oid) { git_tree *tree; diff --git a/src/pack.c b/src/pack.c index 75ac98186..d7e6a1e94 100644 --- a/src/pack.c +++ b/src/pack.c @@ -12,8 +12,8 @@ #include "sha1_lookup.h" #include "mwindow.h" #include "fileops.h" +#include "oid.h" -#include "git2/oid.h" #include <zlib.h> static int packfile_open(struct git_pack_file *p); @@ -85,15 +85,27 @@ static void cache_free(git_pack_cache *cache) git_offmap_free(cache->entries); git_mutex_free(&cache->lock); } + + memset(cache, 0, sizeof(*cache)); } static int cache_init(git_pack_cache *cache) { - memset(cache, 0, sizeof(git_pack_cache)); + memset(cache, 0, sizeof(*cache)); + cache->entries = git_offmap_alloc(); GITERR_CHECK_ALLOC(cache->entries); + cache->memory_limit = GIT_PACK_CACHE_MEMORY_LIMIT; - git_mutex_init(&cache->lock); + + if (git_mutex_init(&cache->lock)) { + giterr_set(GITERR_OS, "Failed to initialize pack cache mutex"); + + git__free(cache->entries); + cache->entries = NULL; + + return -1; + } return 0; } @@ -205,13 +217,18 @@ static int pack_index_check(const char *path, struct git_pack_file *p) if (fd < 0) return fd; - if (p_fstat(fd, &st) < 0 || - !S_ISREG(st.st_mode) || + if (p_fstat(fd, &st) < 0) { + p_close(fd); + giterr_set(GITERR_OS, "Unable to stat pack index '%s'", path); + return -1; + } + + if (!S_ISREG(st.st_mode) || !git__is_sizet(st.st_size) || (idx_size = (size_t)st.st_size) < 4 * 256 + 20 + 20) { p_close(fd); - giterr_set(GITERR_OS, "Failed to check pack index."); + giterr_set(GITERR_ODB, "Invalid pack index '%s'", path); return -1; } @@ -288,32 +305,42 @@ static int pack_index_check(const char *path, struct git_pack_file *p) } } - p->index_version = version; p->num_objects = nr; + p->index_version = version; return 0; } static int pack_index_open(struct git_pack_file *p) { char *idx_name; - int error; - size_t name_len, offset; + int error = 0; + size_t name_len, base_len; - if (p->index_map.data) + if (p->index_version > -1) return 0; - idx_name = git__strdup(p->pack_name); - GITERR_CHECK_ALLOC(idx_name); + name_len = strlen(p->pack_name); + assert(name_len > strlen(".pack")); /* checked by git_pack_file alloc */ - name_len = strlen(idx_name); - offset = name_len - strlen(".pack"); - assert(offset < name_len); /* make sure no underflow */ + if ((idx_name = git__malloc(name_len)) == NULL) + return -1; + + base_len = name_len - strlen(".pack"); + memcpy(idx_name, p->pack_name, base_len); + memcpy(idx_name + base_len, ".idx", sizeof(".idx")); + + if ((error = git_mutex_lock(&p->lock)) < 0) { + git__free(idx_name); + return error; + } - strncpy(idx_name + offset, ".idx", name_len - offset); + if (p->index_version == -1) + error = pack_index_check(idx_name, p); - error = pack_index_check(idx_name, p); git__free(idx_name); + git_mutex_unlock(&p->lock); + return error; } @@ -389,12 +416,12 @@ int git_packfile_unpack_header( * the maximum deflated object size is 2^137, which is just * insane, so we know won't exceed what we have been given. */ -// base = pack_window_open(p, w_curs, *curpos, &left); +/* base = pack_window_open(p, w_curs, *curpos, &left); */ base = git_mwindow_open(mwf, w_curs, *curpos, 20, &left); if (base == NULL) return GIT_EBUFS; - ret = packfile_unpack_header1(&used, size_p, type_p, base, left); + ret = packfile_unpack_header1(&used, size_p, type_p, base, left); git_mwindow_close(w_curs); if (ret == GIT_EBUFS) return ret; @@ -786,30 +813,23 @@ git_off_t get_delta_base( * ***********************************************************/ -static struct git_pack_file *packfile_alloc(size_t extra) -{ - struct git_pack_file *p = git__calloc(1, sizeof(*p) + extra); - if (p != NULL) - p->mwf.fd = -1; - return p; -} - - void git_packfile_free(struct git_pack_file *p) { - assert(p); + if (!p) + return; cache_free(&p->bases); git_mwindow_free_all(&p->mwf); - git_mwindow_file_deregister(&p->mwf); - if (p->mwf.fd != -1) + if (p->mwf.fd >= 0) p_close(p->mwf.fd); pack_index_free(p); git__free(p->bad_object_sha1); + + git_mutex_free(&p->lock); git__free(p); } @@ -820,17 +840,22 @@ static int packfile_open(struct git_pack_file *p) git_oid sha1; unsigned char *idx_sha1; - assert(p->index_map.data); - - if (!p->index_map.data && pack_index_open(p) < 0) + if (p->index_version == -1 && pack_index_open(p) < 0) return git_odb__error_notfound("failed to open packfile", NULL); + /* if mwf opened by another thread, return now */ + if (git_mutex_lock(&p->lock) < 0) + return packfile_error("failed to get lock for open"); + + if (p->mwf.fd >= 0) { + git_mutex_unlock(&p->lock); + return 0; + } + /* TODO: open with noatime */ p->mwf.fd = git_futils_open_ro(p->pack_name); - if (p->mwf.fd < 0) { - p->mwf.fd = -1; - return -1; - } + if (p->mwf.fd < 0) + goto cleanup; if (p_fstat(p->mwf.fd, &st) < 0 || git_mwindow_file_register(&p->mwf) < 0) @@ -871,44 +896,55 @@ static int packfile_open(struct git_pack_file *p) idx_sha1 = ((unsigned char *)p->index_map.data) + p->index_map.len - 40; - if (git_oid_cmp(&sha1, (git_oid *)idx_sha1) == 0) - return 0; + if (git_oid__cmp(&sha1, (git_oid *)idx_sha1) != 0) + goto cleanup; + + git_mutex_unlock(&p->lock); + return 0; cleanup: giterr_set(GITERR_OS, "Invalid packfile '%s'", p->pack_name); - p_close(p->mwf.fd); + + if (p->mwf.fd >= 0) + p_close(p->mwf.fd); p->mwf.fd = -1; + + git_mutex_unlock(&p->lock); + return -1; } -int git_packfile_check(struct git_pack_file **pack_out, const char *path) +int git_packfile_alloc(struct git_pack_file **pack_out, const char *path) { struct stat st; struct git_pack_file *p; - size_t path_len; + size_t path_len = path ? strlen(path) : 0; *pack_out = NULL; - path_len = strlen(path); - p = packfile_alloc(path_len + 2); + + if (path_len < strlen(".idx")) + return git_odb__error_notfound("invalid packfile path", NULL); + + p = git__calloc(1, sizeof(*p) + path_len + 2); GITERR_CHECK_ALLOC(p); + memcpy(p->pack_name, path, path_len + 1); + /* * Make sure a corresponding .pack file exists and that * the index looks sane. */ - path_len -= strlen(".idx"); - if (path_len < 1) { - git__free(p); - return git_odb__error_notfound("invalid packfile path", NULL); - } + if (git__suffixcmp(path, ".idx") == 0) { + size_t root_len = path_len - strlen(".idx"); - memcpy(p->pack_name, path, path_len); + memcpy(p->pack_name + root_len, ".keep", sizeof(".keep")); + if (git_path_exists(p->pack_name) == true) + p->pack_keep = 1; - strcpy(p->pack_name + path_len, ".keep"); - if (git_path_exists(p->pack_name) == true) - p->pack_keep = 1; + memcpy(p->pack_name + root_len, ".pack", sizeof(".pack")); + path_len = path_len - strlen(".idx") + strlen(".pack"); + } - strcpy(p->pack_name + path_len, ".pack"); if (p_stat(p->pack_name, &st) < 0 || !S_ISREG(st.st_mode)) { git__free(p); return git_odb__error_notfound("packfile not found", NULL); @@ -917,9 +953,17 @@ int git_packfile_check(struct git_pack_file **pack_out, const char *path) /* ok, it looks sane as far as we can check without * actually mapping the pack file. */ + p->mwf.fd = -1; p->mwf.size = st.st_size; p->pack_local = 1; p->mtime = (git_time_t)st.st_mtime; + p->index_version = -1; + + if (git_mutex_init(&p->lock)) { + giterr_set(GITERR_OS, "Failed to initialize packfile mutex"); + git__free(p); + return -1; + } /* see if we can parse the sha1 oid in the packfile name */ if (path_len < 40 || @@ -1034,12 +1078,11 @@ static int pack_entry_find_offset( *offset_out = 0; - if (index == NULL) { + if (p->index_version == -1) { int error; if ((error = pack_index_open(p)) < 0) return error; - assert(p->index_map.data); index = p->index_map.data; @@ -1099,6 +1142,7 @@ static int pack_entry_find_offset( return git_odb__error_notfound("failed to find offset for pack entry", short_oid); if (found > 1) return git_odb__error_ambiguous("found multiple offsets for pack entry"); + *offset_out = nth_packed_object_offset(p, pos); git_oid_fromraw(found_oid, current); @@ -1110,6 +1154,7 @@ static int pack_entry_find_offset( printf("found lo=%d %s\n", lo, hex_sha1); } #endif + return 0; } @@ -1128,7 +1173,7 @@ int git_pack_entry_find( if (len == GIT_OID_HEXSZ && p->num_bad_objects) { unsigned i; for (i = 0; i < p->num_bad_objects; i++) - if (git_oid_cmp(short_oid, &p->bad_object_sha1[i]) == 0) + if (git_oid__cmp(short_oid, &p->bad_object_sha1[i]) == 0) return packfile_error("bad object found in packfile"); } diff --git a/src/pack.h b/src/pack.h index 8d7e33dfe..aeeac9ce1 100644 --- a/src/pack.h +++ b/src/pack.h @@ -79,6 +79,7 @@ typedef struct { struct git_pack_file { git_mwindow_file mwf; git_map index_map; + git_mutex lock; /* protect updates to mwf and index_map */ uint32_t num_objects; uint32_t num_bad_objects; @@ -142,7 +143,8 @@ git_off_t get_delta_base(struct git_pack_file *p, git_mwindow **w_curs, git_off_t delta_obj_offset); void git_packfile_free(struct git_pack_file *p); -int git_packfile_check(struct git_pack_file **pack_out, const char *path); +int git_packfile_alloc(struct git_pack_file **pack_out, const char *path); + int git_pack_entry_find( struct git_pack_entry *e, struct git_pack_file *p, diff --git a/src/path.h b/src/path.h index ead4fa338..b2899e97f 100644 --- a/src/path.h +++ b/src/path.h @@ -175,7 +175,6 @@ extern bool git_path_contains(git_buf *dir, const char *item); * * @param parent Directory path that might contain subdir * @param subdir Subdirectory name to look for in parent - * @param append_if_exists If true, then subdir will be appended to the parent path if it does exist * @return true if subdirectory exists, false otherwise. */ extern bool git_path_contains_dir(git_buf *parent, const char *subdir); @@ -185,7 +184,6 @@ extern bool git_path_contains_dir(git_buf *parent, const char *subdir); * * @param dir Directory path that might contain file * @param file File name to look for in parent - * @param append_if_exists If true, then file will be appended to the path if it does exist * @return true if file exists, false otherwise. */ extern bool git_path_contains_file(git_buf *dir, const char *file); diff --git a/src/pathspec.c b/src/pathspec.c index 9dee55ea1..4266bb99e 100644 --- a/src/pathspec.c +++ b/src/pathspec.c @@ -5,9 +5,16 @@ * a Linking Exception. For full terms see the included COPYING file. */ +#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) @@ -56,7 +63,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 +100,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,63 +113,602 @@ 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; +} + +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_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); + int result; + size_t pos; + 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; + pathspec_match_context_init(&ctxt, disable_fnmatch, casefold); - if (casefold) { - use_strcmp = git__strcasecmp; - use_strncmp = git__strncasecmp; - } else { - use_strcmp = git__strcmp; - use_strncmp = git__strncmp; + 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; } - git_vector_foreach(vspec, i, match) { - int result = (match->flags & GIT_ATTR_FNMATCH_MATCH_ALL) ? 0 : FNM_NOMATCH; - - 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 (matched_pathspec) - *matched_pathspec = match->pattern; - - return (match->flags & GIT_ATTR_FNMATCH_NEGATIVE) ? false : true; + return (result > 0); +} + + +int git_pathspec__init(git_pathspec *ps, const git_strarray *paths) +{ + int error = 0; + + memset(ps, 0, sizeof(*ps)); + + ps->prefix = git_pathspec_prefix(paths); + + 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__clear(git_pathspec *ps) +{ + 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, int datatype) +{ + 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; + m->datatype = datatype; + + return m; +} + +GIT_INLINE(size_t) pathspec_mark_pattern(git_bitvec *used, size_t pos) +{ + 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( + git_pathspec_match_list **out, + git_iterator *iter, + uint32_t flags, + git_pathspec *ps) +{ + int error = 0; + git_pathspec_match_list *m = NULL; + const git_index_entry *entry = 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 pos, used_ct = 0, found_files = 0; + git_index *index = 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, PATHSPEC_DATATYPE_STRINGS); + GITERR_CHECK_ALLOC(m); + } + + if ((error = git_iterator_reset(iter, ps->prefix, ps->prefix)) < 0) + goto done; + + 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))) { + /* 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) + continue; + + /* if result was a negative pattern match, then don't list file */ + if (!result) { + used_ct += pathspec_mark_pattern(&used_patterns, pos); + continue; + } + + /* 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 */ + 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) + 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) { + if (used_ct == patterns->length) + break; + continue; } + + /* insert matched path into matches array */ + if ((file = (char **)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 && + (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) { + giterr_set(GITERR_INVALID, "No matching files were found"); + error = GIT_ENOTFOUND; } - return false; +done: + git_bitvec_free(&used_patterns); + + if (error < 0) { + pathspec_match_free(m); + if (out) *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(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(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(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; +} + +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) +{ + if (m) + pathspec_match_free(m); +} + +size_t git_pathspec_match_list_entrycount( + const git_pathspec_match_list *m) +{ + return m ? git_array_size(m->matches) : 0; +} + +const char *git_pathspec_match_list_entry( + const git_pathspec_match_list *m, size_t pos) +{ + 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 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 = m ? git_array_get(m->failures, pos) : NULL; + + return entry ? *entry : NULL; } diff --git a/src/pathspec.h b/src/pathspec.h index 43a94baad..40cd21c3f 100644 --- a/src/pathspec.h +++ b/src/pathspec.h @@ -8,9 +8,35 @@ #define INCLUDE_pathspec_h__ #include "common.h" +#include <git2/pathspec.h> #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; +}; + +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(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 */ extern char *git_pathspec_prefix(const git_strarray *pathspec); @@ -19,22 +45,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 */ + +extern int git_pathspec__init(git_pathspec *ps, const git_strarray *paths); + +extern void git_pathspec__clear(git_pathspec *ps); #endif diff --git a/src/pool.c b/src/pool.c index b3cd49665..d484769e9 100644 --- a/src/pool.c +++ b/src/pool.c @@ -194,6 +194,11 @@ char *git_pool_strndup(git_pool *pool, const char *str, size_t n) assert(pool && str && pool->item_size == sizeof(char)); + if (n + 1 == 0) { + giterr_set_oom(); + return NULL; + } + if ((ptr = git_pool_malloc(pool, (uint32_t)(n + 1))) != NULL) { memcpy(ptr, str, n); *(((char *)ptr) + n) = '\0'; diff --git a/src/posix.c b/src/posix.c index 5d526d33c..b75109b83 100644 --- a/src/posix.c +++ b/src/posix.c @@ -111,12 +111,12 @@ int p_open(const char *path, int flags, ...) va_end(arg_list); } - return open(path, flags | O_BINARY, mode); + return open(path, flags | O_BINARY | O_CLOEXEC, mode); } int p_creat(const char *path, mode_t mode) { - return open(path, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, mode); + return open(path, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | O_CLOEXEC, mode); } int p_getcwd(char *buffer_out, size_t size) diff --git a/src/posix.h b/src/posix.h index 719c8a04c..40bcc1ab0 100644 --- a/src/posix.h +++ b/src/posix.h @@ -25,6 +25,9 @@ #if !defined(O_BINARY) #define O_BINARY 0 #endif +#if !defined(O_CLOEXEC) +#define O_CLOEXEC 0 +#endif typedef int git_file; diff --git a/src/pqueue.h b/src/pqueue.h index ed7139285..9061f8279 100644 --- a/src/pqueue.h +++ b/src/pqueue.h @@ -48,7 +48,7 @@ typedef struct { * should be preallocated * @param cmppri the callback function to compare two nodes of the queue * - * @Return the handle or NULL for insufficent memory + * @return the handle or NULL for insufficent memory */ int git_pqueue_init(git_pqueue *q, size_t n, git_pqueue_cmp cmppri); @@ -83,8 +83,7 @@ int git_pqueue_insert(git_pqueue *q, void *d); /** * pop the highest-ranking item from the queue. - * @param p the queue - * @param d where to copy the entry to + * @param q the queue * @return NULL on error, otherwise the entry */ void *git_pqueue_pop(git_pqueue *q); @@ -93,7 +92,6 @@ void *git_pqueue_pop(git_pqueue *q); /** * access highest-ranking item without removing it. * @param q the queue - * @param d the entry * @return NULL on error, otherwise the entry */ void *git_pqueue_peek(git_pqueue *q); diff --git a/src/push.c b/src/push.c index f81a0aee9..452d71789 100644 --- a/src/push.c +++ b/src/push.c @@ -99,19 +99,17 @@ static int check_lref(git_push *push, char *ref) /* lref must be resolvable to an existing object */ git_object *obj; int error = git_revparse_single(&obj, push->repo, ref); + git_object_free(obj); - if (error) { - if (error == GIT_ENOTFOUND) - giterr_set(GITERR_REFERENCE, - "src refspec '%s' does not match any existing object", ref); - else - giterr_set(GITERR_INVALID, "Not a valid reference '%s'", ref); + if (!error) + return 0; - return -1; - } else - git_object_free(obj); - - return 0; + if (error == GIT_ENOTFOUND) + giterr_set(GITERR_REFERENCE, + "src refspec '%s' does not match any existing object", ref); + else + giterr_set(GITERR_INVALID, "Not a valid reference '%s'", ref); + return -1; } static int parse_refspec(git_push *push, push_spec **spec, const char *str) @@ -179,10 +177,10 @@ int git_push_add_refspec(git_push *push, const char *refspec) int git_push_update_tips(git_push *push) { - git_refspec *fetch_spec = &push->remote->fetch; git_buf remote_ref_name = GIT_BUF_INIT; size_t i, j; - push_spec *push_spec; + git_refspec *fetch_spec; + push_spec *push_spec = NULL; git_reference *remote_ref; push_status *status; int error = 0; @@ -193,7 +191,8 @@ int git_push_update_tips(git_push *push) continue; /* Find the corresponding remote ref */ - if (!git_refspec_src_matches(fetch_spec, status->ref)) + fetch_spec = git_remote__matching_refspec(push->remote, status->ref); + if (!fetch_spec) continue; if ((error = git_refspec_transform_r(&remote_ref_name, fetch_spec, status->ref)) < 0) @@ -304,7 +303,7 @@ static int revwalk(git_vector *commits, git_push *push) continue; if (!git_odb_exists(push->repo->_odb, &spec->roid)) { - giterr_clear(); + giterr_set(GITERR_REFERENCE, "Cannot push missing reference"); error = GIT_ENONFASTFORWARD; goto on_error; } @@ -314,7 +313,8 @@ static int revwalk(git_vector *commits, git_push *push) if (error == GIT_ENOTFOUND || (!error && !git_oid_equal(&base, &spec->roid))) { - giterr_clear(); + giterr_set(GITERR_REFERENCE, + "Cannot push non-fastforwardable reference"); error = GIT_ENONFASTFORWARD; goto on_error; } @@ -334,12 +334,13 @@ static int revwalk(git_vector *commits, git_push *push) while ((error = git_revwalk_next(&oid, rw)) == 0) { git_oid *o = git__malloc(GIT_OID_RAWSZ); - GITERR_CHECK_ALLOC(o); - git_oid_cpy(o, &oid); - if (git_vector_insert(commits, o) < 0) { + if (!o) { error = -1; goto on_error; } + git_oid_cpy(o, &oid); + if ((error = git_vector_insert(commits, o)) < 0) + goto on_error; } on_error: @@ -377,7 +378,7 @@ static int queue_differences( const git_tree_entry *d_entry = git_tree_entry_byindex(delta, j); int cmp = 0; - if (!git_oid_cmp(&b_entry->oid, &d_entry->oid)) + if (!git_oid__cmp(&b_entry->oid, &d_entry->oid)) goto loop; cmp = strcmp(b_entry->filename, d_entry->filename); @@ -520,7 +521,7 @@ static int calculate_work(git_push *push) /* This is a create or update. Local ref must exist. */ if (git_reference_name_to_id( &spec->loid, push->repo, spec->lref) < 0) { - giterr_set(GIT_ENOTFOUND, "No such reference '%s'", spec->lref); + giterr_set(GITERR_REFERENCE, "No such reference '%s'", spec->lref); return -1; } } diff --git a/src/refdb.c b/src/refdb.c index d9b73c6e7..4de7188b2 100644 --- a/src/refdb.c +++ b/src/refdb.c @@ -7,15 +7,16 @@ #include "common.h" #include "posix.h" + #include "git2/object.h" #include "git2/refs.h" #include "git2/refdb.h" +#include "git2/sys/refdb_backend.h" + #include "hash.h" #include "refdb.h" #include "refs.h" -#include "git2/refdb_backend.h" - int git_refdb_new(git_refdb **out, git_repository *repo) { git_refdb *db; @@ -45,7 +46,7 @@ int git_refdb_open(git_refdb **out, git_repository *repo) return -1; /* Add the default (filesystem) backend */ - if (git_refdb_backend_fs(&dir, repo, db) < 0) { + if (git_refdb_backend_fs(&dir, repo) < 0) { git_refdb_free(db); return -1; } @@ -57,15 +58,19 @@ int git_refdb_open(git_refdb **out, git_repository *repo) return 0; } -int git_refdb_set_backend(git_refdb *db, git_refdb_backend *backend) +static void refdb_free_backend(git_refdb *db) { if (db->backend) { - if(db->backend->free) + if (db->backend->free) db->backend->free(db->backend); else git__free(db->backend); } +} +int git_refdb_set_backend(git_refdb *db, git_refdb_backend *backend) +{ + refdb_free_backend(db); db->backend = backend; return 0; @@ -74,23 +79,17 @@ int git_refdb_set_backend(git_refdb *db, git_refdb_backend *backend) int git_refdb_compress(git_refdb *db) { assert(db); - - if (db->backend->compress) { + + if (db->backend->compress) return db->backend->compress(db->backend); - } - + return 0; } -static void refdb_free(git_refdb *db) +void git_refdb__free(git_refdb *db) { - if (db->backend) { - if(db->backend->free) - db->backend->free(db->backend); - else - git__free(db->backend); - } - + refdb_free_backend(db); + git__memzero(db, sizeof(*db)); git__free(db); } @@ -99,7 +98,7 @@ void git_refdb_free(git_refdb *db) if (db == NULL) return; - GIT_REFCOUNT_DEC(db, refdb_free); + GIT_REFCOUNT_DEC(db, git_refdb__free); } int git_refdb_exists(int *exists, git_refdb *refdb, const char *ref_name) @@ -111,75 +110,96 @@ int git_refdb_exists(int *exists, git_refdb *refdb, const char *ref_name) int git_refdb_lookup(git_reference **out, git_refdb *db, const char *ref_name) { - assert(db && db->backend && ref_name); + git_reference *ref; + int error; - return db->backend->lookup(out, db->backend, ref_name); -} + assert(db && db->backend && out && ref_name); -int git_refdb_foreach( - git_refdb *db, - unsigned int list_flags, - git_reference_foreach_cb callback, - void *payload) -{ - assert(db && db->backend); + error = db->backend->lookup(&ref, db->backend, ref_name); + if (error < 0) + return error; - return db->backend->foreach(db->backend, list_flags, callback, payload); -} + GIT_REFCOUNT_INC(db); + ref->db = db; -struct glob_cb_data { - const char *glob; - git_reference_foreach_cb callback; - void *payload; -}; + *out = ref; + return 0; +} -static int fromglob_cb(const char *reference_name, void *payload) +int git_refdb_iterator(git_reference_iterator **out, git_refdb *db, const char *glob) { - struct glob_cb_data *data = (struct glob_cb_data *)payload; + if (!db->backend || !db->backend->iterator) { + giterr_set(GITERR_REFERENCE, "This backend doesn't support iterators"); + return -1; + } - if (!p_fnmatch(data->glob, reference_name, 0)) - return data->callback(reference_name, data->payload); + if (db->backend->iterator(out, db->backend, glob) < 0) + return -1; + + GIT_REFCOUNT_INC(db); + (*out)->db = db; return 0; } -int git_refdb_foreach_glob( - git_refdb *db, - const char *glob, - unsigned int list_flags, - git_reference_foreach_cb callback, - void *payload) +int git_refdb_iterator_next(git_reference **out, git_reference_iterator *iter) { int error; - struct glob_cb_data data; - assert(db && db->backend && glob && callback); + if ((error = iter->next(out, iter)) < 0) + return error; - if(db->backend->foreach_glob != NULL) - error = db->backend->foreach_glob(db->backend, - glob, list_flags, callback, payload); - else { - data.glob = glob; - data.callback = callback; - data.payload = payload; + GIT_REFCOUNT_INC(iter->db); + (*out)->db = iter->db; - error = db->backend->foreach(db->backend, - list_flags, fromglob_cb, &data); - } + return 0; +} + +int git_refdb_iterator_next_name(const char **out, git_reference_iterator *iter) +{ + return iter->next_name(out, iter); +} - return error; +void git_refdb_iterator_free(git_reference_iterator *iter) +{ + GIT_REFCOUNT_DEC(iter->db, git_refdb__free); + iter->free(iter); } -int git_refdb_write(git_refdb *db, const git_reference *ref) +int git_refdb_write(git_refdb *db, git_reference *ref, int force) { assert(db && db->backend); - return db->backend->write(db->backend, ref); + GIT_REFCOUNT_INC(db); + ref->db = db; + + return db->backend->write(db->backend, ref, force); } -int git_refdb_delete(struct git_refdb *db, const git_reference *ref) +int git_refdb_rename( + git_reference **out, + git_refdb *db, + const char *old_name, + const char *new_name, + int force) { + int error; + assert(db && db->backend); + error = db->backend->rename(out, db->backend, old_name, new_name, force); + if (error < 0) + return error; + + if (out) { + GIT_REFCOUNT_INC(db); + (*out)->db = db; + } + + return 0; +} - return db->backend->delete(db->backend, ref); +int git_refdb_delete(struct git_refdb *db, const char *ref_name) +{ + assert(db && db->backend); + return db->backend->delete(db->backend, ref_name); } diff --git a/src/refdb.h b/src/refdb.h index 0969711b9..3aea37b62 100644 --- a/src/refdb.h +++ b/src/refdb.h @@ -16,6 +16,8 @@ struct git_refdb { git_refdb_backend *backend; }; +void git_refdb__free(git_refdb *db); + int git_refdb_exists( int *exists, git_refdb *refdb, @@ -26,21 +28,19 @@ int git_refdb_lookup( git_refdb *refdb, const char *ref_name); -int git_refdb_foreach( - git_refdb *refdb, - unsigned int list_flags, - git_reference_foreach_cb callback, - void *payload); - -int git_refdb_foreach_glob( - git_refdb *refdb, - const char *glob, - unsigned int list_flags, - git_reference_foreach_cb callback, - void *payload); - -int git_refdb_write(git_refdb *refdb, const git_reference *ref); - -int git_refdb_delete(struct git_refdb *refdb, const git_reference *ref); +int git_refdb_rename( + git_reference **out, + git_refdb *db, + const char *old_name, + const char *new_name, + int force); + +int git_refdb_iterator(git_reference_iterator **out, git_refdb *db, const char *glob); +int git_refdb_iterator_next(git_reference **out, git_reference_iterator *iter); +int git_refdb_iterator_next_name(const char **out, git_reference_iterator *iter); +void git_refdb_iterator_free(git_reference_iterator *iter); + +int git_refdb_write(git_refdb *refdb, git_reference *ref, int force); +int git_refdb_delete(git_refdb *refdb, const char *ref_name); #endif diff --git a/src/refdb_fs.c b/src/refdb_fs.c index f00bd72a0..acd82594b 100644 --- a/src/refdb_fs.c +++ b/src/refdb_fs.c @@ -9,16 +9,18 @@ #include "hash.h" #include "repository.h" #include "fileops.h" +#include "filebuf.h" #include "pack.h" #include "reflog.h" -#include "config.h" #include "refdb.h" #include "refdb_fs.h" +#include "iterator.h" #include <git2/tag.h> #include <git2/object.h> #include <git2/refdb.h> -#include <git2/refdb_backend.h> +#include <git2/sys/refdb_backend.h> +#include <git2/sys/refs.h> GIT__USE_STRMAP; @@ -26,8 +28,16 @@ GIT__USE_STRMAP; #define MAX_NESTING_LEVEL 10 enum { - GIT_PACKREF_HAS_PEEL = 1, - GIT_PACKREF_WAS_LOOSE = 2 + PACKREF_HAS_PEEL = 1, + PACKREF_WAS_LOOSE = 2, + PACKREF_CANNOT_PEEL = 4, + PACKREF_SHADOWED = 8, +}; + +enum { + PEELING_NONE = 0, + PEELING_STANDARD, + PEELING_FULL }; struct packref { @@ -41,10 +51,10 @@ typedef struct refdb_fs_backend { git_refdb_backend parent; git_repository *repo; - const char *path; - git_refdb *refdb; + char *path; git_refcache refcache; + int peeling_mode; } refdb_fs_backend; static int reference_read( @@ -62,7 +72,7 @@ static int reference_read( /* Determine the full path of the file */ if (git_buf_joinpath(&path, repo_path, ref_name) < 0) return -1; - + result = git_futils_readbuffer_updated(file_content, path.ptr, mtime, NULL, updated); git_buf_free(&path); @@ -99,7 +109,7 @@ static int packed_parse_oid( refname_len = refname_end - refname_begin; - ref = git__malloc(sizeof(struct packref) + refname_len + 1); + ref = git__calloc(1, sizeof(struct packref) + refname_len + 1); GITERR_CHECK_ALLOC(ref); memcpy(ref->name, refname_begin, refname_len); @@ -107,11 +117,8 @@ static int packed_parse_oid( git_oid_cpy(&ref->oid, &id); - ref->flags = 0; - *ref_out = ref; *buffer_out = refname_end + 1; - return 0; corrupt: @@ -133,10 +140,6 @@ static int packed_parse_peel( if (tag_ref == NULL) goto corrupt; - /* Ensure reference is a tag */ - if (git__prefixcmp(tag_ref->name, GIT_REFS_TAGS_DIR) != 0) - goto corrupt; - if (buffer + GIT_OID_HEXSZ > buffer_end) goto corrupt; @@ -155,6 +158,7 @@ static int packed_parse_peel( goto corrupt; } + tag_ref->flags |= PACKREF_HAS_PEEL; *buffer_out = buffer; return 0; @@ -175,7 +179,10 @@ static int packed_load(refdb_fs_backend *backend) ref_cache->packfile = git_strmap_alloc(); GITERR_CHECK_ALLOC(ref_cache->packfile); } - + + if (backend->path == NULL) + return 0; + result = reference_read(&packfile, &ref_cache->packfile_time, backend->path, GIT_PACKEDREFS_FILE, &updated); @@ -193,7 +200,7 @@ static int packed_load(refdb_fs_backend *backend) if (result < 0) return -1; - + if (!updated) return 0; @@ -206,6 +213,30 @@ static int packed_load(refdb_fs_backend *backend) buffer_start = (const char *)packfile.ptr; buffer_end = (const char *)(buffer_start) + packfile.size; + backend->peeling_mode = PEELING_NONE; + + if (buffer_start[0] == '#') { + static const char *traits_header = "# pack-refs with: "; + + if (git__prefixcmp(buffer_start, traits_header) == 0) { + char *traits = (char *)buffer_start + strlen(traits_header); + char *traits_end = strchr(traits, '\n'); + + if (traits_end == NULL) + goto parse_failed; + + *traits_end = '\0'; + + if (strstr(traits, " fully-peeled ") != NULL) { + backend->peeling_mode = PEELING_FULL; + } else if (strstr(traits, " peeled ") != NULL) { + backend->peeling_mode = PEELING_STANDARD; + } + + buffer_start = traits_end + 1; + } + } + while (buffer_start < buffer_end && buffer_start[0] == '#') { buffer_start = strchr(buffer_start, '\n'); if (buffer_start == NULL) @@ -224,6 +255,10 @@ static int packed_load(refdb_fs_backend *backend) if (buffer_start[0] == '^') { if (packed_parse_peel(ref, &buffer_start, buffer_end) < 0) goto parse_failed; + } else if (backend->peeling_mode == PEELING_FULL || + (backend->peeling_mode == PEELING_STANDARD && + git__prefixcmp(ref->name, GIT_REFS_TAGS_DIR) == 0)) { + ref->flags |= PACKREF_CANNOT_PEEL; } git_strmap_insert(ref_cache->packfile, ref->name, ref, err); @@ -241,7 +276,7 @@ parse_failed: return -1; } -static int loose_parse_oid(git_oid *oid, git_buf *file_content) +static int loose_parse_oid(git_oid *oid, const char *filename, git_buf *file_content) { size_t len; const char *str; @@ -263,7 +298,7 @@ static int loose_parse_oid(git_oid *oid, git_buf *file_content) return 0; corrupted: - giterr_set(GITERR_REFERENCE, "Corrupted loose reference file"); + giterr_set(GITERR_REFERENCE, "Corrupted loose reference file: %s", filename); return -1; } @@ -284,19 +319,19 @@ static int loose_lookup_to_packfile( git_buf_rtrim(&ref_file); name_len = strlen(name); - ref = git__malloc(sizeof(struct packref) + name_len + 1); + ref = git__calloc(1, sizeof(struct packref) + name_len + 1); GITERR_CHECK_ALLOC(ref); memcpy(ref->name, name, name_len); ref->name[name_len] = 0; - if (loose_parse_oid(&ref->oid, &ref_file) < 0) { + if (loose_parse_oid(&ref->oid, name, &ref_file) < 0) { git_buf_free(&ref_file); git__free(ref); return -1; } - ref->flags = GIT_PACKREF_WAS_LOOSE; + ref->flags = PACKREF_WAS_LOOSE; *ref_out = ref; git_buf_free(&ref_file); @@ -417,6 +452,9 @@ static int loose_lookup( git_buf ref_file = GIT_BUF_INIT; int error = 0; + if (out) + *out = NULL; + error = reference_read(&ref_file, NULL, backend->path, ref_name, NULL); if (error < 0) @@ -430,15 +468,17 @@ static int loose_lookup( goto done; } - *out = git_reference__alloc(backend->refdb, ref_name, NULL, target); + if (out) + *out = git_reference__alloc_symbolic(ref_name, target); } else { - if ((error = loose_parse_oid(&oid, &ref_file)) < 0) + if ((error = loose_parse_oid(&oid, ref_name, &ref_file)) < 0) goto done; - - *out = git_reference__alloc(backend->refdb, ref_name, &oid, NULL); + + if (out) + *out = git_reference__alloc(ref_name, &oid, NULL); } - if (*out == NULL) + if (out && *out == NULL) error = -1; done: @@ -456,19 +496,19 @@ static int packed_map_entry( if (packed_load(backend) < 0) return -1; - + /* Look up on the packfile */ packfile_refs = backend->refcache.packfile; *pos = git_strmap_lookup_index(packfile_refs, ref_name); - + if (!git_strmap_valid_index(packfile_refs, *pos)) { giterr_set(GITERR_REFERENCE, "Reference '%s' not found", ref_name); return GIT_ENOTFOUND; } *entry = git_strmap_value_at(packfile_refs, *pos); - + return 0; } @@ -480,13 +520,14 @@ static int packed_lookup( struct packref *entry; khiter_t pos; int error = 0; - + if ((error = packed_map_entry(&entry, &pos, backend, ref_name)) < 0) return error; - if ((*out = git_reference__alloc(backend->refdb, ref_name, &entry->oid, NULL)) == NULL) + if ((*out = git_reference__alloc(ref_name, + &entry->oid, &entry->peel)) == NULL) return -1; - + return 0; } @@ -515,108 +556,255 @@ static int refdb_fs_backend__lookup( return result; } -struct dirent_list_data { - refdb_fs_backend *backend; - size_t repo_path_len; - unsigned int list_type:2; +typedef struct { + git_reference_iterator parent; - git_reference_foreach_cb callback; - void *callback_payload; - int callback_error; -}; + char *glob; + + git_pool pool; + git_vector loose; + + unsigned int loose_pos; + khiter_t packed_pos; +} refdb_fs_iter; -static git_ref_t loose_guess_rtype(const git_buf *full_path) +static void refdb_fs_backend__iterator_free(git_reference_iterator *_iter) { - git_buf ref_file = GIT_BUF_INIT; - git_ref_t type; + refdb_fs_iter *iter = (refdb_fs_iter *) _iter; - type = GIT_REF_INVALID; + git_vector_free(&iter->loose); + git_pool_clear(&iter->pool); + git__free(iter); +} - if (git_futils_readbuffer(&ref_file, full_path->ptr) == 0) { - if (git__prefixcmp((const char *)(ref_file.ptr), GIT_SYMREF) == 0) - type = GIT_REF_SYMBOLIC; - else - type = GIT_REF_OID; +static int iter_load_loose_paths(refdb_fs_backend *backend, refdb_fs_iter *iter) +{ + int error = 0; + git_strmap *packfile = backend->refcache.packfile; + git_buf path = GIT_BUF_INIT; + git_iterator *fsit = NULL; + const git_index_entry *entry = NULL; + + if (!backend->path) /* do nothing if no path for loose refs */ + return 0; + + if (git_buf_printf(&path, "%s/refs", backend->path) < 0) + return -1; + + if ((error = git_iterator_for_filesystem( + &fsit, git_buf_cstr(&path), 0, NULL, NULL)) < 0 || + (error = git_vector_init(&iter->loose, 8, NULL)) < 0 || + (error = git_buf_sets(&path, GIT_REFS_DIR)) < 0) + goto cleanup; + + while (!git_iterator_advance(&entry, fsit)) { + const char *ref_name; + khiter_t pos; + char *ref_dup; + + git_buf_truncate(&path, strlen(GIT_REFS_DIR)); + git_buf_puts(&path, entry->path); + ref_name = git_buf_cstr(&path); + + if (git__suffixcmp(ref_name, ".lock") == 0 || + (iter->glob && p_fnmatch(iter->glob, ref_name, 0) != 0)) + continue; + + pos = git_strmap_lookup_index(packfile, ref_name); + if (git_strmap_valid_index(packfile, pos)) { + struct packref *ref = git_strmap_value_at(packfile, pos); + ref->flags |= PACKREF_SHADOWED; + } + + if (!(ref_dup = git_pool_strdup(&iter->pool, ref_name))) { + error = -1; + goto cleanup; + } + + if ((error = git_vector_insert(&iter->loose, ref_dup)) < 0) + goto cleanup; } - git_buf_free(&ref_file); - return type; +cleanup: + git_iterator_free(fsit); + git_buf_free(&path); + + return 0; } -static int _dirent_loose_listall(void *_data, git_buf *full_path) +static int refdb_fs_backend__iterator_next( + git_reference **out, git_reference_iterator *_iter) { - struct dirent_list_data *data = (struct dirent_list_data *)_data; - const char *file_path = full_path->ptr + data->repo_path_len; + refdb_fs_iter *iter = (refdb_fs_iter *)_iter; + refdb_fs_backend *backend = (refdb_fs_backend *)iter->parent.db->backend; + git_strmap *packfile = backend->refcache.packfile; - if (git_path_isdir(full_path->ptr) == true) - return git_path_direach(full_path, _dirent_loose_listall, _data); + while (iter->loose_pos < iter->loose.length) { + const char *path = git_vector_get(&iter->loose, iter->loose_pos++); - /* do not add twice a reference that exists already in the packfile */ - if (git_strmap_exists(data->backend->refcache.packfile, file_path)) - return 0; + if (loose_lookup(out, backend, path) == 0) + return 0; - if (data->list_type != GIT_REF_LISTALL) { - if ((data->list_type & loose_guess_rtype(full_path)) == 0) - return 0; /* we are filtering out this reference */ + giterr_clear(); } - /* Locked references aren't returned */ - if (!git__suffixcmp(file_path, GIT_FILELOCK_EXTENSION)) + while (iter->packed_pos < kh_end(packfile)) { + struct packref *ref = NULL; + + while (!kh_exist(packfile, iter->packed_pos)) { + iter->packed_pos++; + if (iter->packed_pos == kh_end(packfile)) + return GIT_ITEROVER; + } + + ref = kh_val(packfile, iter->packed_pos); + iter->packed_pos++; + + if (ref->flags & PACKREF_SHADOWED) + continue; + + if (iter->glob && p_fnmatch(iter->glob, ref->name, 0) != 0) + continue; + + *out = git_reference__alloc(ref->name, &ref->oid, &ref->peel); + if (*out == NULL) + return -1; + return 0; + } + + return GIT_ITEROVER; +} + +static int refdb_fs_backend__iterator_next_name( + const char **out, git_reference_iterator *_iter) +{ + refdb_fs_iter *iter = (refdb_fs_iter *)_iter; + refdb_fs_backend *backend = (refdb_fs_backend *)iter->parent.db->backend; + git_strmap *packfile = backend->refcache.packfile; + + while (iter->loose_pos < iter->loose.length) { + const char *path = git_vector_get(&iter->loose, iter->loose_pos++); - if (data->callback(file_path, data->callback_payload)) - data->callback_error = GIT_EUSER; + if (git_strmap_exists(packfile, path)) + continue; + + if (loose_lookup(NULL, backend, path) != 0) { + giterr_clear(); + continue; + } - return data->callback_error; + *out = path; + return 0; + } + + while (iter->packed_pos < kh_end(packfile)) { + while (!kh_exist(packfile, iter->packed_pos)) { + iter->packed_pos++; + if (iter->packed_pos == kh_end(packfile)) + return GIT_ITEROVER; + } + + *out = kh_key(packfile, iter->packed_pos); + iter->packed_pos++; + + if (iter->glob && p_fnmatch(iter->glob, *out, 0) != 0) + continue; + + return 0; + } + + return GIT_ITEROVER; } -static int refdb_fs_backend__foreach( - git_refdb_backend *_backend, - unsigned int list_type, - git_reference_foreach_cb callback, - void *payload) +static int refdb_fs_backend__iterator( + git_reference_iterator **out, git_refdb_backend *_backend, const char *glob) { + refdb_fs_iter *iter; refdb_fs_backend *backend; - int result; - struct dirent_list_data data; - git_buf refs_path = GIT_BUF_INIT; - const char *ref_name; - void *ref = NULL; - - GIT_UNUSED(ref); assert(_backend); backend = (refdb_fs_backend *)_backend; if (packed_load(backend) < 0) return -1; - - /* list all the packed references first */ - if (list_type & GIT_REF_OID) { - git_strmap_foreach(backend->refcache.packfile, ref_name, ref, { - if (callback(ref_name, payload)) - return GIT_EUSER; - }); + + iter = git__calloc(1, sizeof(refdb_fs_iter)); + GITERR_CHECK_ALLOC(iter); + + if (git_pool_init(&iter->pool, 1, 0) < 0) + goto fail; + + if (glob != NULL && + (iter->glob = git_pool_strdup(&iter->pool, glob)) == NULL) + goto fail; + + iter->parent.next = refdb_fs_backend__iterator_next; + iter->parent.next_name = refdb_fs_backend__iterator_next_name; + iter->parent.free = refdb_fs_backend__iterator_free; + + if (iter_load_loose_paths(backend, iter) < 0) + goto fail; + + *out = (git_reference_iterator *)iter; + return 0; + +fail: + refdb_fs_backend__iterator_free((git_reference_iterator *)iter); + return -1; +} + +static bool ref_is_available( + const char *old_ref, const char *new_ref, const char *this_ref) +{ + if (old_ref == NULL || strcmp(old_ref, this_ref)) { + size_t reflen = strlen(this_ref); + size_t newlen = strlen(new_ref); + size_t cmplen = reflen < newlen ? reflen : newlen; + const char *lead = reflen < newlen ? new_ref : this_ref; + + if (!strncmp(new_ref, this_ref, cmplen) && lead[cmplen] == '/') { + return false; + } } - /* now list the loose references, trying not to - * duplicate the ref names already in the packed-refs file */ + return true; +} - data.repo_path_len = strlen(backend->path); - data.list_type = list_type; - data.backend = backend; - data.callback = callback; - data.callback_payload = payload; - data.callback_error = 0; +static int reference_path_available( + refdb_fs_backend *backend, + const char *new_ref, + const char* old_ref, + int force) +{ + struct packref *this_ref; - if (git_buf_joinpath(&refs_path, backend->path, GIT_REFS_DIR) < 0) + if (packed_load(backend) < 0) return -1; - result = git_path_direach(&refs_path, _dirent_loose_listall, &data); + if (!force) { + int exists; - git_buf_free(&refs_path); + if (refdb_fs_backend__exists(&exists, (git_refdb_backend *)backend, new_ref) < 0) + return -1; + + if (exists) { + giterr_set(GITERR_REFERENCE, + "Failed to write reference '%s': a reference with " + " that name already exists.", new_ref); + return GIT_EEXISTS; + } + } + + git_strmap_foreach_value(backend->refcache.packfile, this_ref, { + if (!ref_is_available(old_ref, new_ref, this_ref->name)) { + giterr_set(GITERR_REFERENCE, + "The path to reference '%s' collides with an existing one", new_ref); + return -1; + } + }); - return data.callback_error ? GIT_EUSER : result; + return 0; } static int loose_write(refdb_fs_backend *backend, const git_reference *ref) @@ -627,8 +815,7 @@ static int loose_write(refdb_fs_backend *backend, const git_reference *ref) /* Remove a possibly existing empty directory hierarchy * which name would collide with the reference name */ - if (git_futils_rmdir_r(ref->name, backend->path, - GIT_RMDIR_SKIP_NONEMPTY) < 0) + if (git_futils_rmdir_r(ref->name, backend->path, GIT_RMDIR_SKIP_NONEMPTY) < 0) return -1; if (git_buf_joinpath(&ref_path, backend->path, ref->name) < 0) @@ -678,14 +865,7 @@ static int packed_find_peel(refdb_fs_backend *backend, struct packref *ref) { git_object *object; - if (ref->flags & GIT_PACKREF_HAS_PEEL) - return 0; - - /* - * Only applies to tags, i.e. references - * in the /refs/tags folder - */ - if (git__prefixcmp(ref->name, GIT_REFS_TAGS_DIR) != 0) + if (ref->flags & PACKREF_HAS_PEEL || ref->flags & PACKREF_CANNOT_PEEL) return 0; /* @@ -706,7 +886,7 @@ static int packed_find_peel(refdb_fs_backend *backend, struct packref *ref) * Find the object pointed at by this tag */ git_oid_cpy(&ref->peel, git_tag_target_id(tag)); - ref->flags |= GIT_PACKREF_HAS_PEEL; + ref->flags |= PACKREF_HAS_PEEL; /* * The reference has now cached the resolved OID, and is @@ -739,7 +919,7 @@ static int packed_write_ref(struct packref *ref, git_filebuf *file) * This obviously only applies to tags. * The required peels have already been loaded into `ref->peel_target`. */ - if (ref->flags & GIT_PACKREF_HAS_PEEL) { + if (ref->flags & PACKREF_HAS_PEEL) { char peel[GIT_OID_HEXSZ + 1]; git_oid_fmt(peel, &ref->peel); peel[GIT_OID_HEXSZ] = 0; @@ -776,7 +956,7 @@ static int packed_remove_loose( for (i = 0; i < packing_list->length; ++i) { struct packref *ref = git_vector_get(packing_list, i); - if ((ref->flags & GIT_PACKREF_WAS_LOOSE) == 0) + if ((ref->flags & PACKREF_WAS_LOOSE) == 0) continue; if (git_buf_joinpath(&full_path, backend->path, ref->name) < 0) @@ -895,66 +1075,113 @@ cleanup_memory: static int refdb_fs_backend__write( git_refdb_backend *_backend, - const git_reference *ref) + const git_reference *ref, + int force) { refdb_fs_backend *backend; + int error; assert(_backend); backend = (refdb_fs_backend *)_backend; + error = reference_path_available(backend, ref->name, NULL, force); + if (error < 0) + return error; + return loose_write(backend, ref); } static int refdb_fs_backend__delete( git_refdb_backend *_backend, - const git_reference *ref) + const char *ref_name) { refdb_fs_backend *backend; - git_repository *repo; git_buf loose_path = GIT_BUF_INIT; struct packref *pack_ref; khiter_t pack_ref_pos; - int error = 0, pack_error; - bool loose_deleted; + int error = 0; + bool loose_deleted = 0; assert(_backend); - assert(ref); + assert(ref_name); backend = (refdb_fs_backend *)_backend; - repo = backend->repo; /* If a loose reference exists, remove it from the filesystem */ - - if (git_buf_joinpath(&loose_path, repo->path_repository, ref->name) < 0) + if (git_buf_joinpath(&loose_path, backend->path, ref_name) < 0) return -1; if (git_path_isfile(loose_path.ptr)) { error = p_unlink(loose_path.ptr); loose_deleted = 1; } - + git_buf_free(&loose_path); if (error != 0) return error; /* If a packed reference exists, remove it from the packfile and repack */ + error = packed_map_entry(&pack_ref, &pack_ref_pos, backend, ref_name); - if ((pack_error = packed_map_entry(&pack_ref, &pack_ref_pos, backend, ref->name)) == 0) { + if (error == GIT_ENOTFOUND) + return loose_deleted ? 0 : GIT_ENOTFOUND; + + if (error == 0) { git_strmap_delete_at(backend->refcache.packfile, pack_ref_pos); git__free(pack_ref); - error = packed_write(backend); } - - if (pack_error == GIT_ENOTFOUND) - error = loose_deleted ? 0 : GIT_ENOTFOUND; - else - error = pack_error; return error; } +static int refdb_fs_backend__rename( + git_reference **out, + git_refdb_backend *_backend, + const char *old_name, + const char *new_name, + int force) +{ + refdb_fs_backend *backend; + git_reference *old, *new; + int error; + + assert(_backend); + backend = (refdb_fs_backend *)_backend; + + error = reference_path_available(backend, new_name, old_name, force); + if (error < 0) + return error; + + error = refdb_fs_backend__lookup(&old, _backend, old_name); + if (error < 0) + return error; + + error = refdb_fs_backend__delete(_backend, old_name); + if (error < 0) { + git_reference_free(old); + return error; + } + + new = realloc(old, sizeof(git_reference) + strlen(new_name) + 1); + memcpy(new->name, new_name, strlen(new_name) + 1); + + error = loose_write(backend, new); + if (error < 0) { + git_reference_free(new); + return error; + } + + if (out) { + *out = new; + } else { + git_reference_free(new); + } + + return 0; +} + static int refdb_fs_backend__compress(git_refdb_backend *_backend) { refdb_fs_backend *backend; @@ -993,28 +1220,76 @@ static void refdb_fs_backend__free(git_refdb_backend *_backend) backend = (refdb_fs_backend *)_backend; refcache_free(&backend->refcache); + git__free(backend->path); git__free(backend); } +static int setup_namespace(git_buf *path, git_repository *repo) +{ + char *parts, *start, *end; + + /* Not all repositories have a path */ + if (repo->path_repository == NULL) + return 0; + + /* Load the path to the repo first */ + git_buf_puts(path, repo->path_repository); + + /* if the repo is not namespaced, nothing else to do */ + if (repo->namespace == NULL) + return 0; + + parts = end = git__strdup(repo->namespace); + if (parts == NULL) + return -1; + + /** + * From `man gitnamespaces`: + * namespaces which include a / will expand to a hierarchy + * of namespaces; for example, GIT_NAMESPACE=foo/bar will store + * refs under refs/namespaces/foo/refs/namespaces/bar/ + */ + while ((start = git__strsep(&end, "/")) != NULL) { + git_buf_printf(path, "refs/namespaces/%s/", start); + } + + git_buf_printf(path, "refs/namespaces/%s/refs", end); + git__free(parts); + + /* Make sure that the folder with the namespace exists */ + if (git_futils_mkdir_r(git_buf_cstr(path), repo->path_repository, 0777) < 0) + return -1; + + /* Return the root of the namespaced path, i.e. without the trailing '/refs' */ + git_buf_rtruncate_at_char(path, '/'); + return 0; +} + int git_refdb_backend_fs( git_refdb_backend **backend_out, - git_repository *repository, - git_refdb *refdb) + git_repository *repository) { + git_buf path = GIT_BUF_INIT; refdb_fs_backend *backend; backend = git__calloc(1, sizeof(refdb_fs_backend)); GITERR_CHECK_ALLOC(backend); backend->repo = repository; - backend->path = repository->path_repository; - backend->refdb = refdb; + + if (setup_namespace(&path, repository) < 0) { + git__free(backend); + return -1; + } + + backend->path = git_buf_detach(&path); backend->parent.exists = &refdb_fs_backend__exists; backend->parent.lookup = &refdb_fs_backend__lookup; - backend->parent.foreach = &refdb_fs_backend__foreach; + backend->parent.iterator = &refdb_fs_backend__iterator; backend->parent.write = &refdb_fs_backend__write; backend->parent.delete = &refdb_fs_backend__delete; + backend->parent.rename = &refdb_fs_backend__rename; backend->parent.compress = &refdb_fs_backend__compress; backend->parent.free = &refdb_fs_backend__free; diff --git a/src/reflog.c b/src/reflog.c index 8c133fe53..4cc20d2c7 100644 --- a/src/reflog.c +++ b/src/reflog.c @@ -483,8 +483,10 @@ int git_reflog_drop( entry = (git_reflog_entry *)git_reflog_entry_byindex(reflog, idx); - if (entry == NULL) + if (entry == NULL) { + giterr_set(GITERR_REFERENCE, "No reflog entry at index "PRIuZ, idx); return GIT_ENOTFOUND; + } reflog_entry_free(entry); diff --git a/src/refs.c b/src/refs.c index b1f679632..c0e460cc3 100644 --- a/src/refs.c +++ b/src/refs.c @@ -9,6 +9,7 @@ #include "hash.h" #include "repository.h" #include "fileops.h" +#include "filebuf.h" #include "pack.h" #include "reflog.h" #include "refdb.h" @@ -19,7 +20,7 @@ #include <git2/branch.h> #include <git2/refs.h> #include <git2/refdb.h> -#include <git2/refdb_backend.h> +#include <git2/sys/refs.h> GIT__USE_STRMAP; @@ -31,171 +32,79 @@ enum { GIT_PACKREF_WAS_LOOSE = 2 }; - -git_reference *git_reference__alloc( - git_refdb *refdb, - const char *name, - const git_oid *oid, - const char *symbolic) +static git_reference *alloc_ref(const char *name) { git_reference *ref; - size_t namelen; - - assert(refdb && name && ((oid && !symbolic) || (!oid && symbolic))); - - namelen = strlen(name); + size_t namelen = strlen(name); if ((ref = git__calloc(1, sizeof(git_reference) + namelen + 1)) == NULL) return NULL; - if (oid) { - ref->type = GIT_REF_OID; - git_oid_cpy(&ref->target.oid, oid); - } else { - ref->type = GIT_REF_SYMBOLIC; - - if ((ref->target.symbolic = git__strdup(symbolic)) == NULL) { - git__free(ref); - return NULL; - } - } - - ref->db = refdb; memcpy(ref->name, name, namelen + 1); return ref; } -void git_reference_free(git_reference *reference) +git_reference *git_reference__alloc_symbolic( + const char *name, const char *target) { - if (reference == NULL) - return; - - if (reference->type == GIT_REF_SYMBOLIC) { - git__free(reference->target.symbolic); - reference->target.symbolic = NULL; - } - - reference->db = NULL; - reference->type = GIT_REF_INVALID; - - git__free(reference); -} + git_reference *ref; -struct reference_available_t { - const char *new_ref; - const char *old_ref; - int available; -}; + assert(name && target); -static int _reference_available_cb(const char *ref, void *data) -{ - struct reference_available_t *d; - - assert(ref && data); - d = (struct reference_available_t *)data; + ref = alloc_ref(name); + if (!ref) + return NULL; - if (!d->old_ref || strcmp(d->old_ref, ref)) { - size_t reflen = strlen(ref); - size_t newlen = strlen(d->new_ref); - size_t cmplen = reflen < newlen ? reflen : newlen; - const char *lead = reflen < newlen ? d->new_ref : ref; + ref->type = GIT_REF_SYMBOLIC; - if (!strncmp(d->new_ref, ref, cmplen) && lead[cmplen] == '/') { - d->available = 0; - return -1; - } + if ((ref->target.symbolic = git__strdup(target)) == NULL) { + git__free(ref); + return NULL; } - return 0; + return ref; } -static int reference_path_available( - git_repository *repo, - const char *ref, - const char* old_ref) +git_reference *git_reference__alloc( + const char *name, + const git_oid *oid, + const git_oid *peel) { - int error; - struct reference_available_t data; + git_reference *ref; - data.new_ref = ref; - data.old_ref = old_ref; - data.available = 1; + assert(name && oid); - error = git_reference_foreach( - repo, GIT_REF_LISTALL, _reference_available_cb, (void *)&data); - if (error < 0) - return error; + ref = alloc_ref(name); + if (!ref) + return NULL; - if (!data.available) { - giterr_set(GITERR_REFERENCE, - "The path to reference '%s' collides with an existing one", ref); - return -1; - } + ref->type = GIT_REF_OID; + git_oid_cpy(&ref->target.oid, oid); - return 0; + if (peel != NULL) + git_oid_cpy(&ref->peel, peel); + + return ref; } -/* - * Check if a reference could be written to disk, based on: - * - * - Whether a reference with the same name already exists, - * and we are allowing or disallowing overwrites - * - * - Whether the name of the reference would collide with - * an existing path - */ -static int reference_can_write( - git_repository *repo, - const char *refname, - const char *previous_name, - int force) +void git_reference_free(git_reference *reference) { - git_refdb *refdb; - - if (git_repository_refdb__weakptr(&refdb, repo) < 0) - return -1; - - /* see if the reference shares a path with an existing reference; - * if a path is shared, we cannot create the reference, even when forcing */ - if (reference_path_available(repo, refname, previous_name) < 0) - return -1; - - /* check if the reference actually exists, but only if we are not forcing - * the rename. If we are forcing, it's OK to overwrite */ - if (!force) { - int exists; - - if (git_refdb_exists(&exists, refdb, refname) < 0) - return -1; + if (reference == NULL) + return; - /* We cannot proceed if the reference already exists and we're not forcing - * the rename; the existing one would be overwritten */ - if (exists) { - giterr_set(GITERR_REFERENCE, - "A reference with that name (%s) already exists", refname); - return GIT_EEXISTS; - } - } + if (reference->type == GIT_REF_SYMBOLIC) + git__free(reference->target.symbolic); - /* FIXME: if the reference exists and we are forcing, do we really need to - * remove the reference first? - * - * Two cases: - * - * - the reference already exists and is loose: not a problem, the file - * gets overwritten on disk - * - * - the reference already exists and is packed: we write a new one as - * loose, which by all means renders the packed one useless - */ + if (reference->db) + GIT_REFCOUNT_DEC(reference->db, git_refdb__free); - return 0; + git__free(reference); } int git_reference_delete(git_reference *ref) { - return git_refdb_delete(ref->db, ref); + return git_refdb_delete(ref->db, ref->name); } int git_reference_lookup(git_reference **ref_out, @@ -238,10 +147,10 @@ int git_reference_lookup_resolved( max_nesting = MAX_NESTING_LEVEL; else if (max_nesting < 0) max_nesting = DEFAULT_NESTING_LEVEL; - + strncpy(scan_name, name, GIT_REFNAME_MAX); scan_type = GIT_REF_SYMBOLIC; - + if ((error = git_repository_refdb__weakptr(&refdb, repo)) < 0) return -1; @@ -259,7 +168,7 @@ int git_reference_lookup_resolved( if ((error = git_refdb_lookup(&ref, refdb, scan_name)) < 0) return error; - + scan_type = ref->type; } @@ -274,6 +183,67 @@ int git_reference_lookup_resolved( return 0; } +int git_reference_dwim(git_reference **out, git_repository *repo, const char *refname) +{ + int error = 0, i; + bool fallbackmode = true, foundvalid = false; + git_reference *ref; + git_buf refnamebuf = GIT_BUF_INIT, name = GIT_BUF_INIT; + + static const char* formatters[] = { + "%s", + GIT_REFS_DIR "%s", + GIT_REFS_TAGS_DIR "%s", + GIT_REFS_HEADS_DIR "%s", + GIT_REFS_REMOTES_DIR "%s", + GIT_REFS_REMOTES_DIR "%s/" GIT_HEAD_FILE, + NULL + }; + + if (*refname) + git_buf_puts(&name, refname); + else { + git_buf_puts(&name, GIT_HEAD_FILE); + fallbackmode = false; + } + + for (i = 0; formatters[i] && (fallbackmode || i == 0); i++) { + + git_buf_clear(&refnamebuf); + + if ((error = git_buf_printf(&refnamebuf, formatters[i], git_buf_cstr(&name))) < 0) + goto cleanup; + + if (!git_reference_is_valid_name(git_buf_cstr(&refnamebuf))) { + error = GIT_EINVALIDSPEC; + continue; + } + foundvalid = true; + + error = git_reference_lookup_resolved(&ref, repo, git_buf_cstr(&refnamebuf), -1); + + if (!error) { + *out = ref; + error = 0; + goto cleanup; + } + + if (error != GIT_ENOTFOUND) + goto cleanup; + } + +cleanup: + if (error && !foundvalid) { + /* never found a valid reference name */ + giterr_set(GITERR_REFERENCE, + "Could not use '%s' as valid reference name", git_buf_cstr(&name)); + } + + git_buf_free(&name); + git_buf_free(&refnamebuf); + return error; +} + /** * Getters */ @@ -305,6 +275,16 @@ const git_oid *git_reference_target(const git_reference *ref) return &ref->target.oid; } +const git_oid *git_reference_target_peel(const git_reference *ref) +{ + assert(ref); + + if (ref->type != GIT_REF_OID || git_oid_iszero(&ref->peel)) + return NULL; + + return &ref->peel; +} + const char *git_reference_symbolic_target(const git_reference *ref) { assert(ref); @@ -327,23 +307,32 @@ static int reference__create( git_refdb *refdb; git_reference *ref = NULL; int error = 0; - + if (ref_out) *ref_out = NULL; - if ((error = git_reference__normalize_name_lax(normalized, sizeof(normalized), name)) < 0 || - (error = reference_can_write(repo, normalized, NULL, force)) < 0 || - (error = git_repository_refdb__weakptr(&refdb, repo)) < 0) + error = git_reference__normalize_name_lax(normalized, sizeof(normalized), name); + if (error < 0) + return error; + + error = git_repository_refdb__weakptr(&refdb, repo); + if (error < 0) return error; - - if ((ref = git_reference__alloc(refdb, name, oid, symbolic)) == NULL) - return -1; - if ((error = git_refdb_write(refdb, ref)) < 0) { + if (oid != NULL) { + assert(symbolic == NULL); + ref = git_reference__alloc(normalized, oid, NULL); + } else { + ref = git_reference__alloc_symbolic(normalized, symbolic); + } + + GITERR_CHECK_ALLOC(ref); + + if ((error = git_refdb_write(refdb, ref, force)) < 0) { git_reference_free(ref); return error; } - + if (ref_out == NULL) git_reference_free(ref); else @@ -363,17 +352,17 @@ int git_reference_create( int error = 0; assert(repo && name && oid); - + /* Sanity check the reference being created - target must exist. */ if ((error = git_repository_odb__weakptr(&odb, repo)) < 0) return error; - + if (!git_odb_exists(odb, oid)) { giterr_set(GITERR_REFERENCE, "Target OID for the reference doesn't exist on the repository"); return -1; } - + return reference__create(ref_out, repo, name, oid, NULL, force); } @@ -388,7 +377,7 @@ int git_reference_symbolic_create( int error = 0; assert(repo && name && target); - + if ((error = git_reference__normalize_name_lax( normalized, sizeof(normalized), target)) < 0) return error; @@ -402,7 +391,7 @@ int git_reference_set_target( const git_oid *id) { assert(out && ref && id); - + if (ref->type != GIT_REF_OID) { giterr_set(GITERR_REFERENCE, "Cannot set OID on symbolic reference"); return -1; @@ -417,13 +406,13 @@ int git_reference_symbolic_set_target( const char *target) { assert(out && ref && target); - + if (ref->type != GIT_REF_SYMBOLIC) { giterr_set(GITERR_REFERENCE, "Cannot set symbolic target on a direct reference"); return -1; } - + return git_reference_symbolic_create(out, ref->db->repo, ref->name, target, 1); } @@ -436,96 +425,174 @@ int git_reference_rename( unsigned int normalization_flags; char normalized[GIT_REFNAME_MAX]; bool should_head_be_updated = false; - git_reference *result = NULL; - git_oid *oid; - const char *symbolic; int error = 0; int reference_has_log; - - *out = NULL; normalization_flags = ref->type == GIT_REF_SYMBOLIC ? GIT_REF_FORMAT_ALLOW_ONELEVEL : GIT_REF_FORMAT_NORMAL; - if ((error = git_reference_normalize_name(normalized, sizeof(normalized), new_name, normalization_flags)) < 0 || - (error = reference_can_write(ref->db->repo, normalized, ref->name, force)) < 0) + if ((error = git_reference_normalize_name( + normalized, sizeof(normalized), new_name, normalization_flags)) < 0) return error; - /* - * Create the new reference. - */ - if (ref->type == GIT_REF_OID) { - oid = &ref->target.oid; - symbolic = NULL; - } else { - oid = NULL; - symbolic = ref->target.symbolic; - } - - if ((result = git_reference__alloc(ref->db, new_name, oid, symbolic)) == NULL) - return -1; - /* Check if we have to update HEAD. */ if ((error = git_branch_is_head(ref)) < 0) - goto on_error; + return error; should_head_be_updated = (error > 0); - /* Now delete the old ref and save the new one. */ - if ((error = git_refdb_delete(ref->db, ref)) < 0) - goto on_error; - - /* Save the new reference. */ - if ((error = git_refdb_write(ref->db, result)) < 0) - goto rollback; - + if ((error = git_refdb_rename(out, ref->db, ref->name, new_name, force)) < 0) + return error; + /* Update HEAD it was poiting to the reference being renamed. */ - if (should_head_be_updated && (error = git_repository_set_head(ref->db->repo, new_name)) < 0) { + if (should_head_be_updated && + (error = git_repository_set_head(ref->db->repo, new_name)) < 0) { giterr_set(GITERR_REFERENCE, "Failed to update HEAD after renaming reference"); - goto on_error; + return error; } /* Rename the reflog file, if it exists. */ reference_has_log = git_reference_has_log(ref); - if (reference_has_log < 0) { - error = reference_has_log; - goto on_error; - } + if (reference_has_log < 0) + return reference_has_log; + if (reference_has_log && (error = git_reflog_rename(ref, new_name)) < 0) - goto on_error; + return error; - *out = result; + return 0; +} - return error; +int git_reference_resolve(git_reference **ref_out, const git_reference *ref) +{ + switch (git_reference_type(ref)) { + case GIT_REF_OID: + return git_reference_lookup(ref_out, ref->db->repo, ref->name); + + case GIT_REF_SYMBOLIC: + return git_reference_lookup_resolved(ref_out, ref->db->repo, ref->target.symbolic, -1); -rollback: - git_refdb_write(ref->db, ref); + default: + giterr_set(GITERR_REFERENCE, "Invalid reference"); + return -1; + } +} -on_error: - git_reference_free(result); +int git_reference_foreach( + git_repository *repo, + git_reference_foreach_cb callback, + void *payload) +{ + git_reference_iterator *iter; + git_reference *ref; + int error; + if (git_reference_iterator_new(&iter, repo) < 0) + return -1; + + while ((error = git_reference_next(&ref, iter)) == 0) { + if (callback(ref, payload)) { + error = GIT_EUSER; + goto out; + } + } + + if (error == GIT_ITEROVER) + error = 0; + +out: + git_reference_iterator_free(iter); return error; } -int git_reference_resolve(git_reference **ref_out, const git_reference *ref) +int git_reference_foreach_name( + git_repository *repo, + git_reference_foreach_name_cb callback, + void *payload) { - if (ref->type == GIT_REF_OID) - return git_reference_lookup(ref_out, ref->db->repo, ref->name); - else - return git_reference_lookup_resolved(ref_out, ref->db->repo, - ref->target.symbolic, -1); + git_reference_iterator *iter; + const char *refname; + int error; + + if (git_reference_iterator_new(&iter, repo) < 0) + return -1; + + while ((error = git_reference_next_name(&refname, iter)) == 0) { + if (callback(refname, payload)) { + error = GIT_EUSER; + goto out; + } + } + + if (error == GIT_ITEROVER) + error = 0; + +out: + git_reference_iterator_free(iter); + return error; } -int git_reference_foreach( +int git_reference_foreach_glob( git_repository *repo, - unsigned int list_flags, - git_reference_foreach_cb callback, + const char *glob, + git_reference_foreach_name_cb callback, void *payload) { + git_reference_iterator *iter; + const char *refname; + int error; + + if (git_reference_iterator_glob_new(&iter, repo, glob) < 0) + return -1; + + while ((error = git_reference_next_name(&refname, iter)) == 0) { + if (callback(refname, payload)) { + error = GIT_EUSER; + goto out; + } + } + + if (error == GIT_ITEROVER) + error = 0; + +out: + git_reference_iterator_free(iter); + return error; +} + +int git_reference_iterator_new(git_reference_iterator **out, git_repository *repo) +{ git_refdb *refdb; - git_repository_refdb__weakptr(&refdb, repo); - return git_refdb_foreach(refdb, list_flags, callback, payload); + if (git_repository_refdb__weakptr(&refdb, repo) < 0) + return -1; + + return git_refdb_iterator(out, refdb, NULL); +} + +int git_reference_iterator_glob_new( + git_reference_iterator **out, git_repository *repo, const char *glob) +{ + git_refdb *refdb; + + if (git_repository_refdb__weakptr(&refdb, repo) < 0) + return -1; + + return git_refdb_iterator(out, refdb, glob); +} + +int git_reference_next(git_reference **out, git_reference_iterator *iter) +{ + return git_refdb_iterator_next(out, iter); +} + +int git_reference_next_name(const char **out, git_reference_iterator *iter) +{ + return git_refdb_iterator_next_name(out, iter); +} + +void git_reference_iterator_free(git_reference_iterator *iter) +{ + git_refdb_iterator_free(iter); } static int cb__reflist_add(const char *ref, void *data) @@ -535,8 +602,7 @@ static int cb__reflist_add(const char *ref, void *data) int git_reference_list( git_strarray *array, - git_repository *repo, - unsigned int list_flags) + git_repository *repo) { git_vector ref_list; @@ -548,8 +614,8 @@ int git_reference_list( if (git_vector_init(&ref_list, 8, NULL) < 0) return -1; - if (git_reference_foreach( - repo, list_flags, &cb__reflist_add, (void *)&ref_list) < 0) { + if (git_reference_foreach_name( + repo, &cb__reflist_add, (void *)&ref_list) < 0) { git_vector_free(&ref_list); return -1; } @@ -712,6 +778,7 @@ int git_reference__normalize_name( goto cleanup; if ((segments_count == 1 ) && + !(flags & GIT_REF_FORMAT_REFSPEC_SHORTHAND) && !(is_all_caps_and_underscore(name, (size_t)segment_len) || ((flags & GIT_REF_FORMAT_REFSPEC_PATTERN) && !strcmp("*", name)))) goto cleanup; @@ -778,16 +845,20 @@ int git_reference__normalize_name_lax( int git_reference_cmp(git_reference *ref1, git_reference *ref2) { + git_ref_t type1, type2; assert(ref1 && ref2); + type1 = git_reference_type(ref1); + type2 = git_reference_type(ref2); + /* let's put symbolic refs before OIDs */ - if (ref1->type != ref2->type) - return (ref1->type == GIT_REF_SYMBOLIC) ? -1 : 1; + if (type1 != type2) + return (type1 == GIT_REF_SYMBOLIC) ? -1 : 1; - if (ref1->type == GIT_REF_SYMBOLIC) + if (type1 == GIT_REF_SYMBOLIC) return strcmp(ref1->target.symbolic, ref2->target.symbolic); - return git_oid_cmp(&ref1->target.oid, &ref2->target.oid); + return git_oid__cmp(&ref1->target.oid, &ref2->target.oid); } static int reference__update_terminal( @@ -799,9 +870,11 @@ static int reference__update_terminal( git_reference *ref; int error = 0; - if (nesting > MAX_NESTING_LEVEL) + if (nesting > MAX_NESTING_LEVEL) { + giterr_set(GITERR_REFERENCE, "Reference chain too deep (%d)", nesting); return GIT_ENOTFOUND; - + } + error = git_reference_lookup(&ref, repo, ref_name); /* If we haven't found the reference at all, create a new reference. */ @@ -809,10 +882,10 @@ static int reference__update_terminal( giterr_clear(); return git_reference_create(NULL, repo, ref_name, oid, 0); } - + if (error < 0) return error; - + /* If the ref is a symbolic reference, follow its target. */ if (git_reference_type(ref) == GIT_REF_SYMBOLIC) { error = reference__update_terminal(repo, git_reference_symbolic_target(ref), oid, @@ -822,7 +895,7 @@ static int reference__update_terminal( git_reference_free(ref); error = git_reference_create(NULL, repo, ref_name, oid, 1); } - + return error; } @@ -839,24 +912,6 @@ int git_reference__update_terminal( return reference__update_terminal(repo, ref_name, oid, 0); } -int git_reference_foreach_glob( - git_repository *repo, - const char *glob, - unsigned int list_flags, - int (*callback)( - const char *reference_name, - void *payload), - void *payload) -{ - git_refdb *refdb; - - assert(repo && glob && callback); - - git_repository_refdb__weakptr(&refdb, repo); - - return git_refdb_foreach_glob(refdb, glob, list_flags, callback, payload); -} - int git_reference_has_log( git_reference *ref) { @@ -905,15 +960,6 @@ static int peel_error(int error, git_reference *ref, const char* msg) return error; } -static int reference_target(git_object **object, git_reference *ref) -{ - const git_oid *oid; - - oid = git_reference_target(ref); - - return git_object_lookup(object, git_reference_owner(ref), oid, GIT_OBJ_ANY); -} - int git_reference_peel( git_object **peeled, git_reference *ref, @@ -925,10 +971,22 @@ int git_reference_peel( assert(ref); - if ((error = git_reference_resolve(&resolved, ref)) < 0) - return peel_error(error, ref, "Cannot resolve reference"); + if (ref->type == GIT_REF_OID) { + resolved = ref; + } else { + if ((error = git_reference_resolve(&resolved, ref)) < 0) + return peel_error(error, ref, "Cannot resolve reference"); + } - if ((error = reference_target(&target, resolved)) < 0) { + if (!git_oid_iszero(&resolved->peel)) { + error = git_object_lookup(&target, + git_reference_owner(ref), &resolved->peel, GIT_OBJ_ANY); + } else { + error = git_object_lookup(&target, + git_reference_owner(ref), &resolved->target.oid, GIT_OBJ_ANY); + } + + if (error < 0) { peel_error(error, ref, "Cannot retrieve reference target"); goto cleanup; } @@ -940,7 +998,10 @@ int git_reference_peel( cleanup: git_object_free(target); - git_reference_free(resolved); + + if (resolved != ref) + git_reference_free(resolved); + return error; } @@ -963,3 +1024,20 @@ int git_reference_is_valid_name( refname, GIT_REF_FORMAT_ALLOW_ONELEVEL); } + +const char *git_reference_shorthand(git_reference *ref) +{ + const char *name = ref->name; + + if (!git__prefixcmp(name, GIT_REFS_HEADS_DIR)) + return name + strlen(GIT_REFS_HEADS_DIR); + else if (!git__prefixcmp(name, GIT_REFS_TAGS_DIR)) + return name + strlen(GIT_REFS_TAGS_DIR); + else if (!git__prefixcmp(name, GIT_REFS_REMOTES_DIR)) + return name + strlen(GIT_REFS_REMOTES_DIR); + else if (!git__prefixcmp(name, GIT_REFS_DIR)) + return name + strlen(GIT_REFS_DIR); + + /* No shorthands are avaiable, so just return the name */ + return name; +} diff --git a/src/refs.h b/src/refs.h index 7d63c3fbd..f487ee3fc 100644 --- a/src/refs.h +++ b/src/refs.h @@ -13,6 +13,7 @@ #include "git2/refdb.h" #include "strmap.h" #include "buffer.h" +#include "oid.h" #define GIT_REFS_DIR "refs/" #define GIT_REFS_HEADS_DIR GIT_REFS_DIR "heads/" @@ -25,7 +26,7 @@ #define GIT_SYMREF "ref: " #define GIT_PACKEDREFS_FILE "packed-refs" -#define GIT_PACKEDREFS_HEADER "# pack-refs with: peeled " +#define GIT_PACKEDREFS_HEADER "# pack-refs with: peeled fully-peeled " #define GIT_PACKEDREFS_FILE_MODE 0666 #define GIT_HEAD_FILE "HEAD" @@ -49,14 +50,14 @@ struct git_reference { git_refdb *db; - git_ref_t type; union { git_oid oid; char *symbolic; } target; - + + git_oid peel; char name[0]; }; diff --git a/src/refspec.c b/src/refspec.c index a51b0cfab..492c6ed3f 100644 --- a/src/refspec.c +++ b/src/refspec.c @@ -25,6 +25,7 @@ int git_refspec__parse(git_refspec *refspec, const char *input, bool is_fetch) assert(refspec && input); memset(refspec, 0x0, sizeof(git_refspec)); + refspec->push = !is_fetch; lhs = input; if (*lhs == '+') { @@ -59,7 +60,7 @@ int git_refspec__parse(git_refspec *refspec, const char *input, bool is_fetch) refspec->pattern = is_glob; refspec->src = git__strndup(lhs, llen); - flags = GIT_REF_FORMAT_ALLOW_ONELEVEL + flags = GIT_REF_FORMAT_ALLOW_ONELEVEL | GIT_REF_FORMAT_REFSPEC_SHORTHAND | (is_glob ? GIT_REF_FORMAT_REFSPEC_PATTERN : 0); if (is_fetch) { @@ -119,6 +120,9 @@ int git_refspec__parse(git_refspec *refspec, const char *input, bool is_fetch) } } + refspec->string = git__strdup(input); + GITERR_CHECK_ALLOC(refspec->string); + return 0; invalid: @@ -132,6 +136,7 @@ void git_refspec__free(git_refspec *refspec) git__free(refspec->src); git__free(refspec->dst); + git__free(refspec->string); } const char *git_refspec_src(const git_refspec *refspec) @@ -144,6 +149,11 @@ const char *git_refspec_dst(const git_refspec *refspec) return refspec == NULL ? NULL : refspec->dst; } +const char *git_refspec_string(const git_refspec *refspec) +{ + return refspec == NULL ? NULL : refspec->string; +} + int git_refspec_force(const git_refspec *refspec) { assert(refspec); @@ -215,25 +225,31 @@ int git_refspec_rtransform(char *out, size_t outlen, const git_refspec *spec, co return refspec_transform_internal(out, outlen, spec->dst, spec->src, name); } -static int refspec_transform(git_buf *out, const char *from, const char *to, const char *name) +static int refspec_transform( + git_buf *out, const char *from, const char *to, const char *name) { - if (git_buf_sets(out, to) < 0) - return -1; + size_t to_len = to ? strlen(to) : 0; + size_t from_len = from ? strlen(from) : 0; + size_t name_len = name ? strlen(name) : 0; - /* - * No '*' at the end means that it's mapped to one specific - * branch, so no actual transformation is needed. - */ - if (git_buf_len(out) > 0 && out->ptr[git_buf_len(out) - 1] != '*') - return 0; + if (git_buf_set(out, to, to_len) < 0) + return -1; - git_buf_truncate(out, git_buf_len(out) - 1); /* remove trailing '*' */ - git_buf_puts(out, name + strlen(from) - 1); + if (to_len > 0) { + /* No '*' at the end of 'to' means that refspec is mapped to one + * specific branch, so no actual transformation is needed. + */ + if (out->ptr[to_len - 1] != '*') + return 0; + git_buf_shorten(out, 1); /* remove trailing '*' copied from 'to' */ + } - if (git_buf_oom(out)) - return -1; + if (from_len > 0) /* ignore trailing '*' from 'from' */ + from_len--; + if (from_len > name_len) + from_len = name_len; - return 0; + return git_buf_put(out, name + from_len, name_len - from_len); } int git_refspec_transform_r(git_buf *out, const git_refspec *spec, const char *name) @@ -264,3 +280,10 @@ int git_refspec_is_wildcard(const git_refspec *spec) return (spec->src[strlen(spec->src) - 1] == '*'); } + +git_direction git_refspec_direction(const git_refspec *spec) +{ + assert(spec); + + return spec->push; +} diff --git a/src/refspec.h b/src/refspec.h index a7a4dd834..44d484c7b 100644 --- a/src/refspec.h +++ b/src/refspec.h @@ -11,11 +11,13 @@ #include "buffer.h" struct git_refspec { - struct git_refspec *next; + char *string; char *src; char *dst; unsigned int force :1, + push : 1, pattern :1, + dwim :1, matching :1; }; diff --git a/src/remote.c b/src/remote.c index 896361e30..158f3e938 100644 --- a/src/remote.c +++ b/src/remote.c @@ -8,6 +8,7 @@ #include "git2/config.h" #include "git2/types.h" #include "git2/oid.h" +#include "git2/net.h" #include "config.h" #include "repository.h" @@ -19,15 +20,26 @@ #include <regex.h> -static int parse_remote_refspec(git_config *cfg, git_refspec *refspec, const char *var, bool is_fetch) +static int add_refspec(git_remote *remote, const char *string, bool is_fetch) { - int error; - const char *val; + git_refspec *spec; - if ((error = git_config_get_string(&val, cfg, var)) < 0) - return error; + spec = git__calloc(1, sizeof(git_refspec)); + GITERR_CHECK_ALLOC(spec); + + if (git_refspec__parse(spec, string, is_fetch) < 0) { + git__free(spec); + return -1; + } - return git_refspec__parse(refspec, val, is_fetch); + spec->push = !is_fetch; + if (git_vector_insert(&remote->refspecs, spec) < 0) { + git_refspec__free(spec); + git__free(spec); + return -1; + } + + return 0; } static int download_tags_value(git_remote *remote, git_config *cfg) @@ -36,11 +48,7 @@ static int download_tags_value(git_remote *remote, git_config *cfg) git_buf buf = GIT_BUF_INIT; int error; - if (remote->download_tags != GIT_REMOTE_DOWNLOAD_TAGS_UNSET) - return 0; - - /* This is the default, let's see if we need to change it */ - remote->download_tags = GIT_REMOTE_DOWNLOAD_TAGS_AUTO; + /* The 0 value is the default (auto), let's see if we need to change it */ if (git_buf_printf(&buf, "remote.%s.tagopt", remote->name) < 0) return -1; @@ -51,8 +59,10 @@ static int download_tags_value(git_remote *remote, git_config *cfg) else if (!error && !strcmp(val, "--tags")) remote->download_tags = GIT_REMOTE_DOWNLOAD_TAGS_ALL; - if (error == GIT_ENOTFOUND) + if (error == GIT_ENOTFOUND) { + giterr_clear(); error = 0; + } return error; } @@ -99,14 +109,13 @@ static int create_internal(git_remote **out, git_repository *repo, const char *n } if (fetch != NULL) { - if (git_refspec__parse(&remote->fetch, fetch, true) < 0) + if (add_refspec(remote, fetch, true) < 0) goto on_error; } - /* A remote without a name doesn't download tags */ - if (!name) { + if (!name) + /* A remote without a name doesn't download tags */ remote->download_tags = GIT_REMOTE_DOWNLOAD_TAGS_NONE; - } *out = remote; git_buf_free(&fetchbuf); @@ -186,6 +195,43 @@ int git_remote_create_inmemory(git_remote **out, git_repository *repo, const cha return 0; } +struct refspec_cb_data { + git_remote *remote; + int fetch; +}; + +static int refspec_cb(const git_config_entry *entry, void *payload) +{ + const struct refspec_cb_data *data = (struct refspec_cb_data *)payload; + + return add_refspec(data->remote, entry->value, data->fetch); +} + +static int get_optional_config( + git_config *config, git_buf *buf, git_config_foreach_cb cb, void *payload) +{ + int error = 0; + const char *key = git_buf_cstr(buf); + + if (git_buf_oom(buf)) + return -1; + + if (cb != NULL) + error = git_config_get_multivar(config, key, NULL, cb, payload); + else + error = git_config_get_string(payload, config, key); + + if (error == GIT_ENOTFOUND) { + giterr_clear(); + error = 0; + } + + if (error < 0) + error = -1; + + return error; +} + int git_remote_load(git_remote **out, git_repository *repo, const char *name) { git_remote *remote; @@ -193,6 +239,7 @@ int git_remote_load(git_remote **out, git_repository *repo, const char *name) const char *val; int error = 0; git_config *config; + struct refspec_cb_data data; assert(out && repo && name); @@ -211,7 +258,8 @@ int git_remote_load(git_remote **out, git_repository *repo, const char *name) remote->name = git__strdup(name); GITERR_CHECK_ALLOC(remote->name); - if (git_vector_init(&remote->refs, 32, NULL) < 0) { + if ((git_vector_init(&remote->refs, 32, NULL) < 0) || + (git_vector_init(&remote->refspecs, 2, NULL))) { error = -1; goto cleanup; } @@ -223,7 +271,7 @@ int git_remote_load(git_remote **out, git_repository *repo, const char *name) if ((error = git_config_get_string(&val, config, git_buf_cstr(&buf))) < 0) goto cleanup; - + if (strlen(val) == 0) { giterr_set(GITERR_INVALID, "Malformed remote '%s' - missing URL", name); error = -1; @@ -234,57 +282,32 @@ int git_remote_load(git_remote **out, git_repository *repo, const char *name) remote->url = git__strdup(val); GITERR_CHECK_ALLOC(remote->url); + val = NULL; git_buf_clear(&buf); - if (git_buf_printf(&buf, "remote.%s.pushurl", name) < 0) { - error = -1; - goto cleanup; - } - - error = git_config_get_string(&val, config, git_buf_cstr(&buf)); - if (error == GIT_ENOTFOUND) { - val = NULL; - error = 0; - } + git_buf_printf(&buf, "remote.%s.pushurl", name); - if (error < 0) { - error = -1; + if ((error = get_optional_config(config, &buf, NULL, (void *)&val)) < 0) goto cleanup; - } if (val) { remote->pushurl = git__strdup(val); GITERR_CHECK_ALLOC(remote->pushurl); } + data.remote = remote; + data.fetch = true; git_buf_clear(&buf); - if (git_buf_printf(&buf, "remote.%s.fetch", name) < 0) { - error = -1; - goto cleanup; - } + git_buf_printf(&buf, "remote.%s.fetch", name); - error = parse_remote_refspec(config, &remote->fetch, git_buf_cstr(&buf), true); - if (error == GIT_ENOTFOUND) - error = 0; - - if (error < 0) { - error = -1; + if ((error = get_optional_config(config, &buf, refspec_cb, &data)) < 0) goto cleanup; - } + data.fetch = false; git_buf_clear(&buf); - if (git_buf_printf(&buf, "remote.%s.push", name) < 0) { - error = -1; - goto cleanup; - } + git_buf_printf(&buf, "remote.%s.push", name); - error = parse_remote_refspec(config, &remote->push, git_buf_cstr(&buf), false); - if (error == GIT_ENOTFOUND) - error = 0; - - if (error < 0) { - error = -1; + if ((error = get_optional_config(config, &buf, refspec_cb, &data)) < 0) goto cleanup; - } if (download_tags_value(remote, config) < 0) goto cleanup; @@ -300,36 +323,44 @@ cleanup: return error; } -static int update_config_refspec( - git_config *config, - const char *remote_name, - const git_refspec *refspec, - int git_direction) +static int update_config_refspec(const git_remote *remote, git_config *config, int direction) { - git_buf name = GIT_BUF_INIT, value = GIT_BUF_INIT; - int error = -1; + git_buf name = GIT_BUF_INIT; + int push; + const char *dir; + size_t i; + int error = 0; - if (refspec->src == NULL || refspec->dst == NULL) - return 0; + push = direction == GIT_DIRECTION_PUSH; + dir = push ? "push" : "fetch"; - if (git_buf_printf( - &name, - "remote.%s.%s", - remote_name, - git_direction == GIT_DIRECTION_FETCH ? "fetch" : "push") < 0) - goto cleanup; + if (git_buf_printf(&name, "remote.%s.%s", remote->name, dir) < 0) + return -1; - if (git_refspec__serialize(&value, refspec) < 0) - goto cleanup; + /* Clear out the existing config */ + while (!error) + error = git_config_delete_entry(config, git_buf_cstr(&name)); - error = git_config_set_string( - config, - git_buf_cstr(&name), - git_buf_cstr(&value)); + if (error != GIT_ENOTFOUND) + return error; + + for (i = 0; i < remote->refspecs.length; i++) { + git_refspec *spec = git_vector_get(&remote->refspecs, i); + + if (spec->push != push) + continue; + + if ((error = git_config_set_multivar( + config, git_buf_cstr(&name), "", spec->string)) < 0) { + goto cleanup; + } + } + + giterr_clear(); + error = 0; cleanup: git_buf_free(&name); - git_buf_free(&value); return error; } @@ -383,19 +414,11 @@ int git_remote_save(const git_remote *remote) } } - if (update_config_refspec( - config, - remote->name, - &remote->fetch, - GIT_DIRECTION_FETCH) < 0) - goto on_error; + if (update_config_refspec(remote, config, GIT_DIRECTION_FETCH) < 0) + goto on_error; - if (update_config_refspec( - config, - remote->name, - &remote->push, - GIT_DIRECTION_PUSH) < 0) - goto on_error; + if (update_config_refspec(remote, config, GIT_DIRECTION_PUSH) < 0) + goto on_error; /* * What action to take depends on the old and new values. This @@ -444,6 +467,12 @@ const char *git_remote_name(const git_remote *remote) return remote->name; } +git_repository *git_remote_owner(const git_remote *remote) +{ + assert(remote); + return remote->repo; +} + const char *git_remote_url(const git_remote *remote) { assert(remote); @@ -482,49 +511,6 @@ int git_remote_set_pushurl(git_remote *remote, const char* url) return 0; } -int git_remote_set_fetchspec(git_remote *remote, const char *spec) -{ - git_refspec refspec; - - assert(remote && spec); - - if (git_refspec__parse(&refspec, spec, true) < 0) - return -1; - - git_refspec__free(&remote->fetch); - memcpy(&remote->fetch, &refspec, sizeof(git_refspec)); - - return 0; -} - -const git_refspec *git_remote_fetchspec(const git_remote *remote) -{ - assert(remote); - return &remote->fetch; -} - -int git_remote_set_pushspec(git_remote *remote, const char *spec) -{ - git_refspec refspec; - - assert(remote && spec); - - if (git_refspec__parse(&refspec, spec, false) < 0) - return -1; - - git_refspec__free(&remote->push); - remote->push.src = refspec.src; - remote->push.dst = refspec.dst; - - return 0; -} - -const git_refspec *git_remote_pushspec(const git_remote *remote) -{ - assert(remote); - return &remote->push; -} - const char* git_remote__urlfordirection(git_remote *remote, int direction) { assert(remote); @@ -586,11 +572,6 @@ int git_remote_ls(git_remote *remote, git_headlist_cb list_cb, void *payload) { assert(remote); - if (!git_remote_connected(remote)) { - giterr_set(GITERR_NET, "The remote is not connected"); - return -1; - } - return remote->transport->ls(remote->transport, list_cb, payload); } @@ -651,28 +632,105 @@ int git_remote__get_http_proxy(git_remote *remote, bool use_ssl, char **proxy_ur return 0; } +static int store_refs(git_remote_head *head, void *payload) +{ + git_vector *refs = (git_vector *)payload; + + return git_vector_insert(refs, head); +} + +static int dwim_refspecs(git_vector *refspecs, git_vector *refs) +{ + git_buf buf = GIT_BUF_INIT; + git_refspec *spec; + size_t i, j, pos; + git_remote_head key; + + const char* formatters[] = { + GIT_REFS_DIR "%s", + GIT_REFS_TAGS_DIR "%s", + GIT_REFS_HEADS_DIR "%s", + NULL + }; + + git_vector_foreach(refspecs, i, spec) { + if (spec->dwim) + continue; + + /* shorthand on the lhs */ + if (git__prefixcmp(spec->src, GIT_REFS_DIR)) { + for (j = 0; formatters[j]; j++) { + git_buf_clear(&buf); + if (git_buf_printf(&buf, formatters[j], spec->src) < 0) + return -1; + + key.name = (char *) git_buf_cstr(&buf); + if (!git_vector_search(&pos, refs, &key)) { + /* we found something to match the shorthand, set src to that */ + git__free(spec->src); + spec->src = git_buf_detach(&buf); + } + } + } + + if (spec->dst && git__prefixcmp(spec->dst, GIT_REFS_DIR)) { + /* if it starts with "remotes" then we just prepend "refs/" */ + if (!git__prefixcmp(spec->dst, "remotes/")) { + git_buf_puts(&buf, GIT_REFS_DIR); + } else { + git_buf_puts(&buf, GIT_REFS_HEADS_DIR); + } + + if (git_buf_puts(&buf, spec->dst) < 0) + return -1; + + git__free(spec->dst); + spec->dst = git_buf_detach(&buf); + } + + spec->dwim = 1; + } + + git_buf_free(&buf); + return 0; +} + +static int remote_head_cmp(const void *_a, const void *_b) +{ + const git_remote_head *a = (git_remote_head *) _a; + const git_remote_head *b = (git_remote_head *) _b; + + return git__strcmp_cb(a->name, b->name); +} + int git_remote_download( git_remote *remote, git_transfer_progress_callback progress_cb, void *progress_payload) { int error; + git_vector refs; assert(remote); + if (git_vector_init(&refs, 16, remote_head_cmp) < 0) + return -1; + + if (git_remote_ls(remote, store_refs, &refs) < 0) { + return -1; + } + + error = dwim_refspecs(&remote->refspecs, &refs); + git_vector_free(&refs); + if (error < 0) + return -1; + if ((error = git_fetch_negotiate(remote)) < 0) return error; return git_fetch_download_pack(remote, progress_cb, progress_payload); } -static int update_tips_callback(git_remote_head *head, void *payload) -{ - git_vector *refs = (git_vector *)payload; - - return git_vector_insert(refs, head); -} - static int remote_head_for_fetchspec_src(git_remote_head **out, git_vector *update_heads, const char *fetchspec_src) { unsigned int i; @@ -692,21 +750,21 @@ static int remote_head_for_fetchspec_src(git_remote_head **out, git_vector *upda return 0; } -static int remote_head_for_ref(git_remote_head **out, git_remote *remote, git_vector *update_heads, git_reference *ref) +static int remote_head_for_ref(git_remote_head **out, git_refspec *spec, git_vector *update_heads, git_reference *ref) { git_reference *resolved_ref = NULL; git_reference *tracking_ref = NULL; git_buf remote_name = GIT_BUF_INIT; int error = 0; - assert(out && remote && ref); + assert(out && spec && ref); *out = NULL; if ((error = git_reference_resolve(&resolved_ref, ref)) < 0 || (!git_reference_is_branch(resolved_ref)) || (error = git_branch_upstream(&tracking_ref, resolved_ref)) < 0 || - (error = git_refspec_transform_l(&remote_name, &remote->fetch, git_reference_name(tracking_ref))) < 0) { + (error = git_refspec_transform_l(&remote_name, spec, git_reference_name(tracking_ref))) < 0) { /* Not an error if HEAD is orphaned or no tracking branch */ if (error == GIT_ENOTFOUND) error = 0; @@ -723,9 +781,8 @@ cleanup: return error; } -static int git_remote_write_fetchhead(git_remote *remote, git_vector *update_heads) +static int git_remote_write_fetchhead(git_remote *remote, git_refspec *spec, git_vector *update_heads) { - struct git_refspec *spec; git_reference *head_ref = NULL; git_fetchhead_ref *fetchhead_ref; git_remote_head *remote_ref, *merge_remote_ref; @@ -736,7 +793,9 @@ static int git_remote_write_fetchhead(git_remote *remote, git_vector *update_hea assert(remote); - spec = &remote->fetch; + /* no heads, nothing to do */ + if (update_heads->length == 0) + return 0; if (git_vector_init(&fetchhead_refs, update_heads->length, git_fetchhead_ref_cmp) < 0) return -1; @@ -747,7 +806,7 @@ static int git_remote_write_fetchhead(git_remote *remote, git_vector *update_hea /* Determine what to merge: if refspec was a wildcard, just use HEAD */ if (git_refspec_is_wildcard(spec)) { if ((error = git_reference_lookup(&head_ref, remote->repo, GIT_HEAD_FILE)) < 0 || - (error = remote_head_for_ref(&merge_remote_ref, remote, update_heads, head_ref)) < 0) + (error = remote_head_for_ref(&merge_remote_ref, spec, update_heads, head_ref)) < 0) goto cleanup; } else { /* If we're fetching a single refspec, that's the only thing that should be in FETCH_HEAD. */ @@ -787,7 +846,7 @@ cleanup: return error; } -int git_remote_update_tips(git_remote *remote) +static int update_tips_for_spec(git_remote *remote, git_refspec *spec, git_vector *refs) { int error = 0, autotag; unsigned int i = 0; @@ -796,14 +855,11 @@ int git_remote_update_tips(git_remote *remote) git_odb *odb; git_remote_head *head; git_reference *ref; - struct git_refspec *spec; git_refspec tagspec; - git_vector refs, update_heads; + git_vector update_heads; assert(remote); - spec = &remote->fetch; - if (git_repository_odb__weakptr(&odb, remote->repo) < 0) return -1; @@ -811,35 +867,18 @@ int git_remote_update_tips(git_remote *remote) return -1; /* Make a copy of the transport's refs */ - if (git_vector_init(&refs, 16, NULL) < 0 || - git_vector_init(&update_heads, 16, NULL) < 0) + if (git_vector_init(&update_heads, 16, NULL) < 0) return -1; - if (git_remote_ls(remote, update_tips_callback, &refs) < 0) - goto on_error; - - /* Let's go find HEAD, if it exists. Check only the first ref in the vector. */ - if (refs.length > 0) { - head = (git_remote_head *)refs.contents[0]; - - if (!strcmp(head->name, GIT_HEAD_FILE)) { - if (git_reference_create(&ref, remote->repo, GIT_FETCH_HEAD_FILE, &head->oid, 1) < 0) - goto on_error; - - i = 1; - git_reference_free(ref); - } - } - - for (; i < refs.length; ++i) { - head = (git_remote_head *)refs.contents[i]; + for (; i < refs->length; ++i) { + head = git_vector_get(refs, i); autotag = 0; /* Ignore malformed ref names (which also saves us from tag^{} */ if (!git_reference_is_valid_name(head->name)) continue; - if (git_refspec_src_matches(spec, head->name)) { + if (git_refspec_src_matches(spec, head->name) && spec->dst) { if (git_refspec_transform_r(&refname, spec, head->name) < 0) goto on_error; } else if (remote->download_tags != GIT_REMOTE_DOWNLOAD_TAGS_NONE) { @@ -870,7 +909,7 @@ int git_remote_update_tips(git_remote *remote) if (error == GIT_ENOTFOUND) memset(&old, 0, GIT_OID_RAWSZ); - if (!git_oid_cmp(&old, &head->oid)) + if (!git_oid__cmp(&old, &head->oid)) continue; /* In autotag mode, don't overwrite any locally-existing tags */ @@ -887,17 +926,15 @@ int git_remote_update_tips(git_remote *remote) } if (git_remote_update_fetchhead(remote) && - (error = git_remote_write_fetchhead(remote, &update_heads)) < 0) + (error = git_remote_write_fetchhead(remote, spec, &update_heads)) < 0) goto on_error; - git_vector_free(&refs); git_vector_free(&update_heads); git_refspec__free(&tagspec); git_buf_free(&refname); return 0; on_error: - git_vector_free(&refs); git_vector_free(&update_heads); git_refspec__free(&tagspec); git_buf_free(&refname); @@ -905,6 +942,42 @@ on_error: } +int git_remote_update_tips(git_remote *remote) +{ + git_refspec *spec, tagspec; + git_vector refs; + int error; + size_t i; + + + if (git_refspec__parse(&tagspec, GIT_REFSPEC_TAGS, true) < 0) + return -1; + + if (git_vector_init(&refs, 16, NULL) < 0) + return -1; + + if ((error = git_remote_ls(remote, store_refs, &refs)) < 0) + goto out; + + if (remote->download_tags == GIT_REMOTE_DOWNLOAD_TAGS_ALL) { + error = update_tips_for_spec(remote, &tagspec, &refs); + goto out; + } + + git_vector_foreach(&remote->refspecs, i, spec) { + if (spec->push) + continue; + + if ((error = update_tips_for_spec(remote, spec, &refs)) < 0) + goto out; + } + +out: + git_refspec__free(&tagspec); + git_vector_free(&refs); + return error; +} + int git_remote_connected(git_remote *remote) { assert(remote); @@ -934,6 +1007,9 @@ void git_remote_disconnect(git_remote *remote) void git_remote_free(git_remote *remote) { + git_refspec *spec; + size_t i; + if (remote == NULL) return; @@ -946,8 +1022,12 @@ void git_remote_free(git_remote *remote) git_vector_free(&remote->refs); - git_refspec__free(&remote->fetch); - git_refspec__free(&remote->push); + git_vector_foreach(&remote->refspecs, i, spec) { + git_refspec__free(spec); + git__free(spec); + } + git_vector_free(&remote->refspecs); + git__free(remote->url); git__free(remote->pushurl); git__free(remote->name); @@ -1158,40 +1238,24 @@ static int update_branch_remote_config_entry( update_config_entries_cb, &data); } -static int rename_cb(const char *ref, void *data) -{ - if (git__prefixcmp(ref, GIT_REFS_REMOTES_DIR)) - return 0; - - return git_vector_insert((git_vector *)data, git__strdup(ref)); -} - static int rename_one_remote_reference( - git_repository *repo, - const char *reference_name, + git_reference *reference, const char *old_remote_name, const char *new_remote_name) { int error = -1; git_buf new_name = GIT_BUF_INIT; - git_reference *reference = NULL; - git_reference *newref = NULL; if (git_buf_printf( &new_name, GIT_REFS_REMOTES_DIR "%s%s", new_remote_name, - reference_name + strlen(GIT_REFS_REMOTES_DIR) + strlen(old_remote_name)) < 0) + reference->name + strlen(GIT_REFS_REMOTES_DIR) + strlen(old_remote_name)) < 0) return -1; - if (git_reference_lookup(&reference, repo, reference_name) < 0) - goto cleanup; - - error = git_reference_rename(&newref, reference, git_buf_cstr(&new_name), 0); + error = git_reference_rename(NULL, reference, git_buf_cstr(&new_name), 0); git_reference_free(reference); -cleanup: - git_reference_free(newref); git_buf_free(&new_name); return error; } @@ -1201,33 +1265,30 @@ static int rename_remote_references( const char *old_name, const char *new_name) { - git_vector refnames; int error = -1; - unsigned int i; - char *name; + git_reference *ref; + git_reference_iterator *iter; - if (git_vector_init(&refnames, 8, NULL) < 0) - goto cleanup; + if (git_reference_iterator_new(&iter, repo) < 0) + return -1; - if (git_reference_foreach( - repo, - GIT_REF_LISTALL, - rename_cb, - &refnames) < 0) - goto cleanup; + while ((error = git_reference_next(&ref, iter)) == 0) { + if (git__prefixcmp(ref->name, GIT_REFS_REMOTES_DIR)) { + git_reference_free(ref); + continue; + } - git_vector_foreach(&refnames, i, name) { - if ((error = rename_one_remote_reference(repo, name, old_name, new_name)) < 0) - goto cleanup; + if ((error = rename_one_remote_reference(ref, old_name, new_name)) < 0) { + git_reference_iterator_free(iter); + return error; + } } - error = 0; -cleanup: - git_vector_foreach(&refnames, i, name) { - git__free(name); - } + git_reference_iterator_free(iter); + + if (error == GIT_ITEROVER) + return 0; - git_vector_free(&refnames); return error; } @@ -1238,58 +1299,58 @@ static int rename_fetch_refspecs( void *payload) { git_config *config; - const git_refspec *fetch_refspec; - git_buf dst_prefix = GIT_BUF_INIT, serialized = GIT_BUF_INIT; - const char* pos; + git_buf base = GIT_BUF_INIT, var = GIT_BUF_INIT, val = GIT_BUF_INIT; + const git_refspec *spec; + size_t i; int error = -1; - fetch_refspec = git_remote_fetchspec(remote); - - /* Is there a refspec to deal with? */ - if (fetch_refspec->src == NULL && - fetch_refspec->dst == NULL) - return 0; - - if (git_refspec__serialize(&serialized, fetch_refspec) < 0) + if (git_buf_printf(&base, "+refs/heads/*:refs/remotes/%s/*", remote->name) < 0) goto cleanup; - /* Is it an in-memory remote? */ - if (!remote->name) { - error = (callback(git_buf_cstr(&serialized), payload) < 0) ? GIT_EUSER : 0; - goto cleanup; - } + git_vector_foreach(&remote->refspecs, i, spec) { + if (spec->push) + continue; - if (git_buf_printf(&dst_prefix, ":refs/remotes/%s/", remote->name) < 0) - goto cleanup; + /* Every refspec is a problem refspec for an in-memory remote */ + if (!remote->name) { + if (callback(spec->string, payload) < 0) { + error = GIT_EUSER; + goto cleanup; + } - pos = strstr(git_buf_cstr(&serialized), git_buf_cstr(&dst_prefix)); + continue; + } - /* Does the dst part of the refspec follow the extected standard format? */ - if (!pos) { - error = (callback(git_buf_cstr(&serialized), payload) < 0) ? GIT_EUSER : 0; - goto cleanup; - } + /* Does the dst part of the refspec follow the extected standard format? */ + if (strcmp(git_buf_cstr(&base), spec->string)) { + if (callback(spec->string, payload) < 0) { + error = GIT_EUSER; + goto cleanup; + } + + continue; + } - if (git_buf_splice( - &serialized, - pos - git_buf_cstr(&serialized) + strlen(":refs/remotes/"), - strlen(remote->name), new_name, - strlen(new_name)) < 0) + /* If we do want to move it to the new section */ + if (git_buf_printf(&val, "+refs/heads/*:refs/remotes/%s/*", new_name) < 0) goto cleanup; - git_refspec__free(&remote->fetch); + if (git_buf_printf(&var, "remote.%s.fetch", new_name) < 0) + goto cleanup; - if (git_refspec__parse(&remote->fetch, git_buf_cstr(&serialized), true) < 0) - goto cleanup; + if (git_repository_config__weakptr(&config, remote->repo) < 0) + goto cleanup; - if (git_repository_config__weakptr(&config, remote->repo) < 0) - goto cleanup; + if (git_config_set_string(config, git_buf_cstr(&var), git_buf_cstr(&val)) < 0) + goto cleanup; + } - error = update_config_refspec(config, new_name, &remote->fetch, GIT_DIRECTION_FETCH); + error = 0; cleanup: - git_buf_free(&serialized); - git_buf_free(&dst_prefix); + git_buf_free(&base); + git_buf_free(&var); + git_buf_free(&val); return error; } @@ -1390,3 +1451,128 @@ int git_remote_is_valid_name( giterr_clear(); return error == 0; } + +git_refspec *git_remote__matching_refspec(git_remote *remote, const char *refname) +{ + git_refspec *spec; + size_t i; + + git_vector_foreach(&remote->refspecs, i, spec) { + if (spec->push) + continue; + + if (git_refspec_src_matches(spec, refname)) + return spec; + } + + return NULL; +} + +git_refspec *git_remote__matching_dst_refspec(git_remote *remote, const char *refname) +{ + git_refspec *spec; + size_t i; + + git_vector_foreach(&remote->refspecs, i, spec) { + if (spec->push) + continue; + + if (git_refspec_dst_matches(spec, refname)) + return spec; + } + + return NULL; +} + +void git_remote_clear_refspecs(git_remote *remote) +{ + git_refspec *spec; + size_t i; + + git_vector_foreach(&remote->refspecs, i, spec) { + git_refspec__free(spec); + git__free(spec); + } + git_vector_clear(&remote->refspecs); +} + +int git_remote_add_fetch(git_remote *remote, const char *refspec) +{ + return add_refspec(remote, refspec, true); +} + +int git_remote_add_push(git_remote *remote, const char *refspec) +{ + return add_refspec(remote, refspec, false); +} + +static int copy_refspecs(git_strarray *array, git_remote *remote, int push) +{ + size_t i; + git_vector refspecs; + git_refspec *spec; + char *dup; + + if (git_vector_init(&refspecs, remote->refspecs.length, NULL) < 0) + return -1; + + git_vector_foreach(&remote->refspecs, i, spec) { + if (spec->push != push) + continue; + + if ((dup = git__strdup(spec->string)) == NULL) + goto on_error; + + if (git_vector_insert(&refspecs, dup) < 0) { + git__free(dup); + goto on_error; + } + } + + array->strings = (char **)refspecs.contents; + array->count = refspecs.length; + + return 0; + +on_error: + git_vector_foreach(&refspecs, i, dup) + git__free(dup); + git_vector_free(&refspecs); + + return -1; +} + +int git_remote_get_fetch_refspecs(git_strarray *array, git_remote *remote) +{ + return copy_refspecs(array, remote, false); +} + +int git_remote_get_push_refspecs(git_strarray *array, git_remote *remote) +{ + return copy_refspecs(array, remote, true); +} + +size_t git_remote_refspec_count(git_remote *remote) +{ + return remote->refspecs.length; +} + +const git_refspec *git_remote_get_refspec(git_remote *remote, size_t n) +{ + return git_vector_get(&remote->refspecs, n); +} + +int git_remote_remove_refspec(git_remote *remote, size_t n) +{ + git_refspec *spec; + + assert(remote); + + spec = git_vector_get(&remote->refspecs, n); + if (spec) { + git_refspec__free(spec); + git__free(spec); + } + + return git_vector_remove(&remote->refspecs, n); +} diff --git a/src/remote.h b/src/remote.h index 4c1a18aa7..dce4803ed 100644 --- a/src/remote.h +++ b/src/remote.h @@ -11,7 +11,7 @@ #include "git2/transport.h" #include "refspec.h" -#include "repository.h" +#include "vector.h" #define GIT_REMOTE_ORIGIN "origin" @@ -20,8 +20,7 @@ struct git_remote { char *url; char *pushurl; git_vector refs; - struct git_refspec fetch; - struct git_refspec push; + git_vector refspecs; git_cred_acquire_cb cred_acquire_cb; void *cred_acquire_payload; git_transport *transport; @@ -37,4 +36,7 @@ struct git_remote { const char* git_remote__urlfordirection(struct git_remote *remote, int direction); int git_remote__get_http_proxy(git_remote *remote, bool use_ssl, char **proxy_url); +git_refspec *git_remote__matching_refspec(git_remote *remote, const char *refname); +git_refspec *git_remote__matching_dst_refspec(git_remote *remote, const char *refname); + #endif diff --git a/src/repository.c b/src/repository.c index 0ad7449ba..99ac56ef9 100644 --- a/src/repository.c +++ b/src/repository.c @@ -9,6 +9,7 @@ #include "git2/object.h" #include "git2/refdb.h" +#include "git2/sys/repository.h" #include "common.h" #include "repository.h" @@ -16,12 +17,15 @@ #include "tag.h" #include "blob.h" #include "fileops.h" +#include "filebuf.h" +#include "index.h" #include "config.h" #include "refs.h" #include "filter.h" #include "odb.h" #include "remote.h" #include "merge.h" +#include "diff_driver.h" #define GIT_FILE_CONTENT_PREFIX "gitdir:" @@ -31,61 +35,91 @@ #define GIT_TEMPLATE_DIR "/usr/share/git-core/templates" -static void drop_odb(git_repository *repo) +static void set_odb(git_repository *repo, git_odb *odb) { - if (repo->_odb != NULL) { - GIT_REFCOUNT_OWN(repo->_odb, NULL); - git_odb_free(repo->_odb); - repo->_odb = NULL; + if (odb) { + GIT_REFCOUNT_OWN(odb, repo); + GIT_REFCOUNT_INC(odb); + } + + if ((odb = git__swap(repo->_odb, odb)) != NULL) { + GIT_REFCOUNT_OWN(odb, NULL); + git_odb_free(odb); } } -static void drop_refdb(git_repository *repo) +static void set_refdb(git_repository *repo, git_refdb *refdb) { - if (repo->_refdb != NULL) { - GIT_REFCOUNT_OWN(repo->_refdb, NULL); - git_refdb_free(repo->_refdb); - repo->_refdb = NULL; + if (refdb) { + GIT_REFCOUNT_OWN(refdb, repo); + GIT_REFCOUNT_INC(refdb); + } + + if ((refdb = git__swap(repo->_refdb, refdb)) != NULL) { + GIT_REFCOUNT_OWN(refdb, NULL); + git_refdb_free(refdb); } } -static void drop_config(git_repository *repo) +static void set_config(git_repository *repo, git_config *config) { - if (repo->_config != NULL) { - GIT_REFCOUNT_OWN(repo->_config, NULL); - git_config_free(repo->_config); - repo->_config = NULL; + if (config) { + GIT_REFCOUNT_OWN(config, repo); + GIT_REFCOUNT_INC(config); + } + + if ((config = git__swap(repo->_config, config)) != NULL) { + GIT_REFCOUNT_OWN(config, NULL); + git_config_free(config); } git_repository__cvar_cache_clear(repo); } -static void drop_index(git_repository *repo) +static void set_index(git_repository *repo, git_index *index) { - if (repo->_index != NULL) { - GIT_REFCOUNT_OWN(repo->_index, NULL); - git_index_free(repo->_index); - repo->_index = NULL; + if (index) { + GIT_REFCOUNT_OWN(index, repo); + GIT_REFCOUNT_INC(index); + } + + if ((index = git__swap(repo->_index, index)) != NULL) { + GIT_REFCOUNT_OWN(index, NULL); + git_index_free(index); } } +void git_repository__cleanup(git_repository *repo) +{ + assert(repo); + + git_cache_clear(&repo->objects); + git_attr_cache_flush(repo); + + set_config(repo, NULL); + set_index(repo, NULL); + set_odb(repo, NULL); + set_refdb(repo, NULL); +} + void git_repository_free(git_repository *repo) { if (repo == NULL) return; + git_repository__cleanup(repo); + git_cache_free(&repo->objects); - git_attr_cache_flush(repo); git_submodule_config_free(repo); + git_diff_driver_registry_free(repo->diff_drivers); + repo->diff_drivers = NULL; + git__free(repo->path_repository); git__free(repo->workdir); + git__free(repo->namespace); - drop_config(repo); - drop_index(repo); - drop_odb(repo); - drop_refdb(repo); - + git__memzero(repo, sizeof(*repo)); git__free(repo); } @@ -112,13 +146,11 @@ static bool valid_repository_path(git_buf *repository_path) static git_repository *repository_alloc(void) { - git_repository *repo = git__malloc(sizeof(git_repository)); + git_repository *repo = git__calloc(1, sizeof(git_repository)); if (!repo) return NULL; - memset(repo, 0x0, sizeof(git_repository)); - - if (git_cache_init(&repo->objects, GIT_DEFAULT_CACHE_SIZE, &git_object__free) < 0) { + if (git_cache_init(&repo->objects) < 0) { git__free(repo); return NULL; } @@ -129,6 +161,12 @@ static git_repository *repository_alloc(void) return repo; } +int git_repository_new(git_repository **out) +{ + *out = repository_alloc(); + return 0; +} + static int load_config_data(git_repository *repo) { int is_bare; @@ -228,7 +266,7 @@ static int find_ceiling_dir_offset( buf[--len] = '\0'; if (!strncmp(path, buf2, len) && - path[len] == '/' && + (path[len] == '/' || !path[len]) && len > max_len) { max_len = len; @@ -284,17 +322,18 @@ static int find_repo( git_buf path = GIT_BUF_INIT; struct stat st; dev_t initial_device = 0; - bool try_with_dot_git = false; + bool try_with_dot_git = ((flags & GIT_REPOSITORY_OPEN_BARE) != 0); int ceiling_offset; git_buf_free(repo_path); - if ((error = git_path_prettify_dir(&path, start_path, NULL)) < 0) + if ((error = git_path_prettify(&path, start_path, NULL)) < 0) return error; ceiling_offset = find_ceiling_dir_offset(path.ptr, ceiling_dirs); - if ((error = git_buf_joinpath(&path, path.ptr, DOT_GIT)) < 0) + if (!try_with_dot_git && + (error = git_buf_joinpath(&path, path.ptr, DOT_GIT)) < 0) return error; while (!error && !git_buf_len(repo_path)) { @@ -346,7 +385,7 @@ static int find_repo( try_with_dot_git = !try_with_dot_git; } - if (!error && parent_path != NULL) { + if (!error && parent_path && !(flags & GIT_REPOSITORY_OPEN_BARE)) { if (!git_buf_len(repo_path)) git_buf_clear(parent_path); else { @@ -368,6 +407,37 @@ static int find_repo( return error; } +int git_repository_open_bare( + git_repository **repo_ptr, + const char *bare_path) +{ + int error; + git_buf path = GIT_BUF_INIT; + git_repository *repo = NULL; + + if ((error = git_path_prettify_dir(&path, bare_path, NULL)) < 0) + return error; + + if (!valid_repository_path(&path)) { + git_buf_free(&path); + giterr_set(GITERR_REPOSITORY, "Path is not a repository: %s", bare_path); + return GIT_ENOTFOUND; + } + + repo = repository_alloc(); + GITERR_CHECK_ALLOC(repo); + + repo->path_repository = git_buf_detach(&path); + GITERR_CHECK_ALLOC(repo->path_repository); + + /* of course we're bare! */ + repo->is_bare = 1; + repo->workdir = NULL; + + *repo_ptr = repo; + return 0; +} + int git_repository_open_ext( git_repository **repo_ptr, const char *start_path, @@ -391,7 +461,9 @@ int git_repository_open_ext( repo->path_repository = git_buf_detach(&path); GITERR_CHECK_ALLOC(repo->path_repository); - if ((error = load_config_data(repo)) < 0 || + if ((flags & GIT_REPOSITORY_OPEN_BARE) != 0) + repo->is_bare = 1; + else if ((error = load_config_data(repo)) < 0 || (error = load_workdir(repo, &parent)) < 0) { git_repository_free(repo); @@ -511,39 +583,51 @@ on_error: return error; } -int git_repository_config__weakptr(git_config **out, git_repository *repo) +static const char *path_unless_empty(git_buf *buf) { - if (repo->_config == NULL) { - git_buf global_buf = GIT_BUF_INIT, xdg_buf = GIT_BUF_INIT, system_buf = GIT_BUF_INIT; - int res; - - const char *global_config_path = NULL; - const char *xdg_config_path = NULL; - const char *system_config_path = NULL; - - if (git_config_find_global_r(&global_buf) == 0) - global_config_path = global_buf.ptr; + return git_buf_len(buf) > 0 ? git_buf_cstr(buf) : NULL; +} - if (git_config_find_xdg_r(&xdg_buf) == 0) - xdg_config_path = xdg_buf.ptr; +int git_repository_config__weakptr(git_config **out, git_repository *repo) +{ + int error = 0; - if (git_config_find_system_r(&system_buf) == 0) - system_config_path = system_buf.ptr; + if (repo->_config == NULL) { + git_buf global_buf = GIT_BUF_INIT; + git_buf xdg_buf = GIT_BUF_INIT; + git_buf system_buf = GIT_BUF_INIT; + git_config *config; - res = load_config(&repo->_config, repo, global_config_path, xdg_config_path, system_config_path); + git_config_find_global_r(&global_buf); + git_config_find_xdg_r(&xdg_buf); + git_config_find_system_r(&system_buf); + + /* If there is no global file, open a backend for it anyway */ + if (git_buf_len(&global_buf) == 0) + git_config__global_location(&global_buf); + + error = load_config( + &config, repo, + path_unless_empty(&global_buf), + path_unless_empty(&xdg_buf), + path_unless_empty(&system_buf)); + if (!error) { + GIT_REFCOUNT_OWN(config, repo); + + config = git__compare_and_swap(&repo->_config, NULL, config); + if (config != NULL) { + GIT_REFCOUNT_OWN(config, NULL); + git_config_free(config); + } + } git_buf_free(&global_buf); git_buf_free(&xdg_buf); git_buf_free(&system_buf); - - if (res < 0) - return -1; - - GIT_REFCOUNT_OWN(repo->_config, repo); } *out = repo->_config; - return 0; + return error; } int git_repository_config(git_config **out, git_repository *repo) @@ -558,36 +642,37 @@ int git_repository_config(git_config **out, git_repository *repo) void git_repository_set_config(git_repository *repo, git_config *config) { assert(repo && config); - - drop_config(repo); - - repo->_config = config; - GIT_REFCOUNT_OWN(repo->_config, repo); - GIT_REFCOUNT_INC(repo->_config); + set_config(repo, config); } int git_repository_odb__weakptr(git_odb **out, git_repository *repo) { + int error = 0; + assert(repo && out); if (repo->_odb == NULL) { git_buf odb_path = GIT_BUF_INIT; - int res; + git_odb *odb; - if (git_buf_joinpath(&odb_path, repo->path_repository, GIT_OBJECTS_DIR) < 0) - return -1; + git_buf_joinpath(&odb_path, repo->path_repository, GIT_OBJECTS_DIR); - res = git_odb_open(&repo->_odb, odb_path.ptr); - git_buf_free(&odb_path); /* done with path */ + error = git_odb_open(&odb, odb_path.ptr); + if (!error) { + GIT_REFCOUNT_OWN(odb, repo); - if (res < 0) - return -1; + odb = git__compare_and_swap(&repo->_odb, NULL, odb); + if (odb != NULL) { + GIT_REFCOUNT_OWN(odb, NULL); + git_odb_free(odb); + } + } - GIT_REFCOUNT_OWN(repo->_odb, repo); + git_buf_free(&odb_path); } *out = repo->_odb; - return 0; + return error; } int git_repository_odb(git_odb **out, git_repository *repo) @@ -602,31 +687,32 @@ int git_repository_odb(git_odb **out, git_repository *repo) void git_repository_set_odb(git_repository *repo, git_odb *odb) { assert(repo && odb); - - drop_odb(repo); - - repo->_odb = odb; - GIT_REFCOUNT_OWN(repo->_odb, repo); - GIT_REFCOUNT_INC(odb); + set_odb(repo, odb); } int git_repository_refdb__weakptr(git_refdb **out, git_repository *repo) { + int error = 0; + assert(out && repo); if (repo->_refdb == NULL) { - int res; + git_refdb *refdb; - res = git_refdb_open(&repo->_refdb, repo); + error = git_refdb_open(&refdb, repo); + if (!error) { + GIT_REFCOUNT_OWN(refdb, repo); - if (res < 0) - return -1; - - GIT_REFCOUNT_OWN(repo->_refdb, repo); + refdb = git__compare_and_swap(&repo->_refdb, NULL, refdb); + if (refdb != NULL) { + GIT_REFCOUNT_OWN(refdb, NULL); + git_refdb_free(refdb); + } + } } *out = repo->_refdb; - return 0; + return error; } int git_repository_refdb(git_refdb **out, git_repository *repo) @@ -640,40 +726,40 @@ int git_repository_refdb(git_refdb **out, git_repository *repo) void git_repository_set_refdb(git_repository *repo, git_refdb *refdb) { - assert (repo && refdb); - - drop_refdb(repo); - - repo->_refdb = refdb; - GIT_REFCOUNT_OWN(repo->_refdb, repo); - GIT_REFCOUNT_INC(refdb); + assert(repo && refdb); + set_refdb(repo, refdb); } int git_repository_index__weakptr(git_index **out, git_repository *repo) { + int error = 0; + assert(out && repo); if (repo->_index == NULL) { - int res; git_buf index_path = GIT_BUF_INIT; + git_index *index; - if (git_buf_joinpath(&index_path, repo->path_repository, GIT_INDEX_FILE) < 0) - return -1; + git_buf_joinpath(&index_path, repo->path_repository, GIT_INDEX_FILE); - res = git_index_open(&repo->_index, index_path.ptr); - git_buf_free(&index_path); /* done with path */ + error = git_index_open(&index, index_path.ptr); + if (!error) { + GIT_REFCOUNT_OWN(index, repo); - if (res < 0) - return -1; + index = git__compare_and_swap(&repo->_index, NULL, index); + if (index != NULL) { + GIT_REFCOUNT_OWN(index, NULL); + git_index_free(index); + } - GIT_REFCOUNT_OWN(repo->_index, repo); + error = git_index_set_caps(repo->_index, GIT_INDEXCAP_FROM_OWNER); + } - if (git_index_set_caps(repo->_index, GIT_INDEXCAP_FROM_OWNER) < 0) - return -1; + git_buf_free(&index_path); } *out = repo->_index; - return 0; + return error; } int git_repository_index(git_index **out, git_repository *repo) @@ -688,12 +774,24 @@ int git_repository_index(git_index **out, git_repository *repo) void git_repository_set_index(git_repository *repo, git_index *index) { assert(repo && index); + set_index(repo, index); +} + +int git_repository_set_namespace(git_repository *repo, const char *namespace) +{ + git__free(repo->namespace); - drop_index(repo); + if (namespace == NULL) { + repo->namespace = NULL; + return 0; + } - repo->_index = index; - GIT_REFCOUNT_OWN(repo->_index, repo); - GIT_REFCOUNT_INC(index); + return (repo->namespace = git__strdup(namespace)) ? 0 : -1; +} + +const char *git_repository_get_namespace(git_repository *repo) +{ + return repo->namespace; } static int check_repositoryformatversion(git_config *config) @@ -1383,14 +1481,15 @@ static int at_least_one_cb(const char *refname, void *payload) static int repo_contains_no_reference(git_repository *repo) { - int error; - - error = git_reference_foreach(repo, GIT_REF_LISTALL, at_least_one_cb, NULL); + int error = git_reference_foreach_name(repo, &at_least_one_cb, NULL); if (error == GIT_EUSER) return 0; - return error == 0 ? 1 : error; + if (!error) + return 1; + + return error; } int git_repository_is_empty(git_repository *repo) @@ -1401,12 +1500,12 @@ int git_repository_is_empty(git_repository *repo) if (git_reference_lookup(&head, repo, GIT_HEAD_FILE) < 0) return -1; - if (!(error = git_reference_type(head) == GIT_REF_SYMBOLIC)) + if (!((error = git_reference_type(head)) == GIT_REF_SYMBOLIC)) goto cleanup; - if (!(error = strcmp( + if (!(error = (strcmp( git_reference_symbolic_target(head), - GIT_REFS_HEADS_DIR "master") == 0)) + GIT_REFS_HEADS_DIR "master") == 0))) goto cleanup; error = repo_contains_no_reference(repo); @@ -1507,12 +1606,16 @@ int git_repository_message(char *buffer, size_t len, git_repository *repo) struct stat st; int error; + if (buffer != NULL) + *buffer = '\0'; + if (git_buf_joinpath(&path, repo->path_repository, GIT_MERGE_MSG_FILE) < 0) return -1; if ((error = p_stat(git_buf_cstr(&path), &st)) < 0) { if (errno == ENOENT) error = GIT_ENOTFOUND; + giterr_set(GITERR_OS, "Could not access message file"); } else if (buffer != NULL) { error = git_futils_readbuffer(&buf, git_buf_cstr(&path)); @@ -1543,11 +1646,11 @@ int git_repository_message_remove(git_repository *repo) } int git_repository_hashfile( - git_oid *out, - git_repository *repo, - const char *path, - git_otype type, - const char *as_path) + git_oid *out, + git_repository *repo, + const char *path, + git_otype type, + const char *as_path) { int error; git_vector filters = GIT_VECTOR_INIT; @@ -1729,3 +1832,20 @@ int git_repository_state(git_repository *repo) git_buf_free(&repo_path); return state; } + +int git_repository_is_shallow(git_repository *repo) +{ + git_buf path = GIT_BUF_INIT; + struct stat st; + int error; + + git_buf_joinpath(&path, repo->path_repository, "shallow"); + error = git_path_lstat(path.ptr, &st); + git_buf_free(&path); + + if (error == GIT_ENOTFOUND) + return 0; + if (error < 0) + return -1; + return st.st_size == 0 ? 0 : 1; +} diff --git a/src/repository.h b/src/repository.h index cc2f8c2b8..12dc50d51 100644 --- a/src/repository.h +++ b/src/repository.h @@ -12,16 +12,15 @@ #include "git2/odb.h" #include "git2/repository.h" #include "git2/object.h" +#include "git2/config.h" -#include "index.h" #include "cache.h" #include "refs.h" #include "buffer.h" -#include "odb.h" #include "object.h" #include "attrcache.h" #include "strmap.h" -#include "refdb.h" +#include "diff_driver.h" #define DOT_GIT ".git" #define GIT_DIR DOT_GIT "/" @@ -31,7 +30,13 @@ /** Cvar cache identifiers */ typedef enum { GIT_CVAR_AUTO_CRLF = 0, /* core.autocrlf */ - GIT_CVAR_EOL, /* core.eol */ + GIT_CVAR_EOL, /* core.eol */ + GIT_CVAR_SYMLINKS, /* core.symlinks */ + GIT_CVAR_IGNORECASE, /* core.ignorecase */ + GIT_CVAR_FILEMODE, /* core.filemode */ + GIT_CVAR_IGNORESTAT, /* core.ignorestat */ + GIT_CVAR_TRUSTCTIME, /* core.trustctime */ + GIT_CVAR_ABBREV, /* core.abbrev */ GIT_CVAR_CACHE_MAX } git_cvar_cached; @@ -67,7 +72,21 @@ typedef enum { #else GIT_EOL_NATIVE = GIT_EOL_LF, #endif - GIT_EOL_DEFAULT = GIT_EOL_NATIVE + GIT_EOL_DEFAULT = GIT_EOL_NATIVE, + + /* core.symlinks: bool */ + GIT_SYMLINKS_DEFAULT = GIT_CVAR_TRUE, + /* core.ignorecase */ + GIT_IGNORECASE_DEFAULT = GIT_CVAR_FALSE, + /* core.filemode */ + GIT_FILEMODE_DEFAULT = GIT_CVAR_TRUE, + /* core.ignorestat */ + GIT_IGNORESTAT_DEFAULT = GIT_CVAR_FALSE, + /* core.trustctime */ + GIT_TRUSTCTIME_DEFAULT = GIT_CVAR_TRUE, + /* core.abbrev */ + GIT_ABBREV_DEFAULT = 7, + } git_cvar_value; /* internal repository init flags */ @@ -87,9 +106,11 @@ struct git_repository { git_cache objects; git_attr_cache attrcache; git_strmap *submodules; + git_diff_driver_registry *diff_drivers; char *path_repository; char *workdir; + char *namespace; unsigned is_bare:1; unsigned int lru_counter; diff --git a/src/reset.c b/src/reset.c index c1e1f865e..cea212a93 100644 --- a/src/reset.c +++ b/src/reset.c @@ -32,7 +32,7 @@ int git_reset_default( int error; git_index *index = NULL; - assert(pathspecs != NULL && pathspecs->count > 0); + assert(pathspecs != NULL && pathspecs->count > 0); memset(&entry, 0, sizeof(git_index_entry)); diff --git a/src/revparse.c b/src/revparse.c index fd62a2fd2..d21f08b53 100644 --- a/src/revparse.c +++ b/src/revparse.c @@ -14,60 +14,6 @@ #include "git2.h" -static int disambiguate_refname(git_reference **out, git_repository *repo, const char *refname) -{ - int error, i; - bool fallbackmode = true; - git_reference *ref; - git_buf refnamebuf = GIT_BUF_INIT, name = GIT_BUF_INIT; - - static const char* formatters[] = { - "%s", - GIT_REFS_DIR "%s", - GIT_REFS_TAGS_DIR "%s", - GIT_REFS_HEADS_DIR "%s", - GIT_REFS_REMOTES_DIR "%s", - GIT_REFS_REMOTES_DIR "%s/" GIT_HEAD_FILE, - NULL - }; - - if (*refname) - git_buf_puts(&name, refname); - else { - git_buf_puts(&name, GIT_HEAD_FILE); - fallbackmode = false; - } - - for (i = 0; formatters[i] && (fallbackmode || i == 0); i++) { - - git_buf_clear(&refnamebuf); - - if ((error = git_buf_printf(&refnamebuf, formatters[i], git_buf_cstr(&name))) < 0) - goto cleanup; - - if (!git_reference_is_valid_name(git_buf_cstr(&refnamebuf))) { - error = GIT_EINVALIDSPEC; - continue; - } - - error = git_reference_lookup_resolved(&ref, repo, git_buf_cstr(&refnamebuf), -1); - - if (!error) { - *out = ref; - error = 0; - goto cleanup; - } - - if (error != GIT_ENOTFOUND) - goto cleanup; - } - -cleanup: - git_buf_free(&name); - git_buf_free(&refnamebuf); - return error; -} - static int maybe_sha_or_abbrev(git_object** out, git_repository *repo, const char *spec, size_t speclen) { git_oid oid; @@ -107,7 +53,7 @@ static int build_regex(regex_t *regex, const char *pattern) error = regcomp(regex, pattern, REG_EXTENDED); if (!error) return 0; - + error = giterr_set_regex(regex, error); regfree(regex); @@ -125,7 +71,7 @@ static int maybe_describe(git_object**out, git_repository *repo, const char *spe if (substr == NULL) return GIT_ENOTFOUND; - + if (build_regex(®ex, ".+-[0-9]+-g[0-9a-fA-F]+") < 0) return -1; @@ -138,36 +84,45 @@ static int maybe_describe(git_object**out, git_repository *repo, const char *spe return maybe_abbrev(out, repo, substr+2); } -static int revparse_lookup_object(git_object **out, git_repository *repo, const char *spec) +static int revparse_lookup_object( + git_object **object_out, + git_reference **reference_out, + git_repository *repo, + const char *spec) { int error; git_reference *ref; - error = maybe_sha(out, repo, spec); + error = maybe_sha(object_out, repo, spec); if (!error) return 0; if (error < 0 && error != GIT_ENOTFOUND) return error; - error = disambiguate_refname(&ref, repo, spec); + error = git_reference_dwim(&ref, repo, spec); if (!error) { - error = git_object_lookup(out, repo, git_reference_target(ref), GIT_OBJ_ANY); - git_reference_free(ref); + + error = git_object_lookup( + object_out, repo, git_reference_target(ref), GIT_OBJ_ANY); + + if (!error) + *reference_out = ref; + return error; } if (error < 0 && error != GIT_ENOTFOUND) return error; - error = maybe_abbrev(out, repo, spec); + error = maybe_abbrev(object_out, repo, spec); if (!error) return 0; if (error < 0 && error != GIT_ENOTFOUND) return error; - error = maybe_describe(out, repo, spec); + error = maybe_describe(object_out, repo, spec); if (!error) return 0; @@ -235,7 +190,7 @@ static int retrieve_previously_checked_out_branch_or_revision(git_object **out, git_buf_put(&buf, msg+regexmatches[1].rm_so, regexmatches[1].rm_eo - regexmatches[1].rm_so); - if ((error = disambiguate_refname(base_ref, repo, git_buf_cstr(&buf))) == 0) + if ((error = git_reference_dwim(base_ref, repo, git_buf_cstr(&buf))) == 0) goto cleanup; if (error < 0 && error != GIT_ENOTFOUND) @@ -316,7 +271,7 @@ static int retrieve_revobject_from_reflog(git_object **out, git_reference **base int error = -1; if (*base_ref == NULL) { - if ((error = disambiguate_refname(&ref, repo, identifier)) < 0) + if ((error = git_reference_dwim(&ref, repo, identifier)) < 0) return error; } else { ref = *base_ref; @@ -344,7 +299,7 @@ static int retrieve_remote_tracking_reference(git_reference **base_ref, const ch int error = -1; if (*base_ref == NULL) { - if ((error = disambiguate_refname(&ref, repo, identifier)) < 0) + if ((error = git_reference_dwim(&ref, repo, identifier)) < 0) return error; } else { ref = *base_ref; @@ -358,7 +313,7 @@ static int retrieve_remote_tracking_reference(git_reference **base_ref, const ch if ((error = git_branch_upstream(&tracking, ref)) < 0) goto cleanup; - + *base_ref = tracking; cleanup: @@ -508,7 +463,7 @@ static int walk_and_search(git_object **out, git_revwalk *walk, regex_t *regex) int error; git_oid oid; git_object *obj; - + while (!(error = git_revwalk_next(&oid, walk))) { error = git_object_lookup(&obj, git_revwalk_repository(walk), &oid, GIT_OBJ_COMMIT); @@ -537,7 +492,7 @@ static int handle_grep_syntax(git_object **out, git_repository *repo, const git_ if ((error = build_regex(&preg, pattern)) < 0) return error; - + if ((error = git_revwalk_new(&walk, repo)) < 0) goto cleanup; @@ -551,7 +506,7 @@ static int handle_grep_syntax(git_object **out, git_repository *repo, const git_ goto cleanup; error = walk_and_search(out, walk, &preg); - + cleanup: regfree(&preg); git_revwalk_free(walk); @@ -671,14 +626,8 @@ static int ensure_base_rev_loaded(git_object **object, git_reference **reference if (*object != NULL) return 0; - if (*reference != NULL) { - if ((error = object_from_reference(object, *reference)) < 0) - return error; - - git_reference_free(*reference); - *reference = NULL; - return 0; - } + if (*reference != NULL) + return object_from_reference(object, *reference); if (!allow_empty_identifier && identifier_len == 0) return GIT_EINVALIDSPEC; @@ -686,7 +635,7 @@ static int ensure_base_rev_loaded(git_object **object, git_reference **reference if (git_buf_put(&identifier, spec, identifier_len) < 0) return -1; - error = revparse_lookup_object(object, repo, git_buf_cstr(&identifier)); + error = revparse_lookup_object(object, reference, repo, git_buf_cstr(&identifier)); git_buf_free(&identifier); return error; @@ -722,7 +671,12 @@ static int ensure_left_hand_identifier_is_not_known_yet(git_object *object, git_ return GIT_EINVALIDSPEC; } -int git_revparse_single(git_object **out, git_repository *repo, const char *spec) +int revparse__ext( + git_object **object_out, + git_reference **reference_out, + size_t *identifier_len_out, + git_repository *repo, + const char *spec) { size_t pos = 0, identifier_len = 0; int error = -1, n; @@ -731,13 +685,18 @@ int git_revparse_single(git_object **out, git_repository *repo, const char *spec git_reference *reference = NULL; git_object *base_rev = NULL; - assert(out && repo && spec); + bool should_return_reference = true; - *out = NULL; + assert(object_out && reference_out && repo && spec); + + *object_out = NULL; + *reference_out = NULL; while (spec[pos]) { switch (spec[pos]) { case '^': + should_return_reference = false; + if ((error = ensure_base_rev_loaded(&base_rev, &reference, spec, identifier_len, repo, false)) < 0) goto cleanup; @@ -770,6 +729,8 @@ int git_revparse_single(git_object **out, git_repository *repo, const char *spec { git_object *temp_object = NULL; + should_return_reference = false; + if ((error = extract_how_many(&n, spec, &pos)) < 0) goto cleanup; @@ -788,6 +749,8 @@ int git_revparse_single(git_object **out, git_repository *repo, const char *spec { git_object *temp_object = NULL; + should_return_reference = false; + if ((error = extract_path(&buf, spec, &pos)) < 0) goto cleanup; @@ -852,7 +815,14 @@ int git_revparse_single(git_object **out, git_repository *repo, const char *spec if ((error = ensure_base_rev_loaded(&base_rev, &reference, spec, identifier_len, repo, false)) < 0) goto cleanup; - *out = base_rev; + if (!should_return_reference) { + git_reference_free(reference); + reference = NULL; + } + + *object_out = base_rev; + *reference_out = reference; + *identifier_len_out = identifier_len; error = 0; cleanup: @@ -862,33 +832,99 @@ cleanup: "Failed to parse revision specifier - Invalid pattern '%s'", spec); git_object_free(base_rev); + git_reference_free(reference); } - git_reference_free(reference); + git_buf_free(&buf); return error; } -int git_revparse_rangelike(git_object **left, git_object **right, int *threedots, git_repository *repo, const char *rangelike) +int git_revparse_ext( + git_object **object_out, + git_reference **reference_out, + git_repository *repo, + const char *spec) { + int error; + size_t identifier_len; + git_object *obj = NULL; + git_reference *ref = NULL; + + if ((error = revparse__ext(&obj, &ref, &identifier_len, repo, spec)) < 0) + goto cleanup; + + *object_out = obj; + *reference_out = ref; + GIT_UNUSED(identifier_len); + + return 0; + +cleanup: + git_object_free(obj); + git_reference_free(ref); + return error; +} + +int git_revparse_single(git_object **out, git_repository *repo, const char *spec) +{ + int error; + git_object *obj = NULL; + git_reference *ref = NULL; + + *out = NULL; + + if ((error = git_revparse_ext(&obj, &ref, repo, spec)) < 0) + goto cleanup; + + git_reference_free(ref); + + *out = obj; + + return 0; + +cleanup: + git_object_free(obj); + git_reference_free(ref); + return error; +} + +int git_revparse( + git_revspec *revspec, + git_repository *repo, + const char *spec) +{ + const char *dotdot; int error = 0; - const char *p, *q; - char *revspec; - p = strstr(rangelike, ".."); - if (!p) { - giterr_set(GITERR_INVALID, "Malformed range (or rangelike syntax): %s", rangelike); - return GIT_EINVALIDSPEC; - } else if (p[2] == '.') { - *threedots = 1; - q = p + 3; + assert(revspec && repo && spec); + + memset(revspec, 0x0, sizeof(*revspec)); + + if ((dotdot = strstr(spec, "..")) != NULL) { + char *lstr; + const char *rstr; + revspec->flags = GIT_REVPARSE_RANGE; + + lstr = git__substrdup(spec, dotdot - spec); + rstr = dotdot + 2; + if (dotdot[2] == '.') { + revspec->flags |= GIT_REVPARSE_MERGE_BASE; + rstr++; + } + + if ((error = git_revparse_single(&revspec->from, repo, lstr)) < 0) { + return error; + } + + if ((error = git_revparse_single(&revspec->to, repo, rstr)) < 0) { + return error; + } + + git__free((void*)lstr); } else { - *threedots = 0; - q = p + 2; + revspec->flags = GIT_REVPARSE_SINGLE; + error = git_revparse_single(&revspec->from, repo, spec); } - revspec = git__substrdup(rangelike, p - rangelike); - error = (git_revparse_single(left, repo, revspec) - || git_revparse_single(right, repo, q)); - git__free(revspec); return error; } diff --git a/src/revwalk.c b/src/revwalk.c index c1071843b..528d02b20 100644 --- a/src/revwalk.c +++ b/src/revwalk.c @@ -186,7 +186,7 @@ static int push_glob(git_revwalk *walk, const char *glob, int hide) data.hide = hide; if (git_reference_foreach_glob( - walk->repo, git_buf_cstr(&buf), GIT_REF_LISTALL, push_glob_cb, &data) < 0) + walk->repo, git_buf_cstr(&buf), push_glob_cb, &data) < 0) goto on_error; regfree(&preg); @@ -231,25 +231,26 @@ int git_revwalk_push_ref(git_revwalk *walk, const char *refname) int git_revwalk_push_range(git_revwalk *walk, const char *range) { - git_object *left, *right; - int threedots; + git_revspec revspec; int error = 0; - if ((error = git_revparse_rangelike(&left, &right, &threedots, walk->repo, range))) + if ((error = git_revparse(&revspec, walk->repo, range))) return error; - if (threedots) { + + if (revspec.flags & GIT_REVPARSE_MERGE_BASE) { /* TODO: support "<commit>...<commit>" */ giterr_set(GITERR_INVALID, "Symmetric differences not implemented in revwalk"); return GIT_EINVALIDSPEC; } - if ((error = push_commit(walk, git_object_id(left), 1))) + if ((error = push_commit(walk, git_object_id(revspec.from), 1))) goto out; - error = push_commit(walk, git_object_id(right), 0); - out: - git_object_free(left); - git_object_free(right); + error = push_commit(walk, git_object_id(revspec.to), 0); + +out: + git_object_free(revspec.from); + git_object_free(revspec.to); return error; } diff --git a/src/signature.c b/src/signature.c index 164e8eb67..0a34ccfaa 100644 --- a/src/signature.c +++ b/src/signature.c @@ -9,6 +9,7 @@ #include "signature.h" #include "repository.h" #include "git2/common.h" +#include "posix.h" void git_signature_free(git_signature *sig) { @@ -35,11 +36,11 @@ static bool contains_angle_brackets(const char *input) static char *extract_trimmed(const char *ptr, size_t len) { - while (len && ptr[0] == ' ') { + while (len && git__isspace(ptr[0])) { ptr++; len--; } - while (len && ptr[len - 1] == ' ') { + while (len && git__isspace(ptr[len - 1])) { len--; } @@ -66,10 +67,12 @@ int git_signature_new(git_signature **sig_out, const char *name, const char *ema p->name = extract_trimmed(name, strlen(name)); p->email = extract_trimmed(email, strlen(email)); - if (p->name == NULL || p->email == NULL || - p->name[0] == '\0' || p->email[0] == '\0') { + if (p->name == NULL || p->email == NULL) + return -1; /* oom */ + + if (p->name[0] == '\0') { git_signature_free(p); - return -1; + return signature_error("Signature cannot have an empty name"); } p->when.time = time; @@ -81,9 +84,16 @@ int git_signature_new(git_signature **sig_out, const char *name, const char *ema git_signature *git_signature_dup(const git_signature *sig) { - git_signature *new; - if (git_signature_new(&new, sig->name, sig->email, sig->when.time, sig->when.offset) < 0) + git_signature *new = git__calloc(1, sizeof(git_signature)); + + if (new == NULL) return NULL; + + new->name = git__strdup(sig->name); + new->email = git__strdup(sig->email); + new->when.time = sig->when.time; + new->when.offset = sig->when.offset; + return new; } @@ -164,9 +174,11 @@ int git_signature__parse(git_signature *sig, const char **buffer_out, tz_start = time_end + 1; - if ((tz_start[0] != '-' && tz_start[0] != '+') || - git__strtol32(&offset, tz_start + 1, &tz_end, 10) < 0) - return signature_error("malformed timezone"); + if ((tz_start[0] != '-' && tz_start[0] != '+') || + git__strtol32(&offset, tz_start + 1, &tz_end, 10) < 0) { + //malformed timezone, just assume it's zero + offset = 0; + } hours = offset / 100; mins = offset % 100; diff --git a/src/stash.c b/src/stash.c index 355c5dc9c..48f19144d 100644 --- a/src/stash.c +++ b/src/stash.c @@ -14,6 +14,7 @@ #include "git2/stash.h" #include "git2/status.h" #include "git2/checkout.h" +#include "git2/index.h" #include "signature.h" static int create_error(int error, const char *msg) @@ -116,7 +117,7 @@ static int build_tree_from_index(git_tree **out, git_index *index) static int commit_index( git_commit **i_commit, git_index *index, - git_signature *stasher, + const git_signature *stasher, const char *message, const git_commit *parent) { @@ -266,7 +267,7 @@ cleanup: static int commit_untracked( git_commit **u_commit, git_index *index, - git_signature *stasher, + const git_signature *stasher, const char *message, git_commit *i_commit, uint32_t flags) @@ -353,7 +354,7 @@ cleanup: static int commit_worktree( git_oid *w_commit_oid, git_index *index, - git_signature *stasher, + const git_signature *stasher, const char *message, git_commit *i_commit, git_commit *b_commit, @@ -430,7 +431,7 @@ cleanup: static int update_reflog( git_oid *w_commit_oid, git_repository *repo, - git_signature *stasher, + const git_signature *stasher, const char *message) { git_reference *stash = NULL; @@ -509,7 +510,7 @@ static int reset_index_and_workdir( int git_stash_save( git_oid *out, git_repository *repo, - git_signature *stasher, + const git_signature *stasher, const char *message, uint32_t flags) { @@ -587,8 +588,10 @@ int git_stash_foreach( const git_reflog_entry *entry; error = git_reference_lookup(&stash, repo, GIT_REFS_STASH_FILE); - if (error == GIT_ENOTFOUND) + if (error == GIT_ENOTFOUND) { + giterr_clear(); return 0; + } if (error < 0) goto cleanup; @@ -651,7 +654,7 @@ int git_stash_drop( const git_reflog_entry *entry; entry = git_reflog_entry_byindex(reflog, 0); - + git_reference_free(stash); error = git_reference_create(&stash, repo, GIT_REFS_STASH_FILE, &entry->oid_cur, 1); } diff --git a/src/status.c b/src/status.c index ac6b4379b..ccb8d37da 100644 --- a/src/status.c +++ b/src/status.c @@ -11,19 +11,20 @@ #include "hash.h" #include "vector.h" #include "tree.h" +#include "status.h" #include "git2/status.h" #include "repository.h" #include "ignore.h" +#include "index.h" #include "git2/diff.h" #include "diff.h" -#include "diff_output.h" -static unsigned int index_delta2status(git_delta_t index_status) +static unsigned int index_delta2status(const git_diff_delta *head2idx) { - unsigned int st = GIT_STATUS_CURRENT; + git_status_t st = GIT_STATUS_CURRENT; - switch (index_status) { + switch (head2idx->status) { case GIT_DELTA_ADDED: case GIT_DELTA_COPIED: st = GIT_STATUS_INDEX_NEW; @@ -36,6 +37,9 @@ static unsigned int index_delta2status(git_delta_t index_status) break; case GIT_DELTA_RENAMED: st = GIT_STATUS_INDEX_RENAMED; + + if (!git_oid_equal(&head2idx->old_file.oid, &head2idx->new_file.oid)) + st |= GIT_STATUS_INDEX_MODIFIED; break; case GIT_DELTA_TYPECHANGE: st = GIT_STATUS_INDEX_TYPECHANGE; @@ -47,13 +51,13 @@ static unsigned int index_delta2status(git_delta_t index_status) return st; } -static unsigned int workdir_delta2status(git_delta_t workdir_status) +static unsigned int workdir_delta2status( + git_diff_list *diff, git_diff_delta *idx2wd) { - unsigned int st = GIT_STATUS_CURRENT; + git_status_t st = GIT_STATUS_CURRENT; - switch (workdir_status) { + switch (idx2wd->status) { case GIT_DELTA_ADDED: - case GIT_DELTA_RENAMED: case GIT_DELTA_COPIED: case GIT_DELTA_UNTRACKED: st = GIT_STATUS_WT_NEW; @@ -67,6 +71,31 @@ static unsigned int workdir_delta2status(git_delta_t workdir_status) case GIT_DELTA_IGNORED: st = GIT_STATUS_IGNORED; break; + case GIT_DELTA_RENAMED: + st = GIT_STATUS_WT_RENAMED; + + if (!git_oid_equal(&idx2wd->old_file.oid, &idx2wd->new_file.oid)) { + /* if OIDs don't match, we might need to calculate them now to + * discern between RENAMED vs RENAMED+MODIFED + */ + if (git_oid_iszero(&idx2wd->old_file.oid) && + diff->old_src == GIT_ITERATOR_TYPE_WORKDIR && + !git_diff__oid_for_file( + diff->repo, idx2wd->old_file.path, idx2wd->old_file.mode, + idx2wd->old_file.size, &idx2wd->old_file.oid)) + idx2wd->old_file.flags |= GIT_DIFF_FLAG_VALID_OID; + + if (git_oid_iszero(&idx2wd->new_file.oid) && + diff->new_src == GIT_ITERATOR_TYPE_WORKDIR && + !git_diff__oid_for_file( + diff->repo, idx2wd->new_file.path, idx2wd->new_file.mode, + idx2wd->new_file.size, &idx2wd->new_file.oid)) + idx2wd->new_file.flags |= GIT_DIFF_FLAG_VALID_OID; + + if (!git_oid_equal(&idx2wd->old_file.oid, &idx2wd->new_file.oid)) + st |= GIT_STATUS_WT_MODIFIED; + } + break; case GIT_DELTA_TYPECHANGE: st = GIT_STATUS_WT_TYPECHANGE; break; @@ -77,142 +106,320 @@ static unsigned int workdir_delta2status(git_delta_t workdir_status) return st; } -typedef struct { - git_status_cb cb; - void *payload; - const git_status_options *opts; -} status_user_callback; - -static int status_invoke_cb( - git_diff_delta *h2i, git_diff_delta *i2w, void *payload) +static bool status_is_included( + git_status_list *status, + git_diff_delta *head2idx, + git_diff_delta *idx2wd) { - status_user_callback *usercb = payload; - const char *path = NULL; - unsigned int status = 0; + if (!(status->opts.flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES)) + return 1; - if (i2w) { - path = i2w->old_file.path; - status |= workdir_delta2status(i2w->status); + /* if excluding submodules and this is a submodule everywhere */ + if (head2idx) { + if (head2idx->status != GIT_DELTA_ADDED && + head2idx->old_file.mode != GIT_FILEMODE_COMMIT) + return 1; + if (head2idx->status != GIT_DELTA_DELETED && + head2idx->new_file.mode != GIT_FILEMODE_COMMIT) + return 1; } - if (h2i) { - path = h2i->old_file.path; - status |= index_delta2status(h2i->status); + if (idx2wd) { + if (idx2wd->status != GIT_DELTA_ADDED && + idx2wd->old_file.mode != GIT_FILEMODE_COMMIT) + return 1; + if (idx2wd->status != GIT_DELTA_DELETED && + idx2wd->new_file.mode != GIT_FILEMODE_COMMIT) + return 1; } - /* if excluding submodules and this is a submodule everywhere */ - if (usercb->opts && - (usercb->opts->flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES) != 0) - { - bool in_tree = (h2i && h2i->status != GIT_DELTA_ADDED); - bool in_index = (h2i && h2i->status != GIT_DELTA_DELETED); - bool in_wd = (i2w && i2w->status != GIT_DELTA_DELETED); - - if ((!in_tree || h2i->old_file.mode == GIT_FILEMODE_COMMIT) && - (!in_index || h2i->new_file.mode == GIT_FILEMODE_COMMIT) && - (!in_wd || i2w->new_file.mode == GIT_FILEMODE_COMMIT)) - return 0; + /* only get here if every valid mode is GIT_FILEMODE_COMMIT */ + return 0; +} + +static git_status_t status_compute( + git_status_list *status, + git_diff_delta *head2idx, + git_diff_delta *idx2wd) +{ + git_status_t st = GIT_STATUS_CURRENT; + + if (head2idx) + st |= index_delta2status(head2idx); + + if (idx2wd) + st |= workdir_delta2status(status->idx2wd, idx2wd); + + return st; +} + +static int status_collect( + git_diff_delta *head2idx, + git_diff_delta *idx2wd, + void *payload) +{ + git_status_list *status = payload; + git_status_entry *status_entry; + + if (!status_is_included(status, head2idx, idx2wd)) + return 0; + + status_entry = git__malloc(sizeof(git_status_entry)); + GITERR_CHECK_ALLOC(status_entry); + + status_entry->status = status_compute(status, head2idx, idx2wd); + status_entry->head_to_index = head2idx; + status_entry->index_to_workdir = idx2wd; + + return git_vector_insert(&status->paired, status_entry); +} + +GIT_INLINE(int) status_entry_cmp_base( + const void *a, + const void *b, + int (*strcomp)(const char *a, const char *b)) +{ + const git_status_entry *entry_a = a; + const git_status_entry *entry_b = b; + const git_diff_delta *delta_a, *delta_b; + + delta_a = entry_a->index_to_workdir ? entry_a->index_to_workdir : + entry_a->head_to_index; + delta_b = entry_b->index_to_workdir ? entry_b->index_to_workdir : + entry_b->head_to_index; + + if (!delta_a && delta_b) + return -1; + if (delta_a && !delta_b) + return 1; + if (!delta_a && !delta_b) + return 0; + + return strcomp(delta_a->new_file.path, delta_b->new_file.path); +} + +static int status_entry_icmp(const void *a, const void *b) +{ + return status_entry_cmp_base(a, b, git__strcasecmp); +} + +static int status_entry_cmp(const void *a, const void *b) +{ + return status_entry_cmp_base(a, b, git__strcmp); +} + +static git_status_list *git_status_list_alloc(git_index *index) +{ + git_status_list *status = NULL; + int (*entrycmp)(const void *a, const void *b); + + if (!(status = git__calloc(1, sizeof(git_status_list)))) + return NULL; + + entrycmp = index->ignore_case ? status_entry_icmp : status_entry_cmp; + + if (git_vector_init(&status->paired, 0, entrycmp) < 0) { + git__free(status); + return NULL; } - return usercb->cb(path, status, usercb->payload); + return status; } -int git_status_foreach_ext( +/* +static int newfile_cmp(const void *a, const void *b) +{ + const git_diff_delta *delta_a = a; + const git_diff_delta *delta_b = b; + + return git__strcmp(delta_a->new_file.path, delta_b->new_file.path); +} + +static int newfile_casecmp(const void *a, const void *b) +{ + const git_diff_delta *delta_a = a; + const git_diff_delta *delta_b = b; + + return git__strcasecmp(delta_a->new_file.path, delta_b->new_file.path); +} +*/ + +int git_status_list_new( + git_status_list **out, git_repository *repo, - const git_status_options *opts, - git_status_cb cb, - void *payload) + const git_status_options *opts) { - int err = 0; + git_index *index = NULL; + git_status_list *status = NULL; git_diff_options diffopt = GIT_DIFF_OPTIONS_INIT; - git_diff_list *head2idx = NULL, *idx2wd = NULL; + git_diff_find_options findopts_i2w = GIT_DIFF_FIND_OPTIONS_INIT; git_tree *head = NULL; git_status_show_t show = opts ? opts->show : GIT_STATUS_SHOW_INDEX_AND_WORKDIR; - status_user_callback usercb; + int error = 0; + unsigned int flags = opts ? opts->flags : GIT_STATUS_OPT_DEFAULTS; + + assert(show <= GIT_STATUS_SHOW_WORKDIR_ONLY); - assert(show <= GIT_STATUS_SHOW_INDEX_THEN_WORKDIR); + *out = NULL; GITERR_CHECK_VERSION(opts, GIT_STATUS_OPTIONS_VERSION, "git_status_options"); - if (show != GIT_STATUS_SHOW_INDEX_ONLY && - (err = git_repository__ensure_not_bare(repo, "status")) < 0) - return err; + if ((error = git_repository__ensure_not_bare(repo, "status")) < 0 || + (error = git_repository_index(&index, repo)) < 0) + return error; /* if there is no HEAD, that's okay - we'll make an empty iterator */ - if (((err = git_repository_head_tree(&head, repo)) < 0) && - !(err == GIT_ENOTFOUND || err == GIT_EORPHANEDHEAD)) - return err; + if (((error = git_repository_head_tree(&head, repo)) < 0) && + error != GIT_ENOTFOUND && error != GIT_EORPHANEDHEAD) { + git_index_free(index); /* release index */ + return error; + } + + status = git_status_list_alloc(index); + GITERR_CHECK_ALLOC(status); - memcpy(&diffopt.pathspec, &opts->pathspec, sizeof(diffopt.pathspec)); + if (opts) { + memcpy(&status->opts, opts, sizeof(git_status_options)); + memcpy(&diffopt.pathspec, &opts->pathspec, sizeof(diffopt.pathspec)); + } diffopt.flags = GIT_DIFF_INCLUDE_TYPECHANGE; - if ((opts->flags & GIT_STATUS_OPT_INCLUDE_UNTRACKED) != 0) + if ((flags & GIT_STATUS_OPT_INCLUDE_UNTRACKED) != 0) diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNTRACKED; - if ((opts->flags & GIT_STATUS_OPT_INCLUDE_IGNORED) != 0) + if ((flags & GIT_STATUS_OPT_INCLUDE_IGNORED) != 0) diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_IGNORED; - if ((opts->flags & GIT_STATUS_OPT_INCLUDE_UNMODIFIED) != 0) + if ((flags & GIT_STATUS_OPT_INCLUDE_UNMODIFIED) != 0) diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNMODIFIED; - if ((opts->flags & GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS) != 0) + if ((flags & GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS) != 0) diffopt.flags = diffopt.flags | GIT_DIFF_RECURSE_UNTRACKED_DIRS; - if ((opts->flags & GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH) != 0) + if ((flags & GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH) != 0) diffopt.flags = diffopt.flags | GIT_DIFF_DISABLE_PATHSPEC_MATCH; - if ((opts->flags & GIT_STATUS_OPT_RECURSE_IGNORED_DIRS) != 0) + if ((flags & GIT_STATUS_OPT_RECURSE_IGNORED_DIRS) != 0) diffopt.flags = diffopt.flags | GIT_DIFF_RECURSE_IGNORED_DIRS; - if ((opts->flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES) != 0) + if ((flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES) != 0) diffopt.flags = diffopt.flags | GIT_DIFF_IGNORE_SUBMODULES; + findopts_i2w.flags |= GIT_DIFF_FIND_FOR_UNTRACKED; + if (show != GIT_STATUS_SHOW_WORKDIR_ONLY) { - err = git_diff_tree_to_index(&head2idx, repo, head, NULL, &diffopt); - if (err < 0) - goto cleanup; - } + if ((error = git_diff_tree_to_index( + &status->head2idx, repo, head, NULL, &diffopt)) < 0) + goto done; - if (show != GIT_STATUS_SHOW_INDEX_ONLY) { - err = git_diff_index_to_workdir(&idx2wd, repo, NULL, &diffopt); - if (err < 0) - goto cleanup; + if ((flags & GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX) != 0 && + (error = git_diff_find_similar(status->head2idx, NULL)) < 0) + goto done; } - usercb.cb = cb; - usercb.payload = payload; - usercb.opts = opts; + if (show != GIT_STATUS_SHOW_INDEX_ONLY) { + if ((error = git_diff_index_to_workdir( + &status->idx2wd, repo, NULL, &diffopt)) < 0) + goto done; - if (show == GIT_STATUS_SHOW_INDEX_THEN_WORKDIR) { - if ((err = git_diff__paired_foreach( - head2idx, NULL, status_invoke_cb, &usercb)) < 0) - goto cleanup; + if ((flags & GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR) != 0 && + (error = git_diff_find_similar(status->idx2wd, &findopts_i2w)) < 0) + goto done; + } - git_diff_list_free(head2idx); - head2idx = NULL; + if ((error = git_diff__paired_foreach( + status->head2idx, status->idx2wd, status_collect, status)) < 0) + goto done; + + if (flags & GIT_STATUS_OPT_SORT_CASE_SENSITIVELY) + git_vector_set_cmp(&status->paired, status_entry_cmp); + if (flags & GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY) + git_vector_set_cmp(&status->paired, status_entry_icmp); + + if ((flags & + (GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | + GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR | + GIT_STATUS_OPT_SORT_CASE_SENSITIVELY | + GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY)) != 0) + git_vector_sort(&status->paired); + +done: + if (error < 0) { + git_status_list_free(status); + status = NULL; } - err = git_diff__paired_foreach(head2idx, idx2wd, status_invoke_cb, &usercb); + *out = status; -cleanup: git_tree_free(head); - git_diff_list_free(head2idx); - git_diff_list_free(idx2wd); + git_index_free(index); - if (err == GIT_EUSER) - giterr_clear(); + return error; +} + +size_t git_status_list_entrycount(git_status_list *status) +{ + assert(status); - return err; + return status->paired.length; } -int git_status_foreach( +const git_status_entry *git_status_byindex(git_status_list *status, size_t i) +{ + assert(status); + + return git_vector_get(&status->paired, i); +} + +void git_status_list_free(git_status_list *status) +{ + git_status_entry *status_entry; + size_t i; + + if (status == NULL) + return; + + git_diff_list_free(status->head2idx); + git_diff_list_free(status->idx2wd); + + git_vector_foreach(&status->paired, i, status_entry) + git__free(status_entry); + + git_vector_free(&status->paired); + + git__memzero(status, sizeof(*status)); + git__free(status); +} + +int git_status_foreach_ext( git_repository *repo, - git_status_cb callback, + const git_status_options *opts, + git_status_cb cb, void *payload) { - git_status_options opts = GIT_STATUS_OPTIONS_INIT; + git_status_list *status; + const git_status_entry *status_entry; + size_t i; + int error = 0; - opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; - opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED | - GIT_STATUS_OPT_INCLUDE_UNTRACKED | - GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + if ((error = git_status_list_new(&status, repo, opts)) < 0) + return error; - return git_status_foreach_ext(repo, &opts, callback, payload); + git_vector_foreach(&status->paired, i, status_entry) { + const char *path = status_entry->head_to_index ? + status_entry->head_to_index->old_file.path : + status_entry->index_to_workdir->old_file.path; + + if (cb(path, status_entry->status, payload) != 0) { + error = GIT_EUSER; + giterr_clear(); + break; + } + } + + git_status_list_free(status); + + return error; +} + +int git_status_foreach(git_repository *repo, git_status_cb cb, void *payload) +{ + return git_status_foreach_ext(repo, NULL, cb, payload); } struct status_file_info { @@ -238,7 +445,7 @@ static int get_one_status(const char *path, unsigned int status, void *data) p_fnmatch(sfi->expected, path, sfi->fnm_flags) != 0)) { sfi->ambiguous = true; - return GIT_EAMBIGUOUS; + return GIT_EAMBIGUOUS; /* giterr_set will be done by caller */ } return 0; @@ -264,8 +471,9 @@ int git_status_file( if (index->ignore_case) sfi.fnm_flags = FNM_CASEFOLD; - opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED | + GIT_STATUS_OPT_RECURSE_IGNORED_DIRS | GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS | GIT_STATUS_OPT_INCLUDE_UNMODIFIED; @@ -281,22 +489,9 @@ int git_status_file( } if (!error && !sfi.count) { - git_buf full = GIT_BUF_INIT; - - /* if the file actually exists and we still did not get a callback - * for it, then it must be contained inside an ignored directory, so - * mark it as such instead of generating an error. - */ - if (!git_buf_joinpath(&full, git_repository_workdir(repo), path) && - git_path_exists(full.ptr)) - sfi.status = GIT_STATUS_IGNORED; - else { - giterr_set(GITERR_INVALID, - "Attempt to get status of nonexistent file '%s'", path); - error = GIT_ENOTFOUND; - } - - git_buf_free(&full); + giterr_set(GITERR_INVALID, + "Attempt to get status of nonexistent file '%s'", path); + error = GIT_ENOTFOUND; } *status_flags = sfi.status; diff --git a/src/status.h b/src/status.h new file mode 100644 index 000000000..b58e0ebd6 --- /dev/null +++ b/src/status.h @@ -0,0 +1,23 @@ +/* + * 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_status_h__ +#define INCLUDE_status_h__ + +#include "diff.h" +#include "git2/status.h" +#include "git2/diff.h" + +struct git_status_list { + git_status_options opts; + + git_diff_list *head2idx; + git_diff_list *idx2wd; + + git_vector paired; +}; + +#endif diff --git a/src/submodule.c b/src/submodule.c index 2fdaf2f77..40bda9a41 100644 --- a/src/submodule.c +++ b/src/submodule.c @@ -7,10 +7,9 @@ #include "common.h" #include "git2/config.h" +#include "git2/sys/config.h" #include "git2/types.h" -#include "git2/repository.h" #include "git2/index.h" -#include "git2/submodule.h" #include "buffer.h" #include "buf_text.h" #include "vector.h" @@ -21,6 +20,8 @@ #include "submodule.h" #include "tree.h" #include "iterator.h" +#include "path.h" +#include "index.h" #define GIT_MODULES_FILE ".gitmodules" @@ -29,6 +30,8 @@ static git_cvar_map _sm_update_map[] = { {GIT_CVAR_STRING, "rebase", GIT_SUBMODULE_UPDATE_REBASE}, {GIT_CVAR_STRING, "merge", GIT_SUBMODULE_UPDATE_MERGE}, {GIT_CVAR_STRING, "none", GIT_SUBMODULE_UPDATE_NONE}, + {GIT_CVAR_FALSE, NULL, GIT_SUBMODULE_UPDATE_NONE}, + {GIT_CVAR_TRUE, NULL, GIT_SUBMODULE_UPDATE_CHECKOUT}, }; static git_cvar_map _sm_ignore_map[] = { @@ -36,6 +39,8 @@ static git_cvar_map _sm_ignore_map[] = { {GIT_CVAR_STRING, "untracked", GIT_SUBMODULE_IGNORE_UNTRACKED}, {GIT_CVAR_STRING, "dirty", GIT_SUBMODULE_IGNORE_DIRTY}, {GIT_CVAR_STRING, "all", GIT_SUBMODULE_IGNORE_ALL}, + {GIT_CVAR_FALSE, NULL, GIT_SUBMODULE_IGNORE_NONE}, + {GIT_CVAR_TRUE, NULL, GIT_SUBMODULE_IGNORE_ALL}, }; static kh_inline khint_t str_hash_no_trailing_slash(const char *s) @@ -70,15 +75,11 @@ static int load_submodule_config(git_repository *repo); static git_config_backend *open_gitmodules(git_repository *, bool, const git_oid *); static int lookup_head_remote(git_buf *url, git_repository *repo); static int submodule_get(git_submodule **, git_repository *, const char *, const char *); -static void submodule_release(git_submodule *sm, int decr); -static int submodule_load_from_index(git_repository *, const git_index_entry *); -static int submodule_load_from_head(git_repository*, const char*, const git_oid*); static int submodule_load_from_config(const git_config_entry *, void *); static int submodule_load_from_wd_lite(git_submodule *, const char *, void *); static int submodule_update_config(git_submodule *, const char *, const char *, bool, bool); -static void submodule_mode_mismatch(git_repository *, const char *, unsigned int); -static int submodule_index_status(unsigned int *status, git_submodule *sm); -static int submodule_wd_status(unsigned int *status, git_submodule *sm); +static void submodule_get_index_status(unsigned int *, git_submodule *); +static void submodule_get_wd_status(unsigned int *, git_submodule *, git_repository *, git_submodule_ignore_t); static int submodule_cmp(const void *a, const void *b) { @@ -127,6 +128,10 @@ int git_submodule_lookup( git_buf_free(&path); } + giterr_set(GITERR_SUBMODULE, (error == GIT_ENOTFOUND) ? + "No submodule named '%s'" : + "Submodule '%s' has not been added yet", name); + return error; } @@ -144,7 +149,7 @@ int git_submodule_foreach( int error; git_submodule *sm; git_vector seen = GIT_VECTOR_INIT; - seen._cmp = submodule_cmp; + git_vector_set_cmp(&seen, submodule_cmp); assert(repo && callback); @@ -156,7 +161,7 @@ int git_submodule_foreach( * us from issuing a callback twice for a submodule where the name * and path are not the same. */ - if (sm->refcount > 1) { + if (GIT_REFCOUNT_VAL(sm) > 1) { if (git_vector_bsearch(NULL, &seen, sm) != GIT_ENOTFOUND) continue; if ((error = git_vector_insert(&seen, sm)) < 0) @@ -188,9 +193,7 @@ void git_submodule_config_free(git_repository *repo) if (smcfg == NULL) return; - git_strmap_foreach_value(smcfg, sm, { - submodule_release(sm,1); - }); + git_strmap_foreach_value(smcfg, sm, { git_submodule_free(sm); }); git_strmap_free(smcfg); } @@ -331,7 +334,7 @@ int git_submodule_add_finalize(git_submodule *sm) assert(sm); - if ((error = git_repository_index__weakptr(&index, sm->owner)) < 0 || + if ((error = git_repository_index__weakptr(&index, sm->repo)) < 0 || (error = git_index_add_bypath(index, GIT_MODULES_FILE)) < 0) return error; @@ -341,7 +344,7 @@ int git_submodule_add_finalize(git_submodule *sm) int git_submodule_add_to_index(git_submodule *sm, int write_index) { int error; - git_repository *repo, *sm_repo = NULL; + git_repository *sm_repo = NULL; git_index *index; git_buf path = GIT_BUF_INIT; git_commit *head; @@ -350,14 +353,12 @@ int git_submodule_add_to_index(git_submodule *sm, int write_index) assert(sm); - repo = sm->owner; - /* force reload of wd OID by git_submodule_open */ sm->flags = sm->flags & ~GIT_SUBMODULE_STATUS__WD_OID_VALID; - if ((error = git_repository_index__weakptr(&index, repo)) < 0 || + if ((error = git_repository_index__weakptr(&index, sm->repo)) < 0 || (error = git_buf_joinpath( - &path, git_repository_workdir(repo), sm->path)) < 0 || + &path, git_repository_workdir(sm->repo), sm->path)) < 0 || (error = git_submodule_open(&sm_repo, sm)) < 0) goto cleanup; @@ -409,15 +410,34 @@ cleanup: return error; } +const char *git_submodule_ignore_to_str(git_submodule_ignore_t ignore) +{ + int i; + for (i = 0; i < (int)ARRAY_SIZE(_sm_ignore_map); ++i) + if (_sm_ignore_map[i].map_value == ignore) + return _sm_ignore_map[i].str_match; + return NULL; +} + +const char *git_submodule_update_to_str(git_submodule_update_t update) +{ + int i; + for (i = 0; i < (int)ARRAY_SIZE(_sm_update_map); ++i) + if (_sm_update_map[i].map_value == update) + return _sm_update_map[i].str_match; + return NULL; +} + int git_submodule_save(git_submodule *submodule) { int error = 0; git_config_backend *mods; git_buf key = GIT_BUF_INIT; + const char *val; assert(submodule); - mods = open_gitmodules(submodule->owner, true, NULL); + mods = open_gitmodules(submodule->repo, true, NULL); if (!mods) { giterr_set(GITERR_SUBMODULE, "Adding submodules to a bare repository is not supported (for now)"); @@ -438,22 +458,14 @@ int git_submodule_save(git_submodule *submodule) goto cleanup; if (!(error = submodule_config_key_trunc_puts(&key, "update")) && - submodule->update != GIT_SUBMODULE_UPDATE_DEFAULT) - { - const char *val = (submodule->update == GIT_SUBMODULE_UPDATE_CHECKOUT) ? - NULL : _sm_update_map[submodule->update].str_match; + (val = git_submodule_update_to_str(submodule->update)) != NULL) error = git_config_file_set_string(mods, key.ptr, val); - } if (error < 0) goto cleanup; if (!(error = submodule_config_key_trunc_puts(&key, "ignore")) && - submodule->ignore != GIT_SUBMODULE_IGNORE_DEFAULT) - { - const char *val = (submodule->ignore == GIT_SUBMODULE_IGNORE_NONE) ? - NULL : _sm_ignore_map[submodule->ignore].str_match; + (val = git_submodule_ignore_to_str(submodule->ignore)) != NULL) error = git_config_file_set_string(mods, key.ptr, val); - } if (error < 0) goto cleanup; @@ -480,7 +492,7 @@ cleanup: git_repository *git_submodule_owner(git_submodule *submodule) { assert(submodule); - return submodule->owner; + return submodule->repo; } const char *git_submodule_name(git_submodule *submodule) @@ -537,11 +549,12 @@ const git_oid *git_submodule_wd_id(git_submodule *submodule) { assert(submodule); + /* load unless we think we have a valid oid */ if (!(submodule->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID)) { git_repository *subrepo; /* calling submodule open grabs the HEAD OID if possible */ - if (!git_submodule_open(&subrepo, submodule)) + if (!git_submodule_open_bare(&subrepo, submodule)) git_repository_free(subrepo); else giterr_clear(); @@ -556,7 +569,8 @@ const git_oid *git_submodule_wd_id(git_submodule *submodule) git_submodule_ignore_t git_submodule_ignore(git_submodule *submodule) { assert(submodule); - return submodule->ignore; + return (submodule->ignore < GIT_SUBMODULE_IGNORE_NONE) ? + GIT_SUBMODULE_IGNORE_NONE : submodule->ignore; } git_submodule_ignore_t git_submodule_set_ignore( @@ -566,7 +580,7 @@ git_submodule_ignore_t git_submodule_set_ignore( assert(submodule); - if (ignore == GIT_SUBMODULE_IGNORE_DEFAULT) + if (ignore == GIT_SUBMODULE_IGNORE_RESET) ignore = submodule->ignore_default; old = submodule->ignore; @@ -577,7 +591,8 @@ git_submodule_ignore_t git_submodule_set_ignore( git_submodule_update_t git_submodule_update(git_submodule *submodule) { assert(submodule); - return submodule->update; + return (submodule->update < GIT_SUBMODULE_UPDATE_CHECKOUT) ? + GIT_SUBMODULE_UPDATE_CHECKOUT : submodule->update; } git_submodule_update_t git_submodule_set_update( @@ -587,7 +602,7 @@ git_submodule_update_t git_submodule_set_update( assert(submodule); - if (update == GIT_SUBMODULE_UPDATE_DEFAULT) + if (update == GIT_SUBMODULE_UPDATE_RESET) update = submodule->update_default; old = submodule->update; @@ -618,6 +633,7 @@ int git_submodule_set_fetch_recurse_submodules( int git_submodule_init(git_submodule *submodule, int overwrite) { int error; + const char *val; /* write "submodule.NAME.url" */ @@ -634,14 +650,10 @@ int git_submodule_init(git_submodule *submodule, int overwrite) /* write "submodule.NAME.update" if not default */ - if (submodule->update == GIT_SUBMODULE_UPDATE_CHECKOUT) - error = submodule_update_config( - submodule, "update", NULL, (overwrite != 0), false); - else if (submodule->update != GIT_SUBMODULE_UPDATE_DEFAULT) - error = submodule_update_config( - submodule, "update", - _sm_update_map[submodule->update].str_match, - (overwrite != 0), false); + val = (submodule->update == GIT_SUBMODULE_UPDATE_CHECKOUT) ? + NULL : git_submodule_update_to_str(submodule->update); + error = submodule_update_config( + submodule, "update", val, (overwrite != 0), false); return error; } @@ -660,51 +672,70 @@ int git_submodule_sync(git_submodule *submodule) submodule, "url", submodule->url, true, true); } -int git_submodule_open( - git_repository **subrepo, - git_submodule *submodule) +static int git_submodule__open( + git_repository **subrepo, git_submodule *sm, bool bare) { int error; git_buf path = GIT_BUF_INIT; - git_repository *repo; - const char *workdir; - - assert(submodule && subrepo); + unsigned int flags = GIT_REPOSITORY_OPEN_NO_SEARCH; + const char *wd; - repo = submodule->owner; - workdir = git_repository_workdir(repo); + assert(sm && subrepo); - if (!workdir) { - giterr_set(GITERR_REPOSITORY, - "Cannot open submodule repository in a bare repo"); - return GIT_ENOTFOUND; - } + if (git_repository__ensure_not_bare( + sm->repo, "open submodule repository") < 0) + return GIT_EBAREREPO; - if ((submodule->flags & GIT_SUBMODULE_STATUS_IN_WD) == 0) { - giterr_set(GITERR_REPOSITORY, - "Cannot open submodule repository that is not checked out"); - return GIT_ENOTFOUND; - } + wd = git_repository_workdir(sm->repo); - if (git_buf_joinpath(&path, workdir, submodule->path) < 0) + if (git_buf_joinpath(&path, wd, sm->path) < 0 || + git_buf_joinpath(&path, path.ptr, DOT_GIT) < 0) return -1; - error = git_repository_open(subrepo, path.ptr); + sm->flags = sm->flags & + ~(GIT_SUBMODULE_STATUS_IN_WD | + GIT_SUBMODULE_STATUS__WD_OID_VALID | + GIT_SUBMODULE_STATUS__WD_SCANNED); - git_buf_free(&path); + if (bare) + flags |= GIT_REPOSITORY_OPEN_BARE; - /* if we have opened the submodule successfully, let's grab the HEAD OID */ + error = git_repository_open_ext(subrepo, path.ptr, flags, wd); + + /* if we opened the submodule successfully, grab HEAD OID, etc. */ if (!error) { - if (!git_reference_name_to_id( - &submodule->wd_oid, *subrepo, GIT_HEAD_FILE)) - submodule->flags |= GIT_SUBMODULE_STATUS__WD_OID_VALID; + sm->flags |= GIT_SUBMODULE_STATUS_IN_WD | + GIT_SUBMODULE_STATUS__WD_SCANNED; + + if (!git_reference_name_to_id(&sm->wd_oid, *subrepo, GIT_HEAD_FILE)) + sm->flags |= GIT_SUBMODULE_STATUS__WD_OID_VALID; else giterr_clear(); + } else if (git_path_exists(path.ptr)) { + sm->flags |= GIT_SUBMODULE_STATUS__WD_SCANNED | + GIT_SUBMODULE_STATUS_IN_WD; + } else { + git_buf_rtruncate_at_char(&path, '/'); /* remove "/.git" */ + + if (git_path_isdir(path.ptr)) + sm->flags |= GIT_SUBMODULE_STATUS__WD_SCANNED; } + git_buf_free(&path); + return error; } +int git_submodule_open_bare(git_repository **subrepo, git_submodule *sm) +{ + return git_submodule__open(subrepo, sm, true); +} + +int git_submodule_open(git_repository **subrepo, git_submodule *sm) +{ + return git_submodule__open(subrepo, sm, false); +} + int git_submodule_reload_all(git_repository *repo) { assert(repo); @@ -712,74 +743,100 @@ int git_submodule_reload_all(git_repository *repo) return load_submodule_config(repo); } -int git_submodule_reload(git_submodule *submodule) +static void submodule_update_from_index_entry( + git_submodule *sm, const git_index_entry *ie) { - git_repository *repo; - git_index *index; - int error; - size_t pos; - git_tree *head; - git_config_backend *mods; + bool already_found = (sm->flags & GIT_SUBMODULE_STATUS_IN_INDEX) != 0; - assert(submodule); + if (!S_ISGITLINK(ie->mode)) { + if (!already_found) + sm->flags |= GIT_SUBMODULE_STATUS__INDEX_NOT_SUBMODULE; + } else { + if (already_found) + sm->flags |= GIT_SUBMODULE_STATUS__INDEX_MULTIPLE_ENTRIES; + else + git_oid_cpy(&sm->index_oid, &ie->oid); - /* refresh index data */ + sm->flags |= GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS__INDEX_OID_VALID; + } +} + +static int submodule_update_index(git_submodule *sm) +{ + git_index *index; + const git_index_entry *ie; - repo = submodule->owner; - if (git_repository_index__weakptr(&index, repo) < 0) + if (git_repository_index__weakptr(&index, sm->repo) < 0) return -1; - submodule->flags = submodule->flags & + sm->flags = sm->flags & ~(GIT_SUBMODULE_STATUS_IN_INDEX | GIT_SUBMODULE_STATUS__INDEX_OID_VALID); - if (!git_index_find(&pos, index, submodule->path)) { - const git_index_entry *entry = git_index_get_byindex(index, pos); + if (!(ie = git_index_get_bypath(index, sm->path, 0))) + return 0; - if (S_ISGITLINK(entry->mode)) { - if ((error = submodule_load_from_index(repo, entry)) < 0) - return error; - } else { - submodule_mode_mismatch( - repo, entry->path, GIT_SUBMODULE_STATUS__INDEX_NOT_SUBMODULE); - } + submodule_update_from_index_entry(sm, ie); + + return 0; +} + +static void submodule_update_from_head_data( + git_submodule *sm, mode_t mode, const git_oid *id) +{ + if (!S_ISGITLINK(mode)) + sm->flags |= GIT_SUBMODULE_STATUS__HEAD_NOT_SUBMODULE; + else { + git_oid_cpy(&sm->head_oid, id); + + sm->flags |= GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS__HEAD_OID_VALID; } +} - /* refresh HEAD tree data */ +static int submodule_update_head(git_submodule *submodule) +{ + git_tree *head = NULL; + git_tree_entry *te = NULL; - if (!(error = git_repository_head_tree(&head, repo))) { - git_tree_entry *te; + submodule->flags = submodule->flags & + ~(GIT_SUBMODULE_STATUS_IN_HEAD | + GIT_SUBMODULE_STATUS__HEAD_OID_VALID); - submodule->flags = submodule->flags & - ~(GIT_SUBMODULE_STATUS_IN_HEAD | - GIT_SUBMODULE_STATUS__HEAD_OID_VALID); + /* if we can't look up file in current head, then done */ + if (git_repository_head_tree(&head, submodule->repo) < 0 || + git_tree_entry_bypath(&te, head, submodule->path) < 0) + giterr_clear(); + else + submodule_update_from_head_data(submodule, te->attr, &te->oid); - if (!(error = git_tree_entry_bypath(&te, head, submodule->path))) { + git_tree_entry_free(te); + git_tree_free(head); + return 0; +} - if (S_ISGITLINK(te->attr)) { - error = submodule_load_from_head(repo, submodule->path, &te->oid); - } else { - submodule_mode_mismatch( - repo, submodule->path, - GIT_SUBMODULE_STATUS__HEAD_NOT_SUBMODULE); - } +int git_submodule_reload(git_submodule *submodule) +{ + int error = 0; + git_config_backend *mods; - git_tree_entry_free(te); - } - else if (error == GIT_ENOTFOUND) { - giterr_clear(); - error = 0; - } + assert(submodule); - git_tree_free(head); - } + /* refresh index data */ - if (error < 0) - return error; + if (submodule_update_index(submodule) < 0) + return -1; + + /* refresh HEAD tree data */ + + if (submodule_update_head(submodule) < 0) + return -1; /* refresh config data */ - if ((mods = open_gitmodules(repo, false, NULL)) != NULL) { + mods = open_gitmodules(submodule->repo, false, NULL); + if (mods != NULL) { git_buf path = GIT_BUF_INIT; git_buf_sets(&path, "submodule\\."); @@ -790,7 +847,7 @@ int git_submodule_reload(git_submodule *submodule) error = -1; else error = git_config_file_foreach_match( - mods, path.ptr, submodule_load_from_config, repo); + mods, path.ptr, submodule_load_from_config, submodule->repo); git_buf_free(&path); git_config_file_free(mods); @@ -809,38 +866,90 @@ int git_submodule_reload(git_submodule *submodule) return error; } -int git_submodule_status( - unsigned int *status, - git_submodule *submodule) +static void submodule_copy_oid_maybe( + git_oid *tgt, const git_oid *src, bool valid) { - int error = 0; - unsigned int status_val; + if (tgt) { + if (valid) + memcpy(tgt, src, sizeof(*tgt)); + else + memset(tgt, 0, sizeof(*tgt)); + } +} - assert(status && submodule); +int git_submodule__status( + unsigned int *out_status, + git_oid *out_head_id, + git_oid *out_index_id, + git_oid *out_wd_id, + git_submodule *sm, + git_submodule_ignore_t ign) +{ + unsigned int status; + git_repository *smrepo = NULL; - status_val = GIT_SUBMODULE_STATUS__CLEAR_INTERNAL(submodule->flags); + if (ign < GIT_SUBMODULE_IGNORE_NONE) + ign = sm->ignore; - if (submodule->ignore != GIT_SUBMODULE_IGNORE_ALL) { - if (!(error = submodule_index_status(&status_val, submodule))) - error = submodule_wd_status(&status_val, submodule); + /* only return location info if ignore == all */ + if (ign == GIT_SUBMODULE_IGNORE_ALL) { + *out_status = (sm->flags & GIT_SUBMODULE_STATUS__IN_FLAGS); + return 0; } - *status = status_val; + /* refresh the index OID */ + if (submodule_update_index(sm) < 0) + return -1; - return error; + /* refresh the HEAD OID */ + if (submodule_update_head(sm) < 0) + return -1; + + /* for ignore == dirty, don't scan the working directory */ + if (ign == GIT_SUBMODULE_IGNORE_DIRTY) { + /* git_submodule_open_bare will load WD OID data */ + if (git_submodule_open_bare(&smrepo, sm) < 0) + giterr_clear(); + else + git_repository_free(smrepo); + smrepo = NULL; + } else if (git_submodule_open(&smrepo, sm) < 0) { + giterr_clear(); + smrepo = NULL; + } + + status = GIT_SUBMODULE_STATUS__CLEAR_INTERNAL(sm->flags); + + submodule_get_index_status(&status, sm); + submodule_get_wd_status(&status, sm, smrepo, ign); + + git_repository_free(smrepo); + + *out_status = status; + + submodule_copy_oid_maybe(out_head_id, &sm->head_oid, + (sm->flags & GIT_SUBMODULE_STATUS__HEAD_OID_VALID) != 0); + submodule_copy_oid_maybe(out_index_id, &sm->index_oid, + (sm->flags & GIT_SUBMODULE_STATUS__INDEX_OID_VALID) != 0); + submodule_copy_oid_maybe(out_wd_id, &sm->wd_oid, + (sm->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) != 0); + + return 0; } -int git_submodule_location( - unsigned int *location_status, - git_submodule *submodule) +int git_submodule_status(unsigned int *status, git_submodule *sm) { - assert(location_status && submodule); + assert(status && sm); - *location_status = submodule->flags & - (GIT_SUBMODULE_STATUS_IN_HEAD | GIT_SUBMODULE_STATUS_IN_INDEX | - GIT_SUBMODULE_STATUS_IN_CONFIG | GIT_SUBMODULE_STATUS_IN_WD); + return git_submodule__status(status, NULL, NULL, NULL, sm, 0); +} - return 0; +int git_submodule_location(unsigned int *location, git_submodule *sm) +{ + assert(location && sm); + + return git_submodule__status( + location, NULL, NULL, NULL, sm, GIT_SUBMODULE_IGNORE_ALL); } @@ -850,54 +959,50 @@ int git_submodule_location( static git_submodule *submodule_alloc(git_repository *repo, const char *name) { + size_t namelen; git_submodule *sm; - if (!name || !strlen(name)) { + if (!name || !(namelen = strlen(name))) { giterr_set(GITERR_SUBMODULE, "Invalid submodule name"); return NULL; } sm = git__calloc(1, sizeof(git_submodule)); if (sm == NULL) - goto fail; + return NULL; - sm->path = sm->name = git__strdup(name); - if (!sm->name) - goto fail; + sm->name = sm->path = git__strdup(name); + if (!sm->name) { + git__free(sm); + return NULL; + } - sm->owner = repo; - sm->refcount = 1; + GIT_REFCOUNT_INC(sm); + sm->ignore = sm->ignore_default = GIT_SUBMODULE_IGNORE_NONE; + sm->update = sm->update_default = GIT_SUBMODULE_UPDATE_CHECKOUT; + sm->repo = repo; return sm; - -fail: - submodule_release(sm, 0); - return NULL; } -static void submodule_release(git_submodule *sm, int decr) +static void submodule_release(git_submodule *sm) { if (!sm) return; - sm->refcount -= decr; - - if (sm->refcount == 0) { - if (sm->name != sm->path) { - git__free(sm->path); - sm->path = NULL; - } - - git__free(sm->name); - sm->name = NULL; - - git__free(sm->url); - sm->url = NULL; - - sm->owner = NULL; + if (sm->path != sm->name) + git__free(sm->path); + git__free(sm->name); + git__free(sm->url); + git__memzero(sm, sizeof(*sm)); + git__free(sm); +} - git__free(sm); - } +void git_submodule_free(git_submodule *sm) +{ + if (!sm) + return; + GIT_REFCOUNT_DEC(sm, submodule_release); } static int submodule_get( @@ -920,6 +1025,7 @@ static int submodule_get( if (!git_strmap_valid_index(smcfg, pos)) { sm = submodule_alloc(repo, name); + GITERR_CHECK_ALLOC(sm); /* insert value at name - if another thread beats us to it, then use * their record and release our own. @@ -927,10 +1033,10 @@ static int submodule_get( pos = kh_put(str, smcfg, sm->name, &error); if (error < 0) { - submodule_release(sm, 1); + git_submodule_free(sm); sm = NULL; } else if (error == 0) { - submodule_release(sm, 1); + git_submodule_free(sm); sm = git_strmap_value_at(smcfg, pos); } else { git_strmap_set_value_at(smcfg, pos, sm); @@ -944,50 +1050,41 @@ static int submodule_get( return (sm != NULL) ? 0 : -1; } -static int submodule_load_from_index( - git_repository *repo, const git_index_entry *entry) +static int submodule_config_error(const char *property, const char *value) { - git_submodule *sm; + giterr_set(GITERR_INVALID, + "Invalid value for submodule '%s' property: '%s'", property, value); + return -1; +} - if (submodule_get(&sm, repo, entry->path, NULL) < 0) - return -1; +int git_submodule_parse_ignore(git_submodule_ignore_t *out, const char *value) +{ + int val; - if (sm->flags & GIT_SUBMODULE_STATUS_IN_INDEX) { - sm->flags |= GIT_SUBMODULE_STATUS__INDEX_MULTIPLE_ENTRIES; - return 0; + if (git_config_lookup_map_value( + &val, _sm_ignore_map, ARRAY_SIZE(_sm_ignore_map), value) < 0) { + *out = GIT_SUBMODULE_IGNORE_NONE; + return submodule_config_error("ignore", value); } - sm->flags |= GIT_SUBMODULE_STATUS_IN_INDEX; - - git_oid_cpy(&sm->index_oid, &entry->oid); - sm->flags |= GIT_SUBMODULE_STATUS__INDEX_OID_VALID; - + *out = (git_submodule_ignore_t)val; return 0; } -static int submodule_load_from_head( - git_repository *repo, const char *path, const git_oid *oid) +int git_submodule_parse_update(git_submodule_update_t *out, const char *value) { - git_submodule *sm; - - if (submodule_get(&sm, repo, path, NULL) < 0) - return -1; - - sm->flags |= GIT_SUBMODULE_STATUS_IN_HEAD; + int val; - git_oid_cpy(&sm->head_oid, oid); - sm->flags |= GIT_SUBMODULE_STATUS__HEAD_OID_VALID; + if (git_config_lookup_map_value( + &val, _sm_update_map, ARRAY_SIZE(_sm_update_map), value) < 0) { + *out = GIT_SUBMODULE_UPDATE_CHECKOUT; + return submodule_config_error("update", value); + } + *out = (git_submodule_update_t)val; return 0; } -static int submodule_config_error(const char *property, const char *value) -{ - giterr_set(GITERR_INVALID, - "Invalid value for submodule '%s' property: '%s'", property, value); - return -1; -} - static int submodule_load_from_config( const git_config_entry *entry, void *data) { @@ -1005,8 +1102,10 @@ static int submodule_load_from_config( namestart = key + strlen("submodule."); property = strrchr(namestart, '.'); - if (property == NULL) + + if (!property || (property == namestart)) return 0; + property++; is_path = (strcasecmp(property, "path") == 0); @@ -1040,11 +1139,11 @@ static int submodule_load_from_config( git_strmap_insert2(smcfg, alternate, sm, old_sm, error); if (error >= 0) - sm->refcount++; /* inserted under a new key */ + GIT_REFCOUNT_INC(sm); /* inserted under a new key */ /* if we replaced an old module under this key, release the old one */ if (old_sm && ((git_submodule *)old_sm) != sm) { - submodule_release(old_sm, 1); + git_submodule_free(old_sm); /* TODO: log warning about multiple submodules with same path */ } } @@ -1069,22 +1168,18 @@ static int submodule_load_from_config( return -1; } else if (strcasecmp(property, "update") == 0) { - int val; - if (git_config_lookup_map_value( - &val, _sm_update_map, ARRAY_SIZE(_sm_update_map), value) < 0) - return submodule_config_error("update", value); - sm->update_default = sm->update = (git_submodule_update_t)val; + if (git_submodule_parse_update(&sm->update, value) < 0) + return -1; + sm->update_default = sm->update; } else if (strcasecmp(property, "fetchRecurseSubmodules") == 0) { if (git__parse_bool(&sm->fetch_recurse, value) < 0) return submodule_config_error("fetchRecurseSubmodules", value); } else if (strcasecmp(property, "ignore") == 0) { - int val; - if (git_config_lookup_map_value( - &val, _sm_ignore_map, ARRAY_SIZE(_sm_ignore_map), value) < 0) - return submodule_config_error("ignore", value); - sm->ignore_default = sm->ignore = (git_submodule_ignore_t)val; + if (git_submodule_parse_ignore(&sm->ignore, value) < 0) + return -1; + sm->ignore_default = sm->ignore; } /* ignore other unknown submodule properties */ @@ -1094,13 +1189,15 @@ static int submodule_load_from_config( static int submodule_load_from_wd_lite( git_submodule *sm, const char *name, void *payload) { - git_repository *repo = git_submodule_owner(sm); git_buf path = GIT_BUF_INIT; GIT_UNUSED(name); GIT_UNUSED(payload); - if (git_buf_joinpath(&path, git_repository_workdir(repo), sm->path) < 0) + if (git_repository_is_bare(sm->repo)) + return 0; + + if (git_buf_joinpath(&path, git_repository_workdir(sm->repo), sm->path) < 0) return -1; if (git_path_isdir(path.ptr)) @@ -1114,18 +1211,6 @@ static int submodule_load_from_wd_lite( return 0; } -static void submodule_mode_mismatch( - git_repository *repo, const char *path, unsigned int flag) -{ - khiter_t pos = git_strmap_lookup_index(repo->submodules, path); - - if (git_strmap_valid_index(repo->submodules, pos)) { - git_submodule *sm = git_strmap_value_at(repo->submodules, pos); - - sm->flags |= flag; - } -} - static int load_submodule_config_from_index( git_repository *repo, git_oid *gitmodules_oid) { @@ -1138,25 +1223,27 @@ static int load_submodule_config_from_index( (error = git_iterator_for_index(&i, index, 0, NULL, NULL)) < 0) return error; - error = git_iterator_current(&entry, i); - - while (!error && entry != NULL) { - - if (S_ISGITLINK(entry->mode)) { - error = submodule_load_from_index(repo, entry); - if (error < 0) - break; - } else { - submodule_mode_mismatch( - repo, entry->path, GIT_SUBMODULE_STATUS__INDEX_NOT_SUBMODULE); - - if (strcmp(entry->path, GIT_MODULES_FILE) == 0) - git_oid_cpy(gitmodules_oid, &entry->oid); - } - - error = git_iterator_advance(&entry, i); + while (!(error = git_iterator_advance(&entry, i))) { + khiter_t pos = git_strmap_lookup_index(repo->submodules, entry->path); + git_submodule *sm; + + if (git_strmap_valid_index(repo->submodules, pos)) { + sm = git_strmap_value_at(repo->submodules, pos); + + if (S_ISGITLINK(entry->mode)) + submodule_update_from_index_entry(sm, entry); + else + sm->flags |= GIT_SUBMODULE_STATUS__INDEX_NOT_SUBMODULE; + } else if (S_ISGITLINK(entry->mode)) { + if (!submodule_get(&sm, repo, entry->path, NULL)) + submodule_update_from_index_entry(sm, entry); + } else if (strcmp(entry->path, GIT_MODULES_FILE) == 0) + git_oid_cpy(gitmodules_oid, &entry->oid); } + if (error == GIT_ITEROVER) + error = 0; + git_iterator_free(i); return error; @@ -1170,34 +1257,42 @@ static int load_submodule_config_from_head( git_iterator *i; const git_index_entry *entry; - if ((error = git_repository_head_tree(&head, repo)) < 0) - return error; + /* if we can't look up current head, then there's no submodule in it */ + if (git_repository_head_tree(&head, repo) < 0) { + giterr_clear(); + return 0; + } if ((error = git_iterator_for_tree(&i, head, 0, NULL, NULL)) < 0) { git_tree_free(head); return error; } - error = git_iterator_current(&entry, i); - - while (!error && entry != NULL) { - - if (S_ISGITLINK(entry->mode)) { - error = submodule_load_from_head(repo, entry->path, &entry->oid); - if (error < 0) - break; - } else { - submodule_mode_mismatch( - repo, entry->path, GIT_SUBMODULE_STATUS__HEAD_NOT_SUBMODULE); - - if (strcmp(entry->path, GIT_MODULES_FILE) == 0 && - git_oid_iszero(gitmodules_oid)) - git_oid_cpy(gitmodules_oid, &entry->oid); + while (!(error = git_iterator_advance(&entry, i))) { + khiter_t pos = git_strmap_lookup_index(repo->submodules, entry->path); + git_submodule *sm; + + if (git_strmap_valid_index(repo->submodules, pos)) { + sm = git_strmap_value_at(repo->submodules, pos); + + if (S_ISGITLINK(entry->mode)) + submodule_update_from_head_data( + sm, entry->mode, &entry->oid); + else + sm->flags |= GIT_SUBMODULE_STATUS__HEAD_NOT_SUBMODULE; + } else if (S_ISGITLINK(entry->mode)) { + if (!submodule_get(&sm, repo, entry->path, NULL)) + submodule_update_from_head_data( + sm, entry->mode, &entry->oid); + } else if (strcmp(entry->path, GIT_MODULES_FILE) == 0 && + git_oid_iszero(gitmodules_oid)) { + git_oid_cpy(gitmodules_oid, &entry->oid); } - - error = git_iterator_advance(&entry, i); } + if (error == GIT_ITEROVER) + error = 0; + git_iterator_free(i); git_tree_free(head); @@ -1376,7 +1471,7 @@ static int submodule_update_config( assert(submodule); - error = git_repository_config__weakptr(&config, submodule->owner); + error = git_repository_config__weakptr(&config, submodule->repo); if (error < 0) return error; @@ -1404,11 +1499,13 @@ cleanup: return error; } -static int submodule_index_status(unsigned int *status, git_submodule *sm) +static void submodule_get_index_status(unsigned int *status, git_submodule *sm) { const git_oid *head_oid = git_submodule_head_id(sm); const git_oid *index_oid = git_submodule_index_id(sm); + *status = *status & ~GIT_SUBMODULE_STATUS__INDEX_FLAGS; + if (!head_oid) { if (index_oid) *status |= GIT_SUBMODULE_STATUS_INDEX_ADDED; @@ -1417,27 +1514,22 @@ static int submodule_index_status(unsigned int *status, git_submodule *sm) *status |= GIT_SUBMODULE_STATUS_INDEX_DELETED; else if (!git_oid_equal(head_oid, index_oid)) *status |= GIT_SUBMODULE_STATUS_INDEX_MODIFIED; - - return 0; } -static int submodule_wd_status(unsigned int *status, git_submodule *sm) +static void submodule_get_wd_status( + unsigned int *status, + git_submodule *sm, + git_repository *sm_repo, + git_submodule_ignore_t ign) { - int error = 0; - const git_oid *wd_oid, *index_oid; - git_repository *sm_repo = NULL; - - /* open repo now if we need it (so wd_id() call won't reopen) */ - if ((sm->ignore == GIT_SUBMODULE_IGNORE_NONE || - sm->ignore == GIT_SUBMODULE_IGNORE_UNTRACKED) && - (sm->flags & GIT_SUBMODULE_STATUS_IN_WD) != 0) - { - if ((error = git_submodule_open(&sm_repo, sm)) < 0) - return error; - } + const git_oid *index_oid = git_submodule_index_id(sm); + const git_oid *wd_oid = + (sm->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) ? &sm->wd_oid : NULL; + git_tree *sm_head = NULL; + git_diff_options opt = GIT_DIFF_OPTIONS_INIT; + git_diff_list *diff; - index_oid = git_submodule_index_id(sm); - wd_oid = git_submodule_wd_id(sm); + *status = *status & ~GIT_SUBMODULE_STATUS__WD_FLAGS; if (!index_oid) { if (wd_oid) @@ -1453,59 +1545,49 @@ static int submodule_wd_status(unsigned int *status, git_submodule *sm) else if (!git_oid_equal(index_oid, wd_oid)) *status |= GIT_SUBMODULE_STATUS_WD_MODIFIED; - if (sm_repo != NULL) { - git_tree *sm_head; - git_diff_options opt = GIT_DIFF_OPTIONS_INIT; - git_diff_list *diff; - - /* the diffs below could be optimized with an early termination - * option to the git_diff functions, but for now this is sufficient - * (and certainly no worse that what core git does). - */ - - /* perform head-to-index diff on submodule */ - - if ((error = git_repository_head_tree(&sm_head, sm_repo)) < 0) - return error; + /* if we have no repo, then we're done */ + if (!sm_repo) + return; - if (sm->ignore == GIT_SUBMODULE_IGNORE_NONE) - opt.flags |= GIT_DIFF_INCLUDE_UNTRACKED; + /* the diffs below could be optimized with an early termination + * option to the git_diff functions, but for now this is sufficient + * (and certainly no worse that what core git does). + */ - error = git_diff_tree_to_index(&diff, sm_repo, sm_head, NULL, &opt); + if (ign == GIT_SUBMODULE_IGNORE_NONE) + opt.flags |= GIT_DIFF_INCLUDE_UNTRACKED; - if (!error) { + /* if we don't have an orphaned head, check diff with index */ + if (git_repository_head_tree(&sm_head, sm_repo) < 0) + giterr_clear(); + else { + /* perform head to index diff on submodule */ + if (git_diff_tree_to_index(&diff, sm_repo, sm_head, NULL, &opt) < 0) + giterr_clear(); + else { if (git_diff_num_deltas(diff) > 0) *status |= GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED; - git_diff_list_free(diff); diff = NULL; } git_tree_free(sm_head); + } - if (error < 0) - return error; - - /* perform index-to-workdir diff on submodule */ - - error = git_diff_index_to_workdir(&diff, sm_repo, NULL, &opt); - - if (!error) { - size_t untracked = - git_diff_num_deltas_of_type(diff, GIT_DELTA_UNTRACKED); - - if (untracked > 0) - *status |= GIT_SUBMODULE_STATUS_WD_UNTRACKED; + /* perform index-to-workdir diff on submodule */ + if (git_diff_index_to_workdir(&diff, sm_repo, NULL, &opt) < 0) + giterr_clear(); + else { + size_t untracked = + git_diff_num_deltas_of_type(diff, GIT_DELTA_UNTRACKED); - if (git_diff_num_deltas(diff) != untracked) - *status |= GIT_SUBMODULE_STATUS_WD_WD_MODIFIED; + if (untracked > 0) + *status |= GIT_SUBMODULE_STATUS_WD_UNTRACKED; - git_diff_list_free(diff); - diff = NULL; - } + if (git_diff_num_deltas(diff) != untracked) + *status |= GIT_SUBMODULE_STATUS_WD_WD_MODIFIED; - git_repository_free(sm_repo); + git_diff_list_free(diff); + diff = NULL; } - - return error; } diff --git a/src/submodule.h b/src/submodule.h index ba8e2518e..b05937503 100644 --- a/src/submodule.h +++ b/src/submodule.h @@ -7,6 +7,10 @@ #ifndef INCLUDE_submodule_h__ #define INCLUDE_submodule_h__ +#include "git2/submodule.h" +#include "git2/repository.h" +#include "fileops.h" + /* Notes: * * Submodule information can be in four places: the index, the config files @@ -44,44 +48,51 @@ * an entry for every submodule found in the HEAD and index, and for every * submodule described in .gitmodules. The fields are as follows: * - * - `owner` is the git_repository containing this submodule + * - `rc` tracks the refcount of how many hash table entries in the + * git_submodule_cache there are for this submodule. It only comes into + * play if the name and path of the submodule differ. + * * - `name` is the name of the submodule from .gitmodules. * - `path` is the path to the submodule from the repo root. It is almost * always the same as `name`. * - `url` is the url for the submodule. - * - `tree_oid` is the SHA1 for the submodule path in the repo HEAD. - * - `index_oid` is the SHA1 for the submodule recorded in the index. - * - `workdir_oid` is the SHA1 for the HEAD of the checked out submodule. * - `update` is a git_submodule_update_t value - see gitmodules(5) update. + * - `update_default` is the update value from the config * - `ignore` is a git_submodule_ignore_t value - see gitmodules(5) ignore. + * - `ignore_default` is the ignore value from the config * - `fetch_recurse` is 0 or 1 - see gitmodules(5) fetchRecurseSubmodules. - * - `refcount` tracks how many hashmap entries there are for this submodule. - * It only comes into play if the name and path of the submodule differ. - * - `flags` is for internal use, tracking where this submodule has been - * found (head, index, config, workdir) and other misc info about it. + * + * - `repo` is the parent repository that contains this submodule. + * - `flags` after for internal use, tracking where this submodule has been + * found (head, index, config, workdir) and known status info, etc. + * - `head_oid` is the SHA1 for the submodule path in the repo HEAD. + * - `index_oid` is the SHA1 for the submodule recorded in the index. + * - `wd_oid` is the SHA1 for the HEAD of the checked out submodule. * * If the submodule has been added to .gitmodules but not yet git added, - * then the `index_oid` will be valid and zero. If the submodule has been - * deleted, but the delete has not been committed yet, then the `index_oid` - * will be set, but the `url` will be NULL. + * then the `index_oid` will be zero but still marked valid. If the + * submodule has been deleted, but the delete has not been committed yet, + * then the `index_oid` will be set, but the `url` will be NULL. */ struct git_submodule { - git_repository *owner; + git_refcount rc; + + /* information from config */ char *name; - char *path; /* important: may point to same string data as "name" */ + char *path; /* important: may just point to "name" string */ char *url; - uint32_t flags; - git_oid head_oid; - git_oid index_oid; - git_oid wd_oid; - /* information from config */ git_submodule_update_t update; git_submodule_update_t update_default; git_submodule_ignore_t ignore; git_submodule_ignore_t ignore_default; int fetch_recurse; + /* internal information */ - int refcount; + git_repository *repo; + uint32_t flags; + git_oid head_oid; + git_oid index_oid; + git_oid wd_oid; }; /* Additional flags on top of public GIT_SUBMODULE_STATUS values */ @@ -99,4 +110,29 @@ enum { #define GIT_SUBMODULE_STATUS__CLEAR_INTERNAL(S) \ ((S) & ~(0xFFFFFFFFu << 20)) +/* Internal status fn returns status and optionally the various OIDs */ +extern int git_submodule__status( + unsigned int *out_status, + git_oid *out_head_id, + git_oid *out_index_id, + git_oid *out_wd_id, + git_submodule *sm, + git_submodule_ignore_t ign); + +/* Open submodule repository as bare repo for quick HEAD check, etc. */ +extern int git_submodule_open_bare( + git_repository **repo, + git_submodule *submodule); + +/* Release reference to submodule object - not currently for external use */ +extern void git_submodule_free(git_submodule *sm); + +extern int git_submodule_parse_ignore( + git_submodule_ignore_t *out, const char *value); +extern int git_submodule_parse_update( + git_submodule_update_t *out, const char *value); + +extern const char *git_submodule_ignore_to_str(git_submodule_ignore_t); +extern const char *git_submodule_update_to_str(git_submodule_update_t); + #endif @@ -13,20 +13,17 @@ #include "git2/object.h" #include "git2/repository.h" #include "git2/signature.h" +#include "git2/odb_backend.h" -void git_tag__free(git_tag *tag) +void git_tag__free(void *_tag) { + git_tag *tag = _tag; git_signature_free(tag->tagger); git__free(tag->message); git__free(tag->tag_name); git__free(tag); } -const git_oid *git_tag_id(const git_tag *c) -{ - return git_object_id((const git_object *)c); -} - int git_tag_target(git_object **target, const git_tag *t) { assert(t); @@ -68,7 +65,7 @@ static int tag_error(const char *str) return -1; } -int git_tag__parse_buffer(git_tag *tag, const char *buffer, size_t length) +static int tag_parse(git_tag *tag, const char *buffer, const char *buffer_end) { static const char *tag_types[] = { NULL, "commit\n", "tree\n", "blob\n", "tag\n" @@ -78,8 +75,6 @@ int git_tag__parse_buffer(git_tag *tag, const char *buffer, size_t length) size_t text_len; char *search; - const char *buffer_end = buffer + length; - if (git_oid__parse(&tag->target, &buffer, buffer_end, "object ") < 0) return tag_error("Object field invalid"); @@ -156,6 +151,15 @@ int git_tag__parse_buffer(git_tag *tag, const char *buffer, size_t length) return 0; } +int git_tag__parse(void *_tag, git_odb_object *odb_obj) +{ + git_tag *tag = _tag; + const char *buffer = git_odb_object_data(odb_obj); + const char *buffer_end = buffer + git_odb_object_size(odb_obj); + + return tag_parse(tag, buffer, buffer_end); +} + static int retrieve_tag_reference( git_reference **tag_reference_out, git_buf *ref_name_out, @@ -276,23 +280,36 @@ cleanup: } int git_tag_create( - git_oid *oid, - git_repository *repo, - const char *tag_name, - const git_object *target, - const git_signature *tagger, - const char *message, - int allow_ref_overwrite) + git_oid *oid, + git_repository *repo, + const char *tag_name, + const git_object *target, + const git_signature *tagger, + const char *message, + int allow_ref_overwrite) { return git_tag_create__internal(oid, repo, tag_name, target, tagger, message, allow_ref_overwrite, 1); } +int git_tag_annotation_create( + git_oid *oid, + git_repository *repo, + const char *tag_name, + const git_object *target, + const git_signature *tagger, + const char *message) +{ + assert(oid && repo && tag_name && target && tagger && message); + + return write_tag_annotation(oid, repo, tag_name, target, tagger, message); +} + int git_tag_create_lightweight( - git_oid *oid, - git_repository *repo, - const char *tag_name, - const git_object *target, - int allow_ref_overwrite) + git_oid *oid, + git_repository *repo, + const char *tag_name, + const git_object *target, + int allow_ref_overwrite) { return git_tag_create__internal(oid, repo, tag_name, target, NULL, NULL, allow_ref_overwrite, 0); } @@ -316,14 +333,14 @@ int git_tag_create_frombuffer(git_oid *oid, git_repository *repo, const char *bu return -1; /* validate the buffer */ - if (git_tag__parse_buffer(&tag, buffer, strlen(buffer)) < 0) + if (tag_parse(&tag, buffer, buffer + strlen(buffer)) < 0) return -1; /* validate the target */ if (git_odb_read(&target_obj, odb, &tag.target) < 0) goto on_error; - if (tag.type != target_obj->raw.type) { + if (tag.type != target_obj->cached.type) { giterr_set(GITERR_TAG, "The type for the given target is invalid"); goto on_error; } @@ -389,14 +406,8 @@ int git_tag_delete(git_repository *repo, const char *tag_name) if ((error = git_reference_delete(tag_ref)) == 0) git_reference_free(tag_ref); - - return error; -} -int git_tag__parse(git_tag *tag, git_odb_object *obj) -{ - assert(tag); - return git_tag__parse_buffer(tag, obj->raw.data, obj->raw.len); + return error; } typedef struct { @@ -429,7 +440,7 @@ int git_tag_foreach(git_repository *repo, git_tag_foreach_cb cb, void *cb_data) data.cb_data = cb_data; data.repo = repo; - return git_reference_foreach(repo, GIT_REF_OID, &tags_cb, &data); + return git_reference_foreach_name(repo, &tags_cb, &data); } typedef struct { @@ -22,8 +22,7 @@ struct git_tag { char *message; }; -void git_tag__free(git_tag *tag); -int git_tag__parse(git_tag *tag, git_odb_object *obj); -int git_tag__parse_buffer(git_tag *tag, const char *data, size_t len); +void git_tag__free(void *tag); +int git_tag__parse(void *tag, git_odb_object *obj); #endif diff --git a/src/thread-utils.h b/src/thread-utils.h index 2ca290adf..04e02959f 100644 --- a/src/thread-utils.h +++ b/src/thread-utils.h @@ -7,8 +7,6 @@ #ifndef INCLUDE_thread_utils_h__ #define INCLUDE_thread_utils_h__ -#include "common.h" - /* Common operations even if threading has been disabled */ typedef struct { #if defined(GIT_WIN32) @@ -18,10 +16,27 @@ typedef struct { #endif } git_atomic; -GIT_INLINE(void) git_atomic_set(git_atomic *a, int val) -{ - a->val = val; -} +#ifdef GIT_ARCH_64 + +typedef struct { +#if defined(GIT_WIN32) + __int64 val; +#else + int64_t val; +#endif +} git_atomic64; + +typedef git_atomic64 git_atomic_ssize; + +#define git_atomic_ssize_add git_atomic64_add + +#else + +typedef git_atomic git_atomic_ssize; + +#define git_atomic_ssize_add git_atomic_add + +#endif #ifdef GIT_THREADS @@ -46,6 +61,17 @@ GIT_INLINE(void) git_atomic_set(git_atomic *a, int val) #define git_cond_signal(c) pthread_cond_signal(c) #define git_cond_broadcast(c) pthread_cond_broadcast(c) +GIT_INLINE(void) git_atomic_set(git_atomic *a, int val) +{ +#if defined(GIT_WIN32) + InterlockedExchange(&a->val, (LONG)val); +#elif defined(__GNUC__) + __sync_lock_test_and_set(&a->val, val); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + GIT_INLINE(int) git_atomic_inc(git_atomic *a) { #if defined(GIT_WIN32) @@ -57,6 +83,17 @@ GIT_INLINE(int) git_atomic_inc(git_atomic *a) #endif } +GIT_INLINE(int) git_atomic_add(git_atomic *a, int32_t addend) +{ +#if defined(GIT_WIN32) + return InterlockedExchangeAdd(&a->val, addend); +#elif defined(__GNUC__) + return __sync_add_and_fetch(&a->val, addend); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + GIT_INLINE(int) git_atomic_dec(git_atomic *a) { #if defined(GIT_WIN32) @@ -68,6 +105,45 @@ GIT_INLINE(int) git_atomic_dec(git_atomic *a) #endif } +GIT_INLINE(void *) git___compare_and_swap( + void * volatile *ptr, void *oldval, void *newval) +{ + volatile void *foundval; +#if defined(GIT_WIN32) + foundval = InterlockedCompareExchangePointer((volatile PVOID *)ptr, newval, oldval); +#elif defined(__GNUC__) + foundval = __sync_val_compare_and_swap(ptr, oldval, newval); +#else +# error "Unsupported architecture for atomic operations" +#endif + return (foundval == oldval) ? oldval : newval; +} + +GIT_INLINE(volatile void *) git___swap( + void * volatile *ptr, void *newval) +{ +#if defined(GIT_WIN32) + return InterlockedExchangePointer(ptr, newval); +#else + return __sync_lock_test_and_set(ptr, newval); +#endif +} + +#ifdef GIT_ARCH_64 + +GIT_INLINE(int64_t) git_atomic64_add(git_atomic64 *a, int64_t addend) +{ +#if defined(GIT_WIN32) + return InterlockedExchangeAdd64(&a->val, addend); +#elif defined(__GNUC__) + return __sync_add_and_fetch(&a->val, addend); +#else +# error "Unsupported architecture for atomic operations" +#endif +} + +#endif + #else #define git_thread unsigned int @@ -78,7 +154,7 @@ GIT_INLINE(int) git_atomic_dec(git_atomic *a) /* Pthreads Mutex */ #define git_mutex unsigned int -#define git_mutex_init(a) (void)0 +#define git_mutex_init(a) 0 #define git_mutex_lock(a) 0 #define git_mutex_unlock(a) (void)0 #define git_mutex_free(a) (void)0 @@ -91,18 +167,78 @@ GIT_INLINE(int) git_atomic_dec(git_atomic *a) #define git_cond_signal(c) (void)0 #define git_cond_broadcast(c) (void)0 +GIT_INLINE(void) git_atomic_set(git_atomic *a, int val) +{ + a->val = val; +} + GIT_INLINE(int) git_atomic_inc(git_atomic *a) { return ++a->val; } +GIT_INLINE(int) git_atomic_add(git_atomic *a, int32_t addend) +{ + a->val += addend; + return a->val; +} + GIT_INLINE(int) git_atomic_dec(git_atomic *a) { return --a->val; } +GIT_INLINE(void *) git___compare_and_swap( + void * volatile *ptr, void *oldval, void *newval) +{ + if (*ptr == oldval) + *ptr = newval; + else + oldval = newval; + return oldval; +} + +GIT_INLINE(volatile void *) git___swap( + void * volatile *ptr, void *newval) +{ + volatile void *old = *ptr; + *ptr = newval; + return old; +} + +#ifdef GIT_ARCH_64 + +GIT_INLINE(int64_t) git_atomic64_add(git_atomic64 *a, int64_t addend) +{ + a->val += addend; + return a->val; +} + #endif +#endif + +GIT_INLINE(int) git_atomic_get(git_atomic *a) +{ + return (int)a->val; +} + +/* Atomically replace oldval with newval + * @return oldval if it was replaced or newval if it was not + */ +#define git__compare_and_swap(P,O,N) \ + git___compare_and_swap((void * volatile *)P, O, N) + +#define git__swap(ptr, val) (void *)git___swap((void * volatile *)&ptr, val) + extern int git_online_cpus(void); +#if defined(GIT_THREADS) && defined(GIT_WIN32) +# define GIT_MEMORY_BARRIER MemoryBarrier() +#elif defined(GIT_THREADS) +# define GIT_MEMORY_BARRIER __sync_synchronize() +#else +# define GIT_MEMORY_BARRIER /* noop */ +#endif + #endif /* INCLUDE_thread_utils_h__ */ diff --git a/src/trace.c b/src/trace.c index 159ac91cc..ee5039f56 100644 --- a/src/trace.c +++ b/src/trace.c @@ -25,7 +25,7 @@ int git_trace_set(git_trace_level_t level, git_trace_callback callback) git_trace__data.level = level; git_trace__data.callback = callback; GIT_MEMORY_BARRIER; - + return 0; #else GIT_UNUSED(level); @@ -36,4 +36,3 @@ int git_trace_set(git_trace_level_t level, git_trace_callback callback) return -1; #endif } - diff --git a/src/trace.h b/src/trace.h index f4bdff88a..77b1e03ef 100644 --- a/src/trace.h +++ b/src/trace.h @@ -25,14 +25,14 @@ GIT_INLINE(void) git_trace__write_fmt( git_trace_level_t level, const char *fmt, ...) { - git_trace_callback callback = git_trace__data.callback; + git_trace_callback callback = git_trace__data.callback; git_buf message = GIT_BUF_INIT; va_list ap; - + va_start(ap, fmt); git_buf_vprintf(&message, fmt, ap); va_end(ap); - + callback(level, git_buf_cstr(&message)); git_buf_free(&message); diff --git a/src/transport.c b/src/transport.c index adb6d5355..354789db1 100644 --- a/src/transport.c +++ b/src/transport.c @@ -18,19 +18,27 @@ typedef struct transport_definition { void *param; } transport_definition; -static transport_definition local_transport_definition = { "file://", 1, git_transport_local, NULL }; -static transport_definition dummy_transport_definition = { NULL, 1, git_transport_dummy, NULL }; - static git_smart_subtransport_definition http_subtransport_definition = { git_smart_subtransport_http, 1 }; static git_smart_subtransport_definition git_subtransport_definition = { git_smart_subtransport_git, 0 }; +#ifdef GIT_SSH +static git_smart_subtransport_definition ssh_subtransport_definition = { git_smart_subtransport_ssh, 0 }; +#endif + +static transport_definition local_transport_definition = { "file://", 1, git_transport_local, NULL }; +#ifdef GIT_SSH +static transport_definition ssh_transport_definition = { "ssh://", 1, git_transport_smart, &ssh_subtransport_definition }; +#else +static transport_definition dummy_transport_definition = { NULL, 1, git_transport_dummy, NULL }; +#endif static transport_definition transports[] = { {"git://", 1, git_transport_smart, &git_subtransport_definition}, {"http://", 1, git_transport_smart, &http_subtransport_definition}, {"https://", 1, git_transport_smart, &http_subtransport_definition}, {"file://", 1, git_transport_local, NULL}, - {"git+ssh://", 1, git_transport_dummy, NULL}, - {"ssh+git://", 1, git_transport_dummy, NULL}, +#ifdef GIT_SSH + {"ssh://", 1, git_transport_smart, &ssh_subtransport_definition}, +#endif {NULL, 0, 0} }; @@ -65,7 +73,7 @@ static int transport_find_fn(const char *url, git_transport_cb *callback, void * /* It could be a SSH remote path. Check to see if there's a : * SSH is an unsupported transport mechanism in this version of libgit2 */ if (!definition && strrchr(url, ':')) - definition = &dummy_transport_definition; + definition = &dummy_transport_definition; #else /* For other systems, perform the SSH check first, to avoid going to the * filesystem if it is not necessary */ @@ -73,7 +81,11 @@ static int transport_find_fn(const char *url, git_transport_cb *callback, void * /* It could be a SSH remote path. Check to see if there's a : * SSH is an unsupported transport mechanism in this version of libgit2 */ if (!definition && strrchr(url, ':')) - definition = &dummy_transport_definition; +#ifdef GIT_SSH + definition = &ssh_transport_definition; +#else + definition = &dummy_transport_definition; +#endif /* Check to see if the path points to a file on the local file system */ if (!definition && git_path_exists(url) && git_path_isdir(url)) @@ -85,7 +97,7 @@ static int transport_find_fn(const char *url, git_transport_cb *callback, void * *callback = definition->fn; *param = definition->param; - + return 0; } diff --git a/src/transports/cred.c b/src/transports/cred.c index ecb026062..a6727e902 100644 --- a/src/transports/cred.c +++ b/src/transports/cred.c @@ -12,16 +12,17 @@ static void plaintext_free(struct git_cred *cred) { git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred; - size_t pass_len = strlen(c->password); git__free(c->username); /* Zero the memory which previously held the password */ - memset(c->password, 0x0, pass_len); - git__free(c->password); - - memset(c, 0, sizeof(*c)); + if (c->password) { + size_t pass_len = strlen(c->password); + git__memzero(c->password, pass_len); + git__free(c->password); + } + git__memzero(c, sizeof(*c)); git__free(c); } @@ -32,8 +33,7 @@ int git_cred_userpass_plaintext_new( { git_cred_userpass_plaintext *c; - if (!cred) - return -1; + assert(cred); c = git__malloc(sizeof(git_cred_userpass_plaintext)); GITERR_CHECK_ALLOC(c); @@ -58,3 +58,97 @@ int git_cred_userpass_plaintext_new( *cred = &c->parent; return 0; } + +static void ssh_keyfile_passphrase_free(struct git_cred *cred) +{ + git_cred_ssh_keyfile_passphrase *c = + (git_cred_ssh_keyfile_passphrase *)cred; + + git__free(c->publickey); + git__free(c->privatekey); + + if (c->passphrase) { + /* Zero the memory which previously held the passphrase */ + size_t pass_len = strlen(c->passphrase); + git__memzero(c->passphrase, pass_len); + git__free(c->passphrase); + } + + git__memzero(c, sizeof(*c)); + git__free(c); +} + +static void ssh_publickey_free(struct git_cred *cred) +{ + git_cred_ssh_publickey *c = (git_cred_ssh_publickey *)cred; + + git__free(c->publickey); + + git__memzero(c, sizeof(*c)); + git__free(c); +} + +int git_cred_ssh_keyfile_passphrase_new( + git_cred **cred, + const char *publickey, + const char *privatekey, + const char *passphrase) +{ + git_cred_ssh_keyfile_passphrase *c; + + assert(cred && privatekey); + + c = git__calloc(1, sizeof(git_cred_ssh_keyfile_passphrase)); + GITERR_CHECK_ALLOC(c); + + c->parent.credtype = GIT_CREDTYPE_SSH_KEYFILE_PASSPHRASE; + c->parent.free = ssh_keyfile_passphrase_free; + + c->privatekey = git__strdup(privatekey); + GITERR_CHECK_ALLOC(c->privatekey); + + if (publickey) { + c->publickey = git__strdup(publickey); + GITERR_CHECK_ALLOC(c->publickey); + } + + if (passphrase) { + c->passphrase = git__strdup(passphrase); + GITERR_CHECK_ALLOC(c->passphrase); + } + + *cred = &c->parent; + return 0; +} + +int git_cred_ssh_publickey_new( + git_cred **cred, + const char *publickey, + size_t publickey_len, + git_cred_sign_callback sign_callback, + void *sign_data) +{ + git_cred_ssh_publickey *c; + + assert(cred); + + c = git__calloc(1, sizeof(git_cred_ssh_publickey)); + GITERR_CHECK_ALLOC(c); + + c->parent.credtype = GIT_CREDTYPE_SSH_PUBLICKEY; + c->parent.free = ssh_publickey_free; + + if (publickey_len > 0) { + c->publickey = git__malloc(publickey_len); + GITERR_CHECK_ALLOC(c->publickey); + + memcpy(c->publickey, publickey, publickey_len); + } + + c->publickey_len = publickey_len; + c->sign_callback = sign_callback; + c->sign_data = sign_data; + + *cred = &c->parent; + return 0; +} diff --git a/src/transports/local.c b/src/transports/local.c index ce89bb213..a9da8146c 100644 --- a/src/transports/local.c +++ b/src/transports/local.c @@ -36,7 +36,8 @@ typedef struct { git_atomic cancelled; git_repository *repo; git_vector refs; - unsigned connected : 1; + unsigned connected : 1, + have_refs : 1; } transport_local; static int add_ref(transport_local *t, const char *name) @@ -118,15 +119,24 @@ on_error: static int store_refs(transport_local *t) { - unsigned int i; + size_t i; + git_remote_head *head; git_strarray ref_names = {0}; assert(t); - if (git_reference_list(&ref_names, t->repo, GIT_REF_LISTALL) < 0 || - git_vector_init(&t->refs, ref_names.count, NULL) < 0) + if (git_reference_list(&ref_names, t->repo) < 0) goto on_error; + /* Clear all heads we might have fetched in a previous connect */ + git_vector_foreach(&t->refs, i, head) { + git__free(head->name); + git__free(head); + } + + /* Clear the vector so we can reuse it */ + git_vector_clear(&t->refs); + /* Sort the references first */ git__tsort((void **)ref_names.strings, ref_names.count, &git__strcmp_cb); @@ -139,6 +149,7 @@ static int store_refs(transport_local *t) goto on_error; } + t->have_refs = 1; git_strarray_free(&ref_names); return 0; @@ -208,8 +219,8 @@ static int local_ls(git_transport *transport, git_headlist_cb list_cb, void *pay unsigned int i; git_remote_head *head = NULL; - if (!t->connected) { - giterr_set(GITERR_NET, "The transport is not connected"); + if (!t->have_refs) { + giterr_set(GITERR_NET, "The transport has not yet loaded the refs"); return -1; } @@ -280,7 +291,7 @@ static int local_push_copy_object( odb_obj_size) < 0 || odb_stream->finalize_write(&remote_odb_obj_oid, odb_stream) < 0) { error = -1; - } else if (git_oid_cmp(&obj->id, &remote_odb_obj_oid) != 0) { + } else if (git_oid__cmp(&obj->id, &remote_odb_obj_oid) != 0) { giterr_set(GITERR_ODB, "Error when writing object to remote odb " "during local push operation. Remote odb object oid does not " "match local oid."); @@ -346,7 +357,7 @@ static int local_push( if ((error = git_repository_open(&remote_repo, push->remote->url)) < 0) return error; - /* We don't currently support pushing locally to non-bare repos. Proper + /* We don't currently support pushing locally to non-bare repos. Proper non-bare repo push support would require checking configs to see if we should override the default 'don't let this happen' behavior */ if (!remote_repo->is_bare) { @@ -493,7 +504,7 @@ static int local_download_pack( /* Tag or some other wanted object. Add it on its own */ error = git_packbuilder_insert(pack, &rhead->oid, rhead->name); } - git_object_free(obj); + git_object_free(obj); } /* Walk the objects, building a packfile */ @@ -569,8 +580,6 @@ static void local_cancel(git_transport *transport) static int local_close(git_transport *transport) { transport_local *t = (transport_local *)transport; - size_t i; - git_remote_head *head; t->connected = 0; @@ -579,13 +588,6 @@ static int local_close(git_transport *transport) t->repo = NULL; } - git_vector_foreach(&t->refs, i, head) { - git__free(head->name); - git__free(head); - } - - git_vector_free(&t->refs); - if (t->url) { git__free(t->url); t->url = NULL; @@ -597,6 +599,15 @@ static int local_close(git_transport *transport) static void local_free(git_transport *transport) { transport_local *t = (transport_local *)transport; + size_t i; + git_remote_head *head; + + git_vector_foreach(&t->refs, i, head) { + git__free(head->name); + git__free(head); + } + + git_vector_free(&t->refs); /* Close the transport, if it's still open. */ local_close(transport); @@ -630,6 +641,7 @@ int git_transport_local(git_transport **out, git_remote *owner, void *param) t->parent.read_flags = local_read_flags; t->parent.cancel = local_cancel; + git_vector_init(&t->refs, 0, NULL); t->owner = owner; *out = (git_transport *) t; diff --git a/src/transports/smart.c b/src/transports/smart.c index bfcce0c08..416eb221f 100644 --- a/src/transports/smart.c +++ b/src/transports/smart.c @@ -253,7 +253,6 @@ static int git_smart__read_flags(git_transport *transport, int *flags) static int git_smart__close(git_transport *transport) { transport_smart *t = (transport_smart *)transport; - git_vector *refs = &t->refs; git_vector *common = &t->common; unsigned int i; git_pkt *p; @@ -261,11 +260,6 @@ static int git_smart__close(git_transport *transport) ret = git_smart__reset_stream(t, true); - git_vector_foreach(refs, i, p) - git_pkt_free(p); - - git_vector_free(refs); - git_vector_foreach(common, i, p) git_pkt_free(p); @@ -284,6 +278,9 @@ static int git_smart__close(git_transport *transport) static void git_smart__free(git_transport *transport) { transport_smart *t = (transport_smart *)transport; + git_vector *refs = &t->refs; + unsigned int i; + git_pkt *p; /* Make sure that the current stream is closed, if we have one. */ git_smart__close(transport); @@ -291,6 +288,11 @@ static void git_smart__free(git_transport *transport) /* Free the subtransport */ t->wrapped->free(t->wrapped); + git_vector_foreach(refs, i, p) + git_pkt_free(p); + + git_vector_free(refs); + git__free(t); } diff --git a/src/transports/smart_protocol.c b/src/transports/smart_protocol.c index 8acedeb49..0cd5e831d 100644 --- a/src/transports/smart_protocol.c +++ b/src/transports/smart_protocol.c @@ -5,6 +5,7 @@ * a Linking Exception. For full terms see the included COPYING file. */ #include "git2.h" +#include "git2/odb_backend.h" #include "smart.h" #include "refs.h" @@ -20,12 +21,18 @@ int git_smart__store_refs(transport_smart *t, int flushes) gitno_buffer *buf = &t->buffer; git_vector *refs = &t->refs; int error, flush = 0, recvd; - const char *line_end; - git_pkt *pkt; + const char *line_end = NULL; + git_pkt *pkt = NULL; + git_pkt_ref *ref; + size_t i; /* Clear existing refs in case git_remote_connect() is called again * after git_remote_disconnect(). */ + git_vector_foreach(refs, i, ref) { + git__free(ref->head.name); + git__free(ref); + } git_vector_clear(refs); do { @@ -128,7 +135,7 @@ int git_smart__detect_caps(git_pkt_ref *pkt, transport_smart_caps *caps) static int recv_pkt(git_pkt **out, gitno_buffer *buf) { const char *ptr = buf->data, *line_end = ptr; - git_pkt *pkt; + git_pkt *pkt = NULL; int pkt_type, error = 0, ret; do { @@ -186,7 +193,7 @@ static int fetch_setup_walk(git_revwalk **out, git_repository *repo) unsigned int i; git_reference *ref; - if (git_reference_list(&refs, repo, GIT_REF_LISTALL) < 0) + if (git_reference_list(&refs, repo) < 0) return -1; if (git_revwalk_new(&walk, repo) < 0) @@ -365,7 +372,7 @@ int git_smart__negotiate_fetch(git_transport *transport, git_repository *repo, c return error; if (pkt->type == GIT_PKT_NAK || - (pkt->type == GIT_PKT_ACK && pkt->status != GIT_ACK_CONTINUE)) { + (pkt->type == GIT_PKT_ACK && pkt->status != GIT_ACK_CONTINUE)) { git__free(pkt); break; } @@ -569,7 +576,7 @@ static int add_push_report_pkt(git_push *push, git_pkt *pkt) switch (pkt->type) { case GIT_PKT_OK: - status = git__malloc(sizeof(push_status)); + status = git__calloc(1, sizeof(push_status)); GITERR_CHECK_ALLOC(status); status->msg = NULL; status->ref = git__strdup(((git_pkt_ok *)pkt)->ref); @@ -633,8 +640,8 @@ static int add_push_report_sideband_pkt(git_push *push, git_pkt_data *data_pkt) static int parse_report(gitno_buffer *buf, git_push *push) { - git_pkt *pkt; - const char *line_end; + git_pkt *pkt = NULL; + const char *line_end = NULL; int error, recvd; for (;;) { @@ -806,13 +813,13 @@ int git_smart__push(git_transport *transport, git_push *push) transport_smart *t = (transport_smart *)transport; git_smart_subtransport_stream *s; git_buf pktline = GIT_BUF_INIT; - int error = -1; + int error = -1, need_pack = 0; + push_spec *spec; + unsigned int i; #ifdef PUSH_DEBUG { git_remote_head *head; - push_spec *spec; - unsigned int i; char hex[41]; hex[40] = '\0'; git_vector_foreach(&push->remote->refs, i, head) { @@ -830,10 +837,23 @@ int git_smart__push(git_transport *transport, git_push *push) } #endif + /* + * Figure out if we need to send a packfile; which is in all + * cases except when we only send delete commands + */ + git_vector_foreach(&push->specs, i, spec) { + if (spec->lref) { + need_pack = 1; + break; + } + } + if (git_smart__get_push_stream(t, &s) < 0 || gen_pktline(&pktline, push) < 0 || - s->write(s, git_buf_cstr(&pktline), git_buf_len(&pktline)) < 0 || - git_packbuilder_foreach(push->pb, &stream_thunk, s) < 0) + s->write(s, git_buf_cstr(&pktline), git_buf_len(&pktline)) < 0) + goto on_error; + + if (need_pack && git_packbuilder_foreach(push->pb, &stream_thunk, s) < 0) goto on_error; /* If we sent nothing or the server doesn't support report-status, then diff --git a/src/transports/ssh.c b/src/transports/ssh.c new file mode 100644 index 000000000..7fb53bc3c --- /dev/null +++ b/src/transports/ssh.c @@ -0,0 +1,531 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "git2.h" +#include "buffer.h" +#include "netops.h" +#include "smart.h" + +#ifdef GIT_SSH + +#include <libssh2.h> + +#define OWNING_SUBTRANSPORT(s) ((ssh_subtransport *)(s)->parent.subtransport) + +static const char prefix_ssh[] = "ssh://"; +static const char default_user[] = "git"; +static const char cmd_uploadpack[] = "git-upload-pack"; +static const char cmd_receivepack[] = "git-receive-pack"; + +typedef struct { + git_smart_subtransport_stream parent; + gitno_socket socket; + LIBSSH2_SESSION *session; + LIBSSH2_CHANNEL *channel; + const char *cmd; + char *url; + unsigned sent_command : 1; +} ssh_stream; + +typedef struct { + git_smart_subtransport parent; + transport_smart *owner; + ssh_stream *current_stream; + git_cred *cred; +} ssh_subtransport; + +/* + * Create a git protocol request. + * + * For example: git-upload-pack '/libgit2/libgit2' + */ +static int gen_proto(git_buf *request, const char *cmd, const char *url) +{ + char *repo; + + if (!git__prefixcmp(url, prefix_ssh)) { + url = url + strlen(prefix_ssh); + repo = strchr(url, '/'); + } else { + repo = strchr(url, ':'); + } + + if (!repo) { + giterr_set(GITERR_NET, "Malformed git protocol URL"); + return -1; + } + + int len = strlen(cmd) + 1 /* Space */ + 1 /* Quote */ + strlen(repo) + 1 /* Quote */ + 1; + + git_buf_grow(request, len); + git_buf_printf(request, "%s '%s'", cmd, repo); + git_buf_putc(request, '\0'); + + if (git_buf_oom(request)) + return -1; + + return 0; +} + +static int send_command(ssh_stream *s) +{ + int error; + git_buf request = GIT_BUF_INIT; + + error = gen_proto(&request, s->cmd, s->url); + if (error < 0) + goto cleanup; + + error = libssh2_channel_exec(s->channel, request.ptr); + if (error < 0) { + giterr_set(GITERR_NET, "SSH could not execute request"); + goto cleanup; + } + + s->sent_command = 1; + +cleanup: + git_buf_free(&request); + return error; +} + +static int ssh_stream_read( + git_smart_subtransport_stream *stream, + char *buffer, + size_t buf_size, + size_t *bytes_read) +{ + int rc; + ssh_stream *s = (ssh_stream *)stream; + + *bytes_read = 0; + + if (!s->sent_command && send_command(s) < 0) + return -1; + + if ((rc = libssh2_channel_read(s->channel, buffer, buf_size)) < 0) { + giterr_set(GITERR_NET, "SSH could not read data"); + return -1; + } + + *bytes_read = rc; + + return 0; +} + +static int ssh_stream_write( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) +{ + ssh_stream *s = (ssh_stream *)stream; + + if (!s->sent_command && send_command(s) < 0) + return -1; + + if (libssh2_channel_write(s->channel, buffer, len) < 0) { + giterr_set(GITERR_NET, "SSH could not write data"); + return -1; + } + + return 0; +} + +static void ssh_stream_free(git_smart_subtransport_stream *stream) +{ + ssh_stream *s = (ssh_stream *)stream; + ssh_subtransport *t = OWNING_SUBTRANSPORT(s); + int ret; + + GIT_UNUSED(ret); + + t->current_stream = NULL; + + if (s->channel) { + libssh2_channel_close(s->channel); + libssh2_channel_free(s->channel); + s->channel = NULL; + } + + if (s->session) { + libssh2_session_free(s->session); + s->session = NULL; + } + + if (s->socket.socket) { + (void)gitno_close(&s->socket); + /* can't do anything here with error return value */ + } + + git__free(s->url); + git__free(s); +} + +static int ssh_stream_alloc( + ssh_subtransport *t, + const char *url, + const char *cmd, + git_smart_subtransport_stream **stream) +{ + ssh_stream *s; + + assert(stream); + + s = git__calloc(sizeof(ssh_stream), 1); + GITERR_CHECK_ALLOC(s); + + s->parent.subtransport = &t->parent; + s->parent.read = ssh_stream_read; + s->parent.write = ssh_stream_write; + s->parent.free = ssh_stream_free; + + s->cmd = cmd; + + s->url = git__strdup(url); + if (!s->url) { + git__free(s); + return -1; + } + + *stream = &s->parent; + return 0; +} + +static int git_ssh_extract_url_parts( + char **host, + char **username, + const char *url) +{ + char *colon, *at; + const char *start; + + colon = strchr(url, ':'); + + if (colon == NULL) { + giterr_set(GITERR_NET, "Malformed URL: missing :"); + return -1; + } + + at = strchr(url, '@'); + if (at) { + start = at+1; + *username = git__substrdup(url, at - url); + } else { + start = url; + *username = git__strdup(default_user); + } + GITERR_CHECK_ALLOC(*username); + + *host = git__substrdup(start, colon - start); + GITERR_CHECK_ALLOC(*host); + + return 0; +} + +static int _git_ssh_authenticate_session( + LIBSSH2_SESSION* session, + const char *user, + git_cred* cred) +{ + int rc; + + do { + switch (cred->credtype) { + case GIT_CREDTYPE_USERPASS_PLAINTEXT: { + git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred; + rc = libssh2_userauth_password(session, c->username, c->password); + break; + } + case GIT_CREDTYPE_SSH_KEYFILE_PASSPHRASE: { + git_cred_ssh_keyfile_passphrase *c = (git_cred_ssh_keyfile_passphrase *)cred; + rc = libssh2_userauth_publickey_fromfile( + session, user, c->publickey, c->privatekey, c->passphrase); + break; + } + case GIT_CREDTYPE_SSH_PUBLICKEY: { + git_cred_ssh_publickey *c = (git_cred_ssh_publickey *)cred; + rc = libssh2_userauth_publickey( + session, user, (const unsigned char *)c->publickey, + c->publickey_len, c->sign_callback, &c->sign_data); + break; + } + default: + rc = LIBSSH2_ERROR_AUTHENTICATION_FAILED; + } + } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); + + if (rc != 0) { + giterr_set(GITERR_NET, "Failed to authenticate SSH session"); + return -1; + } + + return 0; +} + +static int _git_ssh_session_create( + LIBSSH2_SESSION** session, + gitno_socket socket) +{ + int rc = 0; + LIBSSH2_SESSION* s; + + assert(session); + + s = libssh2_session_init(); + if (!s) { + giterr_set(GITERR_NET, "Failed to initialize SSH session"); + return -1; + } + + do { + rc = libssh2_session_startup(s, socket.socket); + } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); + + if (0 != rc) { + libssh2_session_free(s); + giterr_set(GITERR_NET, "Failed to start SSH session"); + return -1; + } + + libssh2_session_set_blocking(s, 1); + + *session = s; + + return 0; +} + +static int _git_ssh_setup_conn( + ssh_subtransport *t, + const char *url, + const char *cmd, + git_smart_subtransport_stream **stream) +{ + char *host, *port=NULL, *user=NULL, *pass=NULL; + const char *default_port="22"; + ssh_stream *s; + LIBSSH2_SESSION* session=NULL; + LIBSSH2_CHANNEL* channel=NULL; + + *stream = NULL; + if (ssh_stream_alloc(t, url, cmd, stream) < 0) + return -1; + + s = (ssh_stream *)*stream; + + if (!git__prefixcmp(url, prefix_ssh)) { + url = url + strlen(prefix_ssh); + if (gitno_extract_url_parts(&host, &port, &user, &pass, url, default_port) < 0) + goto on_error; + } else { + if (git_ssh_extract_url_parts(&host, &user, url) < 0) + goto on_error; + port = git__strdup(default_port); + GITERR_CHECK_ALLOC(port); + } + + if (gitno_connect(&s->socket, host, port, 0) < 0) + goto on_error; + + if (user && pass) { + if (git_cred_userpass_plaintext_new(&t->cred, user, pass) < 0) + goto on_error; + } else if (t->owner->cred_acquire_cb) { + if (t->owner->cred_acquire_cb( + &t->cred, t->owner->url, user, + GIT_CREDTYPE_USERPASS_PLAINTEXT | + GIT_CREDTYPE_SSH_KEYFILE_PASSPHRASE, + t->owner->cred_acquire_payload) < 0) + goto on_error; + + if (!t->cred) { + giterr_set(GITERR_NET, "Callback failed to initialize SSH credentials"); + goto on_error; + } + } else { + giterr_set(GITERR_NET, "Cannot set up SSH connection without credentials"); + goto on_error; + } + assert(t->cred); + + if (!user) { + user = git__strdup(default_user); + GITERR_CHECK_ALLOC(user); + } + + if (_git_ssh_session_create(&session, s->socket) < 0) + goto on_error; + + if (_git_ssh_authenticate_session(session, user, t->cred) < 0) + goto on_error; + + channel = libssh2_channel_open_session(session); + if (!channel) { + giterr_set(GITERR_NET, "Failed to open SSH channel"); + goto on_error; + } + + libssh2_channel_set_blocking(channel, 1); + + s->session = session; + s->channel = channel; + + t->current_stream = s; + git__free(host); + git__free(port); + git__free(user); + git__free(pass); + + return 0; + +on_error: + s->session = NULL; + s->channel = NULL; + t->current_stream = NULL; + + if (*stream) + ssh_stream_free(*stream); + + git__free(host); + git__free(port); + git__free(user); + git__free(pass); + + if (session) + libssh2_session_free(session); + + return -1; +} + +static int ssh_uploadpack_ls( + ssh_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + if (_git_ssh_setup_conn(t, url, cmd_uploadpack, stream) < 0) + return -1; + + return 0; +} + +static int ssh_uploadpack( + ssh_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + GIT_UNUSED(url); + + if (t->current_stream) { + *stream = &t->current_stream->parent; + return 0; + } + + giterr_set(GITERR_NET, "Must call UPLOADPACK_LS before UPLOADPACK"); + return -1; +} + +static int ssh_receivepack_ls( + ssh_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + if (_git_ssh_setup_conn(t, url, cmd_receivepack, stream) < 0) + return -1; + + return 0; +} + +static int ssh_receivepack( + ssh_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) +{ + GIT_UNUSED(url); + + if (t->current_stream) { + *stream = &t->current_stream->parent; + return 0; + } + + giterr_set(GITERR_NET, "Must call RECEIVEPACK_LS before RECEIVEPACK"); + return -1; +} + +static int _ssh_action( + git_smart_subtransport_stream **stream, + git_smart_subtransport *subtransport, + const char *url, + git_smart_service_t action) +{ + ssh_subtransport *t = (ssh_subtransport *) subtransport; + + switch (action) { + case GIT_SERVICE_UPLOADPACK_LS: + return ssh_uploadpack_ls(t, url, stream); + + case GIT_SERVICE_UPLOADPACK: + return ssh_uploadpack(t, url, stream); + + case GIT_SERVICE_RECEIVEPACK_LS: + return ssh_receivepack_ls(t, url, stream); + + case GIT_SERVICE_RECEIVEPACK: + return ssh_receivepack(t, url, stream); + } + + *stream = NULL; + return -1; +} + +static int _ssh_close(git_smart_subtransport *subtransport) +{ + ssh_subtransport *t = (ssh_subtransport *) subtransport; + + assert(!t->current_stream); + + GIT_UNUSED(t); + + return 0; +} + +static void _ssh_free(git_smart_subtransport *subtransport) +{ + ssh_subtransport *t = (ssh_subtransport *) subtransport; + + assert(!t->current_stream); + + git__free(t); +} +#endif + +int git_smart_subtransport_ssh( + git_smart_subtransport **out, git_transport *owner) +{ +#ifdef GIT_SSH + ssh_subtransport *t; + + assert(out); + + t = git__calloc(sizeof(ssh_subtransport), 1); + GITERR_CHECK_ALLOC(t); + + t->owner = (transport_smart *)owner; + t->parent.action = _ssh_action; + t->parent.close = _ssh_close; + t->parent.free = _ssh_free; + + *out = (git_smart_subtransport *) t; + return 0; +#else + GIT_UNUSED(owner); + + assert(out); + *out = NULL; + + giterr_set(GITERR_INVALID, "Cannot create SSH transport. Library was built without SSH support"); + return -1; +#endif +} diff --git a/src/transports/winhttp.c b/src/transports/winhttp.c index e502001cb..95e422dc0 100644 --- a/src/transports/winhttp.c +++ b/src/transports/winhttp.c @@ -19,6 +19,8 @@ #include <winhttp.h> #pragma comment(lib, "winhttp") +#include <strsafe.h> + /* For UuidCreate */ #pragma comment(lib, "rpcrt4") @@ -893,7 +895,7 @@ static int winhttp_connect( wchar_t *ua = L"git/1.0 (libgit2 " WIDEN(LIBGIT2_VERSION) L")"; wchar_t host[GIT_WIN_PATH]; int32_t port; - const char *default_port; + const char *default_port = "80"; int ret; if (!git__prefixcmp(url, prefix_http)) { diff --git a/src/tree.c b/src/tree.c index 17b3c378d..65d01b4d5 100644 --- a/src/tree.c +++ b/src/tree.c @@ -10,6 +10,9 @@ #include "tree.h" #include "git2/repository.h" #include "git2/object.h" +#include "path.h" +#include "tree-cache.h" +#include "index.h" #define DEFAULT_TREE_SIZE 16 #define MAX_FILEMODE_BYTES 6 @@ -149,7 +152,7 @@ static int tree_key_search( /* Initial homing search; find an entry on the tree with * the same prefix as the filename we're looking for */ if (git_vector_bsearch2(&homing, entries, &homing_search_cmp, &ksearch) < 0) - return GIT_ENOTFOUND; + return GIT_ENOTFOUND; /* just a signal error; not passed back to user */ /* We found a common prefix. Look forward as long as * there are entries that share the common prefix */ @@ -219,8 +222,9 @@ git_tree_entry *git_tree_entry_dup(const git_tree_entry *entry) return copy; } -void git_tree__free(git_tree *tree) +void git_tree__free(void *_tree) { + git_tree *tree = _tree; size_t i; git_tree_entry *e; @@ -231,16 +235,6 @@ void git_tree__free(git_tree *tree) git__free(tree); } -const git_oid *git_tree_id(const git_tree *t) -{ - return git_object_id((const git_object *)t); -} - -git_repository *git_tree_owner(const git_tree *t) -{ - return git_object_owner((const git_object *)t); -} - git_filemode_t git_tree_entry_filemode(const git_tree_entry *entry) { return (git_filemode_t)entry->attr; @@ -280,25 +274,27 @@ int git_tree_entry_to_object( } static const git_tree_entry *entry_fromname( - git_tree *tree, const char *name, size_t name_len) + const git_tree *tree, const char *name, size_t name_len) { size_t idx; - if (tree_key_search(&idx, &tree->entries, name, name_len) < 0) + assert(tree->entries.sorted); /* be safe when we cast away constness */ + + if (tree_key_search(&idx, (git_vector *)&tree->entries, name, name_len) < 0) return NULL; return git_vector_get(&tree->entries, idx); } const git_tree_entry *git_tree_entry_byname( - git_tree *tree, const char *filename) + const git_tree *tree, const char *filename) { assert(tree && filename); return entry_fromname(tree, filename, strlen(filename)); } const git_tree_entry *git_tree_entry_byindex( - git_tree *tree, size_t idx) + const git_tree *tree, size_t idx) { assert(tree); return git_vector_get(&tree->entries, idx); @@ -320,9 +316,9 @@ const git_tree_entry *git_tree_entry_byoid( return NULL; } -int git_tree__prefix_position(git_tree *tree, const char *path) +int git_tree__prefix_position(const git_tree *tree, const char *path) { - git_vector *entries = &tree->entries; + const git_vector *entries = &tree->entries; struct tree_key_search ksearch; size_t at_pos; @@ -332,8 +328,11 @@ int git_tree__prefix_position(git_tree *tree, const char *path) ksearch.filename = path; ksearch.filename_len = strlen(path); + assert(tree->entries.sorted); /* be safe when we cast away constness */ + /* Find tree entry with appropriate prefix */ - git_vector_bsearch2(&at_pos, entries, &homing_search_cmp, &ksearch); + git_vector_bsearch2( + &at_pos, (git_vector *)entries, &homing_search_cmp, &ksearch); for (; at_pos < entries->length; ++at_pos) { const git_tree_entry *entry = entries->contents[at_pos]; @@ -371,8 +370,12 @@ static int tree_error(const char *str, const char *path) return -1; } -static int tree_parse_buffer(git_tree *tree, const char *buffer, const char *buffer_end) +int git_tree__parse(void *_tree, git_odb_object *odb_obj) { + git_tree *tree = _tree; + const char *buffer = git_odb_object_data(odb_obj); + const char *buffer_end = buffer + git_odb_object_size(odb_obj); + if (git_vector_init(&tree->entries, DEFAULT_TREE_SIZE, entry_sort_cmp) < 0) return -1; @@ -413,13 +416,9 @@ static int tree_parse_buffer(git_tree *tree, const char *buffer, const char *buf buffer += GIT_OID_RAWSZ; } - return 0; -} + git_vector_sort(&tree->entries); -int git_tree__parse(git_tree *tree, git_odb_object *obj) -{ - assert(tree); - return tree_parse_buffer(tree, (char *)obj->raw.data, (char *)obj->raw.data + obj->raw.len); + return 0; } static size_t find_next_dir(const char *dirname, git_index *index, size_t start) @@ -525,7 +524,6 @@ static int write_tree( /* Write out the subtree */ written = write_tree(&sub_oid, repo, index, subdir, i); if (written < 0) { - tree_error("Failed to write subtree", subdir); git__free(subdir); goto on_error; } else { @@ -808,7 +806,7 @@ static size_t subpath_len(const char *path) int git_tree_entry_bypath( git_tree_entry **entry_out, - git_tree *root, + const git_tree *root, const char *path) { int error = 0; diff --git a/src/tree.h b/src/tree.h index b77bfd961..f07039a07 100644 --- a/src/tree.h +++ b/src/tree.h @@ -37,8 +37,8 @@ GIT_INLINE(bool) git_tree_entry__is_tree(const struct git_tree_entry *e) extern int git_tree_entry_icmp(const git_tree_entry *e1, const git_tree_entry *e2); -void git_tree__free(git_tree *tree); -int git_tree__parse(git_tree *tree, git_odb_object *obj); +void git_tree__free(void *tree); +int git_tree__parse(void *tree, git_odb_object *obj); /** * Lookup the first position in the tree with a given prefix. @@ -47,7 +47,7 @@ int git_tree__parse(git_tree *tree, git_odb_object *obj); * @param prefix the beginning of a path to find in the tree. * @return index of the first item at or after the given prefix. */ -int git_tree__prefix_position(git_tree *tree, const char *prefix); +int git_tree__prefix_position(const git_tree *tree, const char *prefix); /** diff --git a/src/unix/posix.h b/src/unix/posix.h index f4886c5d1..9c9f837b9 100644 --- a/src/unix/posix.h +++ b/src/unix/posix.h @@ -21,6 +21,8 @@ /* The OpenBSD realpath function behaves differently */ #if !defined(__OpenBSD__) # define p_realpath(p, po) realpath(p, po) +#else +char *p_realpath(const char *, char *); #endif #define p_vsnprintf(b, c, f, a) vsnprintf(b, c, f, a) diff --git a/src/unix/realpath.c b/src/unix/realpath.c index f382c2b73..15601bd22 100644 --- a/src/unix/realpath.c +++ b/src/unix/realpath.c @@ -22,7 +22,7 @@ char *p_realpath(const char *pathname, char *resolved) /* Figure out if the file exists */ if (!access(ret, F_OK)) - ret; + return ret; return NULL; } diff --git a/src/util.c b/src/util.c index 44ac1af73..ad7603829 100644 --- a/src/util.c +++ b/src/util.c @@ -11,6 +11,7 @@ #include <ctype.h> #include "posix.h" #include "fileops.h" +#include "cache.h" #ifdef _MSC_VER # include <Shlwapi.h> @@ -32,13 +33,15 @@ int git_libgit2_capabilities() #if defined(GIT_SSL) || defined(GIT_WINHTTP) | GIT_CAP_HTTPS #endif +#if defined(GIT_SSH) + | GIT_CAP_SSH +#endif ; } /* Declarations for tuneable settings */ extern size_t git_mwindow__window_size; extern size_t git_mwindow__mapped_limit; -extern size_t git_odb__cache_size; static int config_level_to_futils_dir(int config_level) { @@ -94,12 +97,25 @@ int git_libgit2_opts(int key, ...) error = git_futils_dirs_set(error, va_arg(ap, const char *)); break; - case GIT_OPT_GET_ODB_CACHE_SIZE: - *(va_arg(ap, size_t *)) = git_odb__cache_size; + case GIT_OPT_SET_CACHE_OBJECT_LIMIT: + { + git_otype type = (git_otype)va_arg(ap, int); + size_t size = va_arg(ap, size_t); + error = git_cache_set_max_object_size(type, size); + break; + } + + case GIT_OPT_SET_CACHE_MAX_SIZE: + git_cache__max_storage = va_arg(ap, ssize_t); + break; + + case GIT_OPT_ENABLE_CACHING: + git_cache__enabled = (va_arg(ap, int) != 0); break; - case GIT_OPT_SET_ODB_CACHE_SIZE: - git_odb__cache_size = va_arg(ap, size_t); + case GIT_OPT_GET_CACHED_MEMORY: + *(va_arg(ap, ssize_t *)) = git_cache__current_storage.val; + *(va_arg(ap, ssize_t *)) = git_cache__max_storage; break; } @@ -266,6 +282,28 @@ int git__strcasecmp(const char *a, const char *b) return (tolower(*a) - tolower(*b)); } +int git__strcasesort_cmp(const char *a, const char *b) +{ + int cmp = 0; + + while (*a && *b) { + if (*a != *b) { + if (tolower(*a) != tolower(*b)) + break; + /* use case in sort order even if not in equivalence */ + if (!cmp) + cmp = (int)(*(const uint8_t *)a) - (int)(*(const uint8_t *)b); + } + + ++a, ++b; + } + + if (*a || *b) + return tolower(*a) - tolower(*b); + + return cmp; +} + int git__strncmp(const char *a, const char *b, size_t sz) { while (sz && *a && *b && *a == *b) @@ -672,7 +710,9 @@ static int GIT_STDLIB_CALL git__qsort_r_glue_cmp( void git__qsort_r( void *els, size_t nel, size_t elsize, git__sort_r_cmp cmp, void *payload) { -#if defined(__MINGW32__) +#if defined(__MINGW32__) || defined(__OpenBSD__) || defined(AMIGA) || \ + defined(__gnu_hurd__) || \ + (__GLIBC__ == 2 && __GLIBC_MINOR__ < 8) git__insertsort_r(els, nel, elsize, NULL, cmp, payload); #elif defined(GIT_WIN32) git__qsort_r_glue glue = { cmp, payload }; diff --git a/src/util.h b/src/util.h index c0f271997..ed9624770 100644 --- a/src/util.h +++ b/src/util.h @@ -7,6 +7,8 @@ #ifndef INCLUDE_util_h__ #define INCLUDE_util_h__ +#include "common.h" + #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) #define bitsizeof(x) (CHAR_BIT * sizeof(x)) #define MSB(x, bits) ((x) & (~0ULL << (bitsizeof(x) - (bits)))) @@ -107,6 +109,13 @@ GIT_INLINE(int) git__is_sizet(git_off_t p) return p == (git_off_t)r; } +/** @return true if p fits into the range of a uint32_t */ +GIT_INLINE(int) git__is_uint32(size_t p) +{ + uint32_t r = (uint32_t)p; + return p == (size_t)r; +} + /* 32-bit cross-platform rotl */ #ifdef _MSC_VER /* use built-in method in MSVC */ # define git__rotl(v, s) (uint32_t)_rotl(v, s) @@ -185,21 +194,25 @@ extern int git__strcasecmp(const char *a, const char *b); extern int git__strncmp(const char *a, const char *b, size_t sz); extern int git__strncasecmp(const char *a, const char *b, size_t sz); +extern int git__strcasesort_cmp(const char *a, const char *b); + +#include "thread-utils.h" + typedef struct { - short refcount; + git_atomic refcount; void *owner; } git_refcount; typedef void (*git_refcount_freeptr)(void *r); #define GIT_REFCOUNT_INC(r) { \ - ((git_refcount *)(r))->refcount++; \ + git_atomic_inc(&((git_refcount *)(r))->refcount); \ } #define GIT_REFCOUNT_DEC(_r, do_free) { \ git_refcount *r = (git_refcount *)(_r); \ - r->refcount--; \ - if (r->refcount <= 0 && r->owner == NULL) { do_free(_r); } \ + int val = git_atomic_dec(&r->refcount); \ + if (val <= 0 && r->owner == NULL) { do_free(_r); } \ } #define GIT_REFCOUNT_OWN(r, o) { \ @@ -208,6 +221,9 @@ typedef void (*git_refcount_freeptr)(void *r); #define GIT_REFCOUNT_OWNER(r) (((git_refcount *)(r))->owner) +#define GIT_REFCOUNT_VAL(r) git_atomic_get(&((git_refcount *)(r))->refcount) + + static signed char from_hex[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 00 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 10 */ @@ -260,22 +276,27 @@ GIT_INLINE(size_t) git__size_t_powerof2(size_t v) GIT_INLINE(bool) git__isupper(int c) { - return (c >= 'A' && c <= 'Z'); + return (c >= 'A' && c <= 'Z'); } GIT_INLINE(bool) git__isalpha(int c) { - return ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')); + return ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')); } GIT_INLINE(bool) git__isdigit(int c) { - return (c >= '0' && c <= '9'); + return (c >= '0' && c <= '9'); } GIT_INLINE(bool) git__isspace(int c) { - return (c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r' || c == '\v' || c == 0x85 /* Unicode CR+LF */); + return (c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r' || c == '\v' || c == 0x85 /* Unicode CR+LF */); +} + +GIT_INLINE(bool) git__isspace_nonlf(int c) +{ + return (c == ' ' || c == '\t' || c == '\f' || c == '\r' || c == '\v' || c == 0x85 /* Unicode CR+LF */); } GIT_INLINE(bool) git__iswildcard(int c) @@ -284,8 +305,7 @@ GIT_INLINE(bool) git__iswildcard(int c) } /* - * Parse a string value as a boolean, just like Core Git - * does. + * Parse a string value as a boolean, just like Core Git does. * * Valid values for true are: 'true', 'yes', 'on' * Valid values for false are: 'false', 'no', 'off' @@ -300,15 +320,31 @@ extern int git__parse_bool(int *out, const char *value); * - "July 17, 2003" * - "2003-7-17 08:23" */ -int git__date_parse(git_time_t *out, const char *date); +extern int git__date_parse(git_time_t *out, const char *date); /* * Unescapes a string in-place. - * + * * Edge cases behavior: * - "jackie\" -> "jacky\" * - "chan\\" -> "chan\" */ extern size_t git__unescape(char *str); +/* + * Safely zero-out memory, making sure that the compiler + * doesn't optimize away the operation. + */ +GIT_INLINE(void) git__memzero(void *data, size_t size) +{ +#ifdef _MSC_VER + SecureZeroMemory((PVOID)data, size); +#else + volatile uint8_t *scan = (volatile uint8_t *)data; + + while (size--) + *scan++ = 0x0; +#endif +} + #endif /* INCLUDE_util_h__ */ diff --git a/src/vector.c b/src/vector.c index f4a818ed2..5ba2fab18 100644 --- a/src/vector.c +++ b/src/vector.c @@ -277,15 +277,13 @@ void git_vector_swap(git_vector *a, git_vector *b) int git_vector_resize_to(git_vector *v, size_t new_length) { - if (new_length <= v->length) - return 0; - if (new_length > v->_alloc_size && resize_vector(v, new_length) < 0) return -1; - memset(&v->contents[v->length], 0, - sizeof(void *) * (new_length - v->length)); + if (new_length > v->length) + memset(&v->contents[v->length], 0, + sizeof(void *) * (new_length - v->length)); v->length = new_length; diff --git a/src/vector.h b/src/vector.h index e2f729b83..1bda9c93d 100644 --- a/src/vector.h +++ b/src/vector.h @@ -78,4 +78,13 @@ void git_vector_remove_matching( int git_vector_resize_to(git_vector *v, size_t new_length); int git_vector_set(void **old, git_vector *v, size_t position, void *value); +/** Set the comparison function used for sorting the vector */ +GIT_INLINE(void) git_vector_set_cmp(git_vector *v, git_vector_cmp cmp) +{ + if (cmp != v->_cmp) { + v->_cmp = cmp; + v->sorted = 0; + } +} + #endif diff --git a/src/win32/dir.c b/src/win32/dir.c index 95ae5060e..8c51d8378 100644 --- a/src/win32/dir.c +++ b/src/win32/dir.c @@ -32,7 +32,7 @@ git__DIR *git__opendir(const char *dir) if (!dir || !init_filter(filter, sizeof(filter), dir)) return NULL; - new = git__malloc(sizeof(*new)); + new = git__calloc(1, sizeof(*new)); if (!new) return NULL; diff --git a/src/win32/error.c b/src/win32/error.c index 4a9a0631f..a62a07e82 100644 --- a/src/win32/error.c +++ b/src/win32/error.c @@ -12,7 +12,9 @@ # include <winhttp.h> #endif +#ifndef WC_ERR_INVALID_CHARS #define WC_ERR_INVALID_CHARS 0x80 +#endif char *git_win32_get_error_message(DWORD error_code) { diff --git a/src/win32/findfile.c b/src/win32/findfile.c index bc36b6b45..9d9051bff 100644 --- a/src/win32/findfile.c +++ b/src/win32/findfile.c @@ -156,7 +156,7 @@ static int win32_find_git_in_registry( } static int win32_find_existing_dirs( - git_buf *out, const wchar_t *tmpl[], char *temp[]) + git_buf *out, const wchar_t *tmpl[]) { struct git_win32__path path16; git_buf buf = GIT_BUF_INIT; @@ -209,7 +209,6 @@ int git_win32__find_system_dirs(git_buf *out) int git_win32__find_global_dirs(git_buf *out) { - char *temp[3]; static const wchar_t *global_tmpls[4] = { L"%HOME%\\", L"%HOMEDRIVE%%HOMEPATH%\\", @@ -217,12 +216,11 @@ int git_win32__find_global_dirs(git_buf *out) NULL, }; - return win32_find_existing_dirs(out, global_tmpls, temp); + return win32_find_existing_dirs(out, global_tmpls); } int git_win32__find_xdg_dirs(git_buf *out) { - char *temp[6]; static const wchar_t *global_tmpls[7] = { L"%XDG_CONFIG_HOME%\\git", L"%APPDATA%\\git", @@ -233,6 +231,5 @@ int git_win32__find_xdg_dirs(git_buf *out) NULL, }; - return win32_find_existing_dirs(out, global_tmpls, temp); + return win32_find_existing_dirs(out, global_tmpls); } - diff --git a/src/win32/git2.rc b/src/win32/git2.rc index 436913228..22c63f695 100644 --- a/src/win32/git2.rc +++ b/src/win32/git2.rc @@ -1,10 +1,8 @@ #include <winver.h> #include "../../include/git2/version.h" -#ifndef INCLUDE_LIB -#define LIBGIT2_FILENAME "git2.dll" -#else -#define LIBGIT2_FILENAME "libgit2.dll" +#ifndef LIBGIT2_FILENAME +# define LIBGIT2_FILENAME "git2" #endif VS_VERSION_INFO VERSIONINFO MOVEABLE IMPURE LOADONCALL DISCARDABLE @@ -27,9 +25,9 @@ BEGIN BEGIN VALUE "FileDescription", "libgit2 - the Git linkable library\0" VALUE "FileVersion", LIBGIT2_VERSION "\0" - VALUE "InternalName", LIBGIT2_FILENAME "\0" + VALUE "InternalName", LIBGIT2_FILENAME ".dll\0" VALUE "LegalCopyright", "Copyright (C) the libgit2 contributors. All rights reserved.\0" - VALUE "OriginalFilename", LIBGIT2_FILENAME "\0" + VALUE "OriginalFilename", LIBGIT2_FILENAME ".dll\0" VALUE "ProductName", "libgit2\0" VALUE "ProductVersion", LIBGIT2_VERSION "\0" VALUE "Comments", "For more information visit http://libgit2.github.com/\0" diff --git a/src/win32/posix_w32.c b/src/win32/posix_w32.c index 4d56299f7..036632e2a 100644 --- a/src/win32/posix_w32.c +++ b/src/win32/posix_w32.c @@ -5,6 +5,7 @@ * a Linking Exception. For full terms see the included COPYING file. */ #include "../posix.h" +#include "../fileops.h" #include "path.h" #include "utf-conv.h" #include "repository.h" @@ -89,6 +90,9 @@ static int do_lstat( if (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) fMode |= S_IFLNK; + if ((fMode & (S_IFDIR | S_IFLNK)) == (S_IFDIR | S_IFLNK)) // junction + fMode ^= S_IFLNK; + buf->st_ino = 0; buf->st_gid = 0; buf->st_uid = 0; @@ -295,7 +299,18 @@ int p_getcwd(char *buffer_out, size_t size) int p_stat(const char* path, struct stat* buf) { - return do_lstat(path, buf, 0); + char target[GIT_WIN_PATH]; + int error = 0; + + error = do_lstat(path, buf, 0); + + /* We need not do this in a loop to unwind chains of symlinks since + * p_readlink calls GetFinalPathNameByHandle which does it for us. */ + if (error >= 0 && S_ISLNK(buf->st_mode) && + (error = p_readlink(path, target, GIT_WIN_PATH)) >= 0) + error = do_lstat(target, buf, 0); + + return error; } int p_chdir(const char* path) @@ -314,9 +329,20 @@ int p_chmod(const char* path, mode_t mode) int p_rmdir(const char* path) { + int error; wchar_t buf[GIT_WIN_PATH]; git__utf8_to_16(buf, GIT_WIN_PATH, path); - return _wrmdir(buf); + + error = _wrmdir(buf); + + /* _wrmdir() is documented to return EACCES if "A program has an open + * handle to the directory." This sounds like what everybody else calls + * EBUSY. Let's convert appropriate error codes. + */ + if (GetLastError() == ERROR_SHARING_VIOLATION) + errno = EBUSY; + + return error; } int p_hide_directory__w32(const char *path) @@ -457,29 +483,29 @@ int p_send(GIT_SOCKET socket, const void *buffer, size_t length, int flags) * Borrowed from http://old.nabble.com/Porting-localtime_r-and-gmtime_r-td15282276.html * On Win32, `gmtime_r` doesn't exist but `gmtime` is threadsafe, so we can use that */ -struct tm * -p_localtime_r (const time_t *timer, struct tm *result) -{ - struct tm *local_result; - local_result = localtime (timer); - - if (local_result == NULL || result == NULL) - return NULL; - - memcpy (result, local_result, sizeof (struct tm)); - return result; -} -struct tm * -p_gmtime_r (const time_t *timer, struct tm *result) -{ - struct tm *local_result; - local_result = gmtime (timer); - - if (local_result == NULL || result == NULL) - return NULL; - - memcpy (result, local_result, sizeof (struct tm)); - return result; +struct tm * +p_localtime_r (const time_t *timer, struct tm *result) +{ + struct tm *local_result; + local_result = localtime (timer); + + if (local_result == NULL || result == NULL) + return NULL; + + memcpy (result, local_result, sizeof (struct tm)); + return result; +} +struct tm * +p_gmtime_r (const time_t *timer, struct tm *result) +{ + struct tm *local_result; + local_result = gmtime (timer); + + if (local_result == NULL || result == NULL) + return NULL; + + memcpy (result, local_result, sizeof (struct tm)); + return result; } #if defined(_MSC_VER) || defined(_MSC_EXTENSIONS) @@ -492,44 +518,44 @@ p_gmtime_r (const time_t *timer, struct tm *result) #define _TIMEZONE_DEFINED struct timezone { - int tz_minuteswest; /* minutes W of Greenwich */ - int tz_dsttime; /* type of dst correction */ + int tz_minuteswest; /* minutes W of Greenwich */ + int tz_dsttime; /* type of dst correction */ }; #endif - + int p_gettimeofday(struct timeval *tv, struct timezone *tz) { - FILETIME ft; - unsigned __int64 tmpres = 0; - static int tzflag; - - if (NULL != tv) - { - GetSystemTimeAsFileTime(&ft); - - tmpres |= ft.dwHighDateTime; - tmpres <<= 32; - tmpres |= ft.dwLowDateTime; - - /*converting file time to unix epoch*/ - tmpres /= 10; /*convert into microseconds*/ - tmpres -= DELTA_EPOCH_IN_MICROSECS; - tv->tv_sec = (long)(tmpres / 1000000UL); - tv->tv_usec = (long)(tmpres % 1000000UL); - } - - if (NULL != tz) - { - if (!tzflag) - { - _tzset(); - tzflag++; - } - tz->tz_minuteswest = _timezone / 60; - tz->tz_dsttime = _daylight; - } - - return 0; + FILETIME ft; + unsigned __int64 tmpres = 0; + static int tzflag; + + if (NULL != tv) + { + GetSystemTimeAsFileTime(&ft); + + tmpres |= ft.dwHighDateTime; + tmpres <<= 32; + tmpres |= ft.dwLowDateTime; + + /*converting file time to unix epoch*/ + tmpres /= 10; /*convert into microseconds*/ + tmpres -= DELTA_EPOCH_IN_MICROSECS; + tv->tv_sec = (long)(tmpres / 1000000UL); + tv->tv_usec = (long)(tmpres % 1000000UL); + } + + if (NULL != tz) + { + if (!tzflag) + { + _tzset(); + tzflag++; + } + tz->tz_minuteswest = _timezone / 60; + tz->tz_dsttime = _daylight; + } + + return 0; } int p_inet_pton(int af, const char* src, void* dst) diff --git a/src/win32/pthread.c b/src/win32/pthread.c index 105f4b89e..2f263b3e0 100644 --- a/src/win32/pthread.c +++ b/src/win32/pthread.c @@ -14,22 +14,30 @@ int pthread_create( void *GIT_RESTRICT arg) { GIT_UNUSED(attr); - *thread = (pthread_t) CreateThread( + *thread = CreateThread( NULL, 0, (LPTHREAD_START_ROUTINE)start_routine, arg, 0, NULL); return *thread ? 0 : -1; } int pthread_join(pthread_t thread, void **value_ptr) { - int ret; - ret = WaitForSingleObject(thread, INFINITE); - if (ret && value_ptr) - GetExitCodeThread(thread, (void*) value_ptr); - return -(!!ret); + DWORD ret = WaitForSingleObject(thread, INFINITE); + + if (ret == WAIT_OBJECT_0) { + if (value_ptr != NULL) { + *value_ptr = NULL; + GetExitCodeThread(thread, (void *)value_ptr); + } + CloseHandle(thread); + return 0; + } + + return -1; } -int pthread_mutex_init(pthread_mutex_t *GIT_RESTRICT mutex, - const pthread_mutexattr_t *GIT_RESTRICT mutexattr) +int pthread_mutex_init( + pthread_mutex_t *GIT_RESTRICT mutex, + const pthread_mutexattr_t *GIT_RESTRICT mutexattr) { GIT_UNUSED(mutexattr); InitializeCriticalSection(mutex); diff --git a/src/win32/pthread.h b/src/win32/pthread.h index a219a0137..8277ecf6e 100644 --- a/src/win32/pthread.h +++ b/src/win32/pthread.h @@ -25,13 +25,16 @@ typedef HANDLE pthread_cond_t; #define PTHREAD_MUTEX_INITIALIZER {(void*)-1}; -int pthread_create(pthread_t *GIT_RESTRICT, - const pthread_attr_t *GIT_RESTRICT, - void *(*start_routine)(void*), void *__restrict); +int pthread_create( + pthread_t *GIT_RESTRICT, + const pthread_attr_t *GIT_RESTRICT, + void *(*start_routine)(void*), + void *__restrict); int pthread_join(pthread_t, void **); -int pthread_mutex_init(pthread_mutex_t *GIT_RESTRICT, const pthread_mutexattr_t *GIT_RESTRICT); +int pthread_mutex_init( + pthread_mutex_t *GIT_RESTRICT, const pthread_mutexattr_t *GIT_RESTRICT); int pthread_mutex_destroy(pthread_mutex_t *); int pthread_mutex_lock(pthread_mutex_t *); int pthread_mutex_unlock(pthread_mutex_t *); |