diff options
| author | Vicent Marti <tanoku@gmail.com> | 2013-04-16 17:46:41 +0200 |
|---|---|---|
| committer | Vicent Marti <tanoku@gmail.com> | 2013-04-16 17:46:41 +0200 |
| commit | a50086d174658914d4d6462afbc83b02825b1f5b (patch) | |
| tree | e8daa1c7bf678222cf351445179837bed7db3a72 /src | |
| parent | 5b9fac39d8a76b9139667c26a63e6b3f204b3977 (diff) | |
| parent | f124ebd457bfbf43de3516629aaba5a279636e04 (diff) | |
| download | libgit2-a50086d174658914d4d6462afbc83b02825b1f5b.tar.gz | |
Merge branch 'development'v0.18.0
Diffstat (limited to 'src')
197 files changed, 34094 insertions, 8958 deletions
diff --git a/src/amiga/map.c b/src/amiga/map.c new file mode 100644 index 000000000..0ba7995c6 --- /dev/null +++ b/src/amiga/map.c @@ -0,0 +1,48 @@ +/* + * 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/common.h> + +#ifndef GIT_WIN32 + +#include "posix.h" +#include "map.h" +#include <errno.h> + +int p_mmap(git_map *out, size_t len, int prot, int flags, int fd, git_off_t offset) +{ + GIT_MMAP_VALIDATE(out, len, prot, flags); + + out->data = NULL; + out->len = 0; + + if ((prot & GIT_PROT_WRITE) && ((flags & GIT_MAP_TYPE) == GIT_MAP_SHARED)) { + giterr_set(GITERR_OS, "Trying to map shared-writeable"); + return -1; + } + + out->data = malloc(len); + GITERR_CHECK_ALLOC(out->data); + + if ((p_lseek(fd, offset, SEEK_SET) < 0) || ((size_t)p_read(fd, out->data, len) != len)) { + giterr_set(GITERR_OS, "mmap emulation failed"); + return -1; + } + + out->len = len; + return 0; +} + +int p_munmap(git_map *map) +{ + assert(map != NULL); + free(map->data); + + return 0; +} + +#endif + diff --git a/src/attr.c b/src/attr.c index 093f64d5c..979fecc14 100644 --- a/src/attr.c +++ b/src/attr.c @@ -1,10 +1,32 @@ #include "repository.h" #include "fileops.h" #include "config.h" +#include "attr.h" +#include "ignore.h" +#include "git2/oid.h" #include <ctype.h> GIT__USE_STRMAP; +const char *git_attr__true = "[internal]__TRUE__"; +const char *git_attr__false = "[internal]__FALSE__"; +const char *git_attr__unset = "[internal]__UNSET__"; + +git_attr_t git_attr_value(const char *attr) +{ + if (attr == NULL || attr == git_attr__unset) + return GIT_ATTR_UNSPECIFIED_T; + + if (attr == git_attr__true) + return GIT_ATTR_TRUE_T; + + if (attr == git_attr__false) + return GIT_ATTR_FALSE_T; + + return GIT_ATTR_VALUE_T; +} + + static int collect_attr_files( git_repository *repo, uint32_t flags, @@ -22,7 +44,7 @@ int git_attr_get( int error; git_attr_path path; git_vector files = GIT_VECTOR_INIT; - unsigned int i, j; + size_t i, j; git_attr_file *file; git_attr_name attr; git_attr_rule *rule; @@ -41,8 +63,9 @@ int git_attr_get( git_vector_foreach(&files, i, file) { git_attr_file__foreach_matching_rule(file, &path, j, rule) { - int pos = git_vector_bsearch(&rule->assigns, &attr); - if (pos >= 0) { + size_t pos; + + if (!git_vector_bsearch(&pos, &rule->assigns, &attr)) { *value = ((git_attr_assignment *)git_vector_get( &rule->assigns, pos))->value; goto cleanup; @@ -74,7 +97,7 @@ int git_attr_get_many( int error; git_attr_path path; git_vector files = GIT_VECTOR_INIT; - unsigned int i, j, k; + size_t i, j, k; git_attr_file *file; git_attr_rule *rule; attr_get_many_info *info = NULL; @@ -96,7 +119,7 @@ int git_attr_get_many( git_attr_file__foreach_matching_rule(file, &path, j, rule) { for (k = 0; k < num_attr; k++) { - int pos; + size_t pos; if (info[k].found != NULL) /* already found assignment */ continue; @@ -106,8 +129,7 @@ int git_attr_get_many( info[k].name.name_hash = git_attr_file__name_hash(names[k]); } - pos = git_vector_bsearch(&rule->assigns, &info[k].name); - if (pos >= 0) { + if (!git_vector_bsearch(&pos, &rule->assigns, &info[k].name)) { info[k].found = (git_attr_assignment *) git_vector_get(&rule->assigns, pos); values[k] = info[k].found->value; @@ -138,7 +160,7 @@ int git_attr_foreach( int error; git_attr_path path; git_vector files = GIT_VECTOR_INIT; - unsigned int i, j, k; + size_t i, j, k; git_attr_file *file; git_attr_rule *rule; git_attr_assignment *assign; @@ -163,11 +185,15 @@ int git_attr_foreach( continue; git_strmap_insert(seen, assign->name, assign, error); - if (error >= 0) - error = callback(assign->name, assign->value, payload); + if (error < 0) + goto cleanup; - if (error != 0) + error = callback(assign->name, assign->value, payload); + if (error) { + giterr_clear(); + error = GIT_EUSER; goto cleanup; + } } } } @@ -237,30 +263,30 @@ bool git_attr_cache__is_cached( static int load_attr_file( const char **data, - git_attr_file_stat_sig *sig, + git_futils_filestamp *stamp, const char *filename) { int error; git_buf content = GIT_BUF_INIT; - struct stat st; - if (p_stat(filename, &st) < 0) - return GIT_ENOTFOUND; + error = git_futils_filestamp_check(stamp, filename); + if (error < 0) + return error; - if (sig != NULL && - (git_time_t)st.st_mtime == sig->seconds && - (git_off_t)st.st_size == sig->size && - (unsigned int)st.st_ino == sig->ino) + /* if error == 0, then file is up to date. By returning GIT_ENOTFOUND, + * we tell the caller not to reparse this file... + */ + if (!error) return GIT_ENOTFOUND; - error = git_futils_readbuffer_updated(&content, filename, NULL, NULL); - if (error < 0) - return error; + error = git_futils_readbuffer(&content, filename); + if (error < 0) { + /* convert error into ENOTFOUND so failed permissions / invalid + * file type don't actually stop the operation in progress. + */ + return GIT_ENOTFOUND; - if (sig != NULL) { - sig->seconds = (git_time_t)st.st_mtime; - sig->size = (git_off_t)st.st_size; - sig->ino = (unsigned int)st.st_ino; + /* TODO: once warnings are available, issue a warning callback */ } *data = git_buf_detach(&content); @@ -276,14 +302,15 @@ static int load_attr_blob_from_index( const char *relfile) { int error; + size_t pos; git_index *index; - git_index_entry *entry; + const git_index_entry *entry; if ((error = git_repository_index__weakptr(&index, repo)) < 0 || - (error = git_index_find(index, relfile)) < 0) + (error = git_index_find(&pos, index, relfile)) < 0) return error; - entry = git_index_get(index, error); + entry = git_index_get_byindex(index, pos); if (old_oid && git_oid_cmp(old_oid, &entry->oid) == 0) return GIT_ENOTFOUND; @@ -352,6 +379,7 @@ int git_attr_cache__push_file( const char *filename, git_attr_file_source source, git_attr_file_parser parse, + void* parsedata, git_vector *stack) { int error = 0; @@ -361,7 +389,7 @@ int git_attr_cache__push_file( git_attr_cache *cache = git_repository_attr_cache(repo); git_attr_file *file = NULL; git_blob *blob = NULL; - git_attr_file_stat_sig st; + git_futils_filestamp stamp; assert(filename && stack); @@ -383,12 +411,10 @@ int git_attr_cache__push_file( /* if not in cache, load data, parse, and cache */ if (source == GIT_ATTR_FILE_FROM_FILE) { - if (file) - memcpy(&st, &file->cache_data.st, sizeof(st)); - else - memset(&st, 0, sizeof(st)); + git_futils_filestamp_set( + &stamp, file ? &file->cache_data.stamp : NULL); - error = load_attr_file(&content, &st, filename); + error = load_attr_file(&content, &stamp, filename); } else { error = load_attr_blob_from_index(&content, &blob, repo, file ? &file->cache_data.oid : NULL, relfile); @@ -403,14 +429,19 @@ int git_attr_cache__push_file( goto finish; } - if (!file && - (error = git_attr_file__new(&file, source, relfile, &cache->pool)) < 0) - goto finish; + /* if we got here, we have to parse and/or reparse the file */ + if (file) + git_attr_file__clear_rules(file); + else { + error = git_attr_file__new(&file, source, relfile, &cache->pool); + if (error < 0) + goto finish; + } - if (parse && (error = parse(repo, content, file)) < 0) + if (parse && (error = parse(repo, parsedata, content, file)) < 0) goto finish; - git_strmap_insert(cache->files, file->key, file, error); + git_strmap_insert(cache->files, file->key, file, error); //-V595 if (error > 0) error = 0; @@ -418,7 +449,7 @@ int git_attr_cache__push_file( if (blob) git_oid_cpy(&file->cache_data.oid, git_object_id((git_object *)blob)); else - memcpy(&file->cache_data.st, &st, sizeof(st)); + git_futils_filestamp_set(&file->cache_data.stamp, &stamp); finish: /* push file onto vector if we found one*/ @@ -439,7 +470,7 @@ finish: } #define push_attr_file(R,S,B,F) \ - git_attr_cache__push_file((R),(B),(F),GIT_ATTR_FILE_FROM_FILE,git_attr_file__parse_buffer,(S)) + git_attr_cache__push_file((R),(B),(F),GIT_ATTR_FILE_FROM_FILE,git_attr_file__parse_buffer,NULL,(S)) typedef struct { git_repository *repo; @@ -488,7 +519,7 @@ static int push_one_attr(void *ref, git_buf *path) for (i = 0; !error && i < n_src; ++i) error = git_attr_cache__push_file( info->repo, path->ptr, GIT_ATTR_FILE, src[i], - git_attr_file__parse_buffer, info->files); + git_attr_file__parse_buffer, NULL, info->files); return error; } @@ -550,8 +581,10 @@ static int collect_attr_files( error = git_futils_find_system_file(&dir, GIT_ATTR_FILE_SYSTEM); if (!error) error = push_attr_file(repo, files, NULL, dir.ptr); - else if (error == GIT_ENOTFOUND) + else if (error == GIT_ENOTFOUND) { + giterr_clear(); error = 0; + } } cleanup: @@ -562,6 +595,29 @@ static int collect_attr_files( return error; } +static int attr_cache__lookup_path( + const char **out, git_config *cfg, const char *key, const char *fallback) +{ + git_buf buf = GIT_BUF_INIT; + int error; + + if (!(error = git_config_get_string(out, cfg, key))) + return 0; + + 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); + } + + return error; +} int git_attr_cache__init(git_repository *repo) { @@ -576,16 +632,16 @@ int git_attr_cache__init(git_repository *repo) if (git_repository_config__weakptr(&cfg, repo) < 0) return -1; - ret = git_config_get_string(&cache->cfg_attr_file, cfg, GIT_ATTR_CONFIG); - if (ret < 0 && ret != GIT_ENOTFOUND) + ret = attr_cache__lookup_path( + &cache->cfg_attr_file, cfg, GIT_ATTR_CONFIG, GIT_ATTR_FILE_XDG); + if (ret < 0) return ret; - ret = git_config_get_string(&cache->cfg_excl_file, cfg, GIT_IGNORE_CONFIG); - if (ret < 0 && ret != GIT_ENOTFOUND) + ret = attr_cache__lookup_path( + &cache->cfg_excl_file, cfg, GIT_IGNORE_CONFIG, GIT_IGNORE_FILE_XDG); + if (ret < 0) return ret; - giterr_clear(); - /* allocate hashtable for attribute and ignore file contents */ if (cache->files == NULL) { cache->files = git_strmap_alloc(); diff --git a/src/attr.h b/src/attr.h index a35b1160f..19c979bcd 100644 --- a/src/attr.h +++ b/src/attr.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -8,24 +8,12 @@ #define INCLUDE_attr_h__ #include "attr_file.h" -#include "strmap.h" -#define GIT_ATTR_CONFIG "core.attributesfile" -#define GIT_IGNORE_CONFIG "core.excludesfile" - -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_attr_cache; +#define GIT_ATTR_CONFIG "core.attributesfile" +#define GIT_IGNORE_CONFIG "core.excludesfile" typedef int (*git_attr_file_parser)( - git_repository *, const char *, git_attr_file *); - -extern int git_attr_cache__init(git_repository *repo); + git_repository *, void *, const char *, git_attr_file *); extern int git_attr_cache__insert_macro( git_repository *repo, git_attr_rule *macro); @@ -39,6 +27,7 @@ extern int git_attr_cache__push_file( const char *filename, git_attr_file_source source, git_attr_file_parser parse, + void *parsedata, /* passed through to parse function */ git_vector *stack); extern int git_attr_cache__internal_file( diff --git a/src/attr_file.c b/src/attr_file.c index 5030ad5de..85cd87624 100644 --- a/src/attr_file.c +++ b/src/attr_file.c @@ -1,16 +1,17 @@ #include "common.h" #include "repository.h" #include "filebuf.h" +#include "attr.h" #include "git2/blob.h" #include "git2/tree.h" #include <ctype.h> -const char *git_attr__true = "[internal]__TRUE__"; -const char *git_attr__false = "[internal]__FALSE__"; -const char *git_attr__unset = "[internal]__UNSET__"; - static int sort_by_hash_and_name(const void *a_raw, const void *b_raw); static void git_attr_rule__clear(git_attr_rule *rule); +static bool parse_optimized_patterns( + git_attr_fnmatch *spec, + git_pool *pool, + const char *pattern); int git_attr_file__new( git_attr_file **attrs_ptr, @@ -57,13 +58,15 @@ fail: } int git_attr_file__parse_buffer( - git_repository *repo, const char *buffer, git_attr_file *attrs) + git_repository *repo, void *parsedata, const char *buffer, git_attr_file *attrs) { int error = 0; const char *scan = NULL; char *context = NULL; git_attr_rule *rule = NULL; + GIT_UNUSED(parsedata); + assert(buffer && attrs); scan = buffer; @@ -127,7 +130,7 @@ int git_attr_file__new_and_load( if (!(error = git_futils_readbuffer(&content, path))) error = git_attr_file__parse_buffer( - NULL, git_buf_cstr(&content), *attrs_ptr); + NULL, NULL, git_buf_cstr(&content), *attrs_ptr); git_buf_free(&content); @@ -139,18 +142,23 @@ int git_attr_file__new_and_load( return error; } -void git_attr_file__free(git_attr_file *file) +void git_attr_file__clear_rules(git_attr_file *file) { unsigned int i; git_attr_rule *rule; - if (!file) - return; - git_vector_foreach(&file->rules, i, rule) git_attr_rule__free(rule); git_vector_free(&file->rules); +} + +void git_attr_file__free(git_attr_file *file) +{ + if (!file) + return; + + git_attr_file__clear_rules(file); if (file->pool_is_allocated) { git_pool_clear(file->pool); @@ -178,7 +186,7 @@ int git_attr_file__lookup_one( const char *attr, const char **value) { - unsigned int i; + size_t i; git_attr_name name; git_attr_rule *rule; @@ -188,9 +196,9 @@ int git_attr_file__lookup_one( name.name_hash = git_attr_file__name_hash(attr); git_attr_file__foreach_matching_rule(file, path, i, rule) { - int pos = git_vector_bsearch(&rule->assigns, &name); + size_t pos; - if (pos >= 0) { + if (!git_vector_bsearch(&pos, &rule->assigns, &name)) { *value = ((git_attr_assignment *) git_vector_get(&rule->assigns, pos))->value; break; @@ -206,16 +214,17 @@ bool git_attr_fnmatch__match( const git_attr_path *path) { int fnm; + int icase_flags = (match->flags & GIT_ATTR_FNMATCH_ICASE) ? FNM_CASEFOLD : 0; if (match->flags & GIT_ATTR_FNMATCH_DIRECTORY && !path->is_dir) return false; if (match->flags & GIT_ATTR_FNMATCH_FULLPATH) - fnm = p_fnmatch(match->pattern, path->path, FNM_PATHNAME); + fnm = p_fnmatch(match->pattern, path->path, FNM_PATHNAME | icase_flags); else if (path->is_dir) - fnm = p_fnmatch(match->pattern, path->basename, FNM_LEADING_DIR); + fnm = p_fnmatch(match->pattern, path->basename, FNM_LEADING_DIR | icase_flags); else - fnm = p_fnmatch(match->pattern, path->basename, 0); + fnm = p_fnmatch(match->pattern, path->basename, icase_flags); return (fnm == FNM_NOMATCH) ? false : true; } @@ -236,31 +245,29 @@ bool git_attr_rule__match( git_attr_assignment *git_attr_rule__lookup_assignment( git_attr_rule *rule, const char *name) { - int pos; + size_t pos; git_attr_name key; key.name = name; key.name_hash = git_attr_file__name_hash(name); - pos = git_vector_bsearch(&rule->assigns, &key); + if (git_vector_bsearch(&pos, &rule->assigns, &key)) + return NULL; - return (pos >= 0) ? git_vector_get(&rule->assigns, pos) : NULL; + return git_vector_get(&rule->assigns, pos); } int git_attr_path__init( git_attr_path *info, const char *path, const char *base) { + ssize_t root; + /* build full path as best we can */ git_buf_init(&info->full, 0); - if (base != NULL && git_path_root(path) < 0) { - if (git_buf_joinpath(&info->full, base, path) < 0) - return -1; - info->path = info->full.ptr + strlen(base); - } else { - if (git_buf_sets(&info->full, path) < 0) - return -1; - info->path = info->full.ptr; - } + if (git_path_join_unrooted(&info->full, path, base, &root) < 0) + return -1; + + info->path = info->full.ptr + root; /* remove trailing slashes */ while (info->full.size > 0) { @@ -293,7 +300,6 @@ void git_attr_path__free(git_attr_path *info) info->basename = NULL; } - /* * From gitattributes(5): * @@ -338,10 +344,16 @@ int git_attr_fnmatch__parse( const char **base) { const char *pattern, *scan; - int slash_count; + int slash_count, allow_space; assert(spec && base && *base); + if (parse_optimized_patterns(spec, pool, *base)) + return 0; + + spec->flags = (spec->flags & GIT_ATTR_FNMATCH_ALLOWSPACE); + allow_space = (spec->flags != 0); + pattern = *base; while (git__isspace(*pattern)) pattern++; @@ -350,8 +362,6 @@ int git_attr_fnmatch__parse( return GIT_ENOTFOUND; } - spec->flags = 0; - if (*pattern == '[') { if (strncmp(pattern, "[attr]", 6) == 0) { spec->flags = spec->flags | GIT_ATTR_FNMATCH_MACRO; @@ -368,8 +378,10 @@ int git_attr_fnmatch__parse( slash_count = 0; for (scan = pattern; *scan != '\0'; ++scan) { /* scan until (non-escaped) white space */ - if (git__isspace(*scan) && *(scan - 1) != '\\') - break; + if (git__isspace(*scan) && *(scan - 1) != '\\') { + if (!allow_space || (*scan != ' ' && *scan != '\t')) + break; + } if (*scan == '/') { spec->flags = spec->flags | GIT_ATTR_FNMATCH_FULLPATH; @@ -418,22 +430,28 @@ int git_attr_fnmatch__parse( return -1; } else { /* strip '\' that might have be used for internal whitespace */ - char *to = spec->pattern; - for (scan = spec->pattern; *scan; to++, scan++) { - if (*scan == '\\') - scan++; /* skip '\' but include next char */ - if (to != scan) - *to = *scan; - } - if (to != scan) { - *to = '\0'; - spec->length = (to - spec->pattern); - } + spec->length = git__unescape(spec->pattern); } return 0; } +static bool parse_optimized_patterns( + git_attr_fnmatch *spec, + git_pool *pool, + const char *pattern) +{ + if (!pattern[1] && (pattern[0] == '*' || pattern[0] == '.')) { + spec->flags = GIT_ATTR_FNMATCH_MATCH_ALL; + spec->pattern = git_pool_strndup(pool, pattern, 1); + spec->length = 1; + + return true; + } + + return false; +} + static int sort_by_hash_and_name(const void *a_raw, const void *b_raw) { const git_attr_name *a = a_raw; diff --git a/src/attr_file.h b/src/attr_file.h index 3718f4bda..d8abcda58 100644 --- a/src/attr_file.h +++ b/src/attr_file.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -7,14 +7,17 @@ #ifndef INCLUDE_attr_file_h__ #define INCLUDE_attr_file_h__ +#include "git2/oid.h" #include "git2/attr.h" #include "vector.h" #include "pool.h" #include "buffer.h" +#include "fileops.h" #define GIT_ATTR_FILE ".gitattributes" #define GIT_ATTR_FILE_INREPO "info/attributes" #define GIT_ATTR_FILE_SYSTEM "gitattributes" +#define GIT_ATTR_FILE_XDG "attributes" #define GIT_ATTR_FNMATCH_NEGATIVE (1U << 0) #define GIT_ATTR_FNMATCH_DIRECTORY (1U << 1) @@ -22,6 +25,13 @@ #define GIT_ATTR_FNMATCH_MACRO (1U << 3) #define GIT_ATTR_FNMATCH_IGNORE (1U << 4) #define GIT_ATTR_FNMATCH_HASWILD (1U << 5) +#define GIT_ATTR_FNMATCH_ALLOWSPACE (1U << 6) +#define GIT_ATTR_FNMATCH_ICASE (1U << 7) +#define GIT_ATTR_FNMATCH_MATCH_ALL (1U << 8) + +extern const char *git_attr__true; +extern const char *git_attr__false; +extern const char *git_attr__unset; typedef struct { char *pattern; @@ -48,27 +58,21 @@ typedef struct { } git_attr_assignment; typedef struct { - git_time_t seconds; - git_off_t size; - unsigned int ino; -} git_attr_file_stat_sig; - -typedef struct { char *key; /* cache "source#path" this was loaded from */ git_vector rules; /* vector of <rule*> or <fnmatch*> */ git_pool *pool; bool pool_is_allocated; union { git_oid oid; - git_attr_file_stat_sig st; + git_futils_filestamp stamp; } cache_data; } git_attr_file; typedef struct { - git_buf full; - const char *path; - const char *basename; - int is_dir; + git_buf full; + char *path; + char *basename; + int is_dir; } git_attr_path; typedef enum { @@ -88,8 +92,10 @@ extern int git_attr_file__new_and_load( extern void git_attr_file__free(git_attr_file *file); +extern void git_attr_file__clear_rules(git_attr_file *file); + extern int git_attr_file__parse_buffer( - git_repository *repo, const char *buf, git_attr_file *file); + git_repository *repo, void *parsedata, const char *buf, git_attr_file *file); extern int git_attr_file__lookup_one( git_attr_file *file, diff --git a/src/attrcache.h b/src/attrcache.h new file mode 100644 index 000000000..12cec4bfb --- /dev/null +++ b/src/attrcache.h @@ -0,0 +1,24 @@ +/* + * 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_attrcache_h__ +#define INCLUDE_attrcache_h__ + +#include "pool.h" +#include "strmap.h" + +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_attr_cache; + +extern int git_attr_cache__init(git_repository *repo); + +#endif diff --git a/src/blob.c b/src/blob.c index e25944b91..c0514fc13 100644 --- a/src/blob.c +++ b/src/blob.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -12,17 +12,18 @@ #include "common.h" #include "blob.h" #include "filter.h" +#include "buf_text.h" -const void *git_blob_rawcontent(git_blob *blob) +const void *git_blob_rawcontent(const git_blob *blob) { assert(blob); return blob->odb_object->raw.data; } -size_t git_blob_rawsize(git_blob *blob) +git_off_t git_blob_rawsize(const git_blob *blob) { assert(blob); - return blob->odb_object->raw.len; + return (git_off_t)blob->odb_object->raw.len; } int git_blob__getbuf(git_buf *buffer, git_blob *blob) @@ -68,6 +69,7 @@ static int write_file_stream( int fd, error; char buffer[4096]; git_odb_stream *stream = NULL; + ssize_t read_len = -1, written = 0; if ((error = git_odb_open_wstream( &stream, odb, (size_t)file_size, GIT_OBJ_BLOB)) < 0) @@ -78,20 +80,18 @@ static int write_file_stream( return -1; } - while (!error && file_size > 0) { - ssize_t read_len = p_read(fd, buffer, sizeof(buffer)); - - if (read_len < 0) { - giterr_set( - GITERR_OS, "Failed to create blob. Can't read whole file"); - error = -1; - } - else if (!(error = stream->write(stream, buffer, read_len))) - file_size -= read_len; + while (!error && (read_len = p_read(fd, buffer, sizeof(buffer))) > 0) { + error = stream->write(stream, buffer, read_len); + written += read_len; } p_close(fd); + if (written != file_size || read_len < 0) { + giterr_set(GITERR_OS, "Failed to read file into stream"); + error = -1; + } + if (!error) error = stream->finalize_write(oid, stream); @@ -148,27 +148,31 @@ static int write_symlink( return error; } -static int blob_create_internal(git_oid *oid, git_repository *repo, const char *path) +static int blob_create_internal(git_oid *oid, git_repository *repo, const char *content_path, const char *hint_path, bool try_load_filters) { int error; struct stat st; git_odb *odb = NULL; git_off_t size; - if ((error = git_path_lstat(path, &st)) < 0 || (error = git_repository_odb__weakptr(&odb, repo)) < 0) + 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; size = st.st_size; if (S_ISLNK(st.st_mode)) { - error = write_symlink(oid, odb, path, (size_t)size); + error = write_symlink(oid, odb, content_path, (size_t)size); } else { git_vector write_filters = GIT_VECTOR_INIT; - int filter_count; + int filter_count = 0; - /* Load the filters for writing this file to the ODB */ - filter_count = git_filters_load( - &write_filters, repo, path, GIT_FILTER_TO_ODB); + if (try_load_filters) { + /* Load the filters for writing this file to the ODB */ + filter_count = git_filters_load( + &write_filters, repo, hint_path, GIT_FILTER_TO_ODB); + } if (filter_count < 0) { /* Negative value means there was a critical error */ @@ -176,10 +180,10 @@ static int blob_create_internal(git_oid *oid, git_repository *repo, const char * } else if (filter_count == 0) { /* No filters need to be applied to the document: we can stream * directly from disk */ - error = write_file_stream(oid, odb, path, size); + error = write_file_stream(oid, odb, content_path, size); } else { /* We need to apply one or more filters */ - error = write_file_filtered(oid, odb, path, &write_filters); + error = write_file_filtered(oid, odb, content_path, &write_filters); } git_filters_free(&write_filters); @@ -202,21 +206,25 @@ static int blob_create_internal(git_oid *oid, git_repository *repo, const char * return error; } -int git_blob_create_fromfile(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); - assert(workdir); /* error to call this on bare 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)); + 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; @@ -226,14 +234,88 @@ int git_blob_create_fromdisk(git_oid *oid, git_repository *repo, const char *pat { int error; git_buf full_path = GIT_BUF_INIT; + const char *workdir, *hintpath; if ((error = git_path_prettify(&full_path, path, NULL)) < 0) { git_buf_free(&full_path); return error; } - error = blob_create_internal(oid, repo, git_buf_cstr(&full_path)); + hintpath = git_buf_cstr(&full_path); + workdir = git_repository_workdir(repo); + + if (workdir && !git__prefixcmp(hintpath, workdir)) + hintpath += strlen(workdir); + + error = blob_create_internal( + oid, repo, git_buf_cstr(&full_path), hintpath, true); git_buf_free(&full_path); return error; } + +#define BUFFER_SIZE 4096 + +int git_blob_create_fromchunks( + git_oid *oid, + git_repository *repo, + const char *hintpath, + int (*source_cb)(char *content, size_t max_length, void *payload), + void *payload) +{ + int error = -1, read_bytes; + char *content = NULL; + 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; + + content = git__malloc(BUFFER_SIZE); + GITERR_CHECK_ALLOC(content); + + if (git_filebuf_open(&file, git_buf_cstr(&path), GIT_FILEBUF_TEMPORARY) < 0) + goto cleanup; + + while (1) { + read_bytes = source_cb(content, BUFFER_SIZE, payload); + + assert(read_bytes <= BUFFER_SIZE); + + if (read_bytes <= 0) + break; + + if (git_filebuf_write(&file, content, read_bytes) < 0) + goto cleanup; + } + + if (read_bytes < 0) + goto cleanup; + + if (git_filebuf_flush(&file) < 0) + goto cleanup; + + error = blob_create_internal(oid, repo, file.path_lock, hintpath, hintpath != NULL); + +cleanup: + git_buf_free(&path); + git_filebuf_cleanup(&file); + git__free(content); + return error; +} + +int git_blob_is_binary(git_blob *blob) +{ + git_buf content; + + assert(blob); + + content.ptr = blob->odb_object->raw.data; + content.size = min(blob->odb_object->raw.len, 4000); + + return git_buf_text_is_binary(&content); +} diff --git a/src/blob.h b/src/blob.h index 0305e9473..524734b1f 100644 --- a/src/blob.h +++ b/src/blob.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. diff --git a/src/branch.c b/src/branch.c index 5d5a24038..e7088790e 100644 --- a/src/branch.c +++ b/src/branch.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -7,8 +7,12 @@ #include "common.h" #include "commit.h" -#include "branch.h" #include "tag.h" +#include "config.h" +#include "refspec.h" +#include "refs.h" + +#include "git2/branch.h" static int retrieve_branch_reference( git_reference **branch_reference_out, @@ -41,168 +45,539 @@ cleanup: return error; } -static int create_error_invalid(const char *msg) +static int not_a_local_branch(const char *reference_name) { - giterr_set(GITERR_INVALID, "Cannot create branch - %s", msg); + giterr_set( + GITERR_INVALID, + "Reference '%s' is not a local branch.", reference_name); return -1; } int git_branch_create( - git_oid *oid_out, - git_repository *repo, - const char *branch_name, - const git_object *target, - int force) -{ - git_otype target_type = GIT_OBJ_BAD; - git_object *commit = NULL; + git_reference **ref_out, + git_repository *repository, + const char *branch_name, + const git_commit *commit, + int force) +{ git_reference *branch = NULL; git_buf canonical_branch_name = GIT_BUF_INIT; int error = -1; - assert(repo && branch_name && target && oid_out); - - if (git_object_owner(target) != repo) - return create_error_invalid("The given target does not belong to this repository"); - - target_type = git_object_type(target); - - switch (target_type) - { - case GIT_OBJ_TAG: - if (git_tag_peel(&commit, (git_tag *)target) < 0) - goto cleanup; - - if (git_object_type(commit) != GIT_OBJ_COMMIT) { - create_error_invalid("The given target does not resolve to a commit"); - goto cleanup; - } - break; - - case GIT_OBJ_COMMIT: - commit = (git_object *)target; - break; - - default: - return create_error_invalid("Only git_tag and git_commit objects are valid targets."); - } + assert(branch_name && commit && ref_out); + assert(git_object_owner((const git_object *)commit) == repository); if (git_buf_joinpath(&canonical_branch_name, GIT_REFS_HEADS_DIR, branch_name) < 0) goto cleanup; - if (git_reference_create_oid(&branch, repo, git_buf_cstr(&canonical_branch_name), git_object_id(commit), force) < 0) - goto cleanup; + error = git_reference_create(&branch, repository, + git_buf_cstr(&canonical_branch_name), git_commit_id(commit), force); - git_oid_cpy(oid_out, git_reference_oid(branch)); - error = 0; + if (!error) + *ref_out = branch; cleanup: - if (target_type == GIT_OBJ_TAG) - git_object_free(commit); - - git_reference_free(branch); git_buf_free(&canonical_branch_name); return error; } -int git_branch_delete(git_repository *repo, const char *branch_name, git_branch_t branch_type) +int git_branch_delete(git_reference *branch) { - git_reference *branch = NULL; - git_reference *head = NULL; - int error; + int is_head; + git_buf config_section = GIT_BUF_INIT; + int error = -1; + + assert(branch); - assert((branch_type == GIT_BRANCH_LOCAL) || (branch_type == GIT_BRANCH_REMOTE)); + if (!git_reference_is_branch(branch) && + !git_reference_is_remote(branch)) { + giterr_set(GITERR_INVALID, "Reference '%s' is not a valid branch.", git_reference_name(branch)); + return -1; + } - if ((error = retrieve_branch_reference(&branch, repo, branch_name, branch_type == GIT_BRANCH_REMOTE)) < 0) - return error; + if ((is_head = git_branch_is_head(branch)) < 0) + return is_head; - if (git_reference_lookup(&head, repo, GIT_HEAD_FILE) < 0) { - giterr_set(GITERR_REFERENCE, "Cannot locate HEAD."); - goto on_error; + if (is_head) { + giterr_set(GITERR_REFERENCE, + "Cannot delete branch '%s' as it is the current HEAD of the repository.", git_reference_name(branch)); + return -1; } - if ((git_reference_type(head) == GIT_REF_SYMBOLIC) - && (strcmp(git_reference_target(head), git_reference_name(branch)) == 0)) { - giterr_set(GITERR_REFERENCE, - "Cannot delete branch '%s' as it is the current HEAD of the repository.", branch_name); + if (git_buf_printf(&config_section, "branch.%s", git_reference_name(branch) + strlen(GIT_REFS_HEADS_DIR)) < 0) + goto on_error; + + if (git_config_rename_section( + git_reference_owner(branch), + git_buf_cstr(&config_section), + NULL) < 0) goto on_error; - } if (git_reference_delete(branch) < 0) goto on_error; - git_reference_free(head); - return 0; + error = 0; on_error: - git_reference_free(head); - git_reference_free(branch); - return -1; + git_buf_free(&config_section); + return error; } typedef struct { - git_vector *branchlist; + git_branch_foreach_cb branch_cb; + void *callback_payload; unsigned int branch_type; -} branch_filter_data; +} branch_foreach_filter; -static int branch_list_cb(const char *branch_name, void *payload) +static int branch_foreach_cb(const char *branch_name, void *payload) { - branch_filter_data *filter = (branch_filter_data *)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_LOCAL && git__prefixcmp(branch_name, GIT_REFS_HEADS_DIR) == 0) - || (filter->branch_type & GIT_BRANCH_REMOTE && git__prefixcmp(branch_name, GIT_REFS_REMOTES_DIR) == 0)) - return git_vector_insert(filter->branchlist, git__strdup(branch_name)); + 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_list(git_strarray *branch_names, git_repository *repo, unsigned int list_flags) +int git_branch_foreach( + git_repository *repo, + unsigned int list_flags, + git_branch_foreach_cb branch_cb, + void *payload) { + branch_foreach_filter filter; + + filter.branch_cb = branch_cb; + filter.branch_type = list_flags; + filter.callback_payload = payload; + + return git_reference_foreach(repo, GIT_REF_LISTALL, &branch_foreach_cb, (void *)&filter); +} + +int git_branch_move( + git_reference **out, + git_reference *branch, + const char *new_branch_name, + int force) +{ + git_buf new_reference_name = GIT_BUF_INIT, + old_config_section = GIT_BUF_INIT, + new_config_section = GIT_BUF_INIT; int error; - branch_filter_data filter; - git_vector branchlist; - assert(branch_names && repo); + assert(branch && new_branch_name); + + 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) + goto done; + + 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; + +done: + git_buf_free(&new_reference_name); + git_buf_free(&old_config_section); + git_buf_free(&new_config_section); - if (git_vector_init(&branchlist, 8, NULL) < 0) + return error; +} + +int git_branch_lookup( + git_reference **ref_out, + git_repository *repo, + const char *branch_name, + git_branch_t branch_type) +{ + assert(ref_out && repo && branch_name); + + return retrieve_branch_reference(ref_out, repo, branch_name, branch_type == GIT_BRANCH_REMOTE); +} + +int git_branch_name(const char **out, git_reference *ref) +{ + const char *branch_name; + + assert(out && ref); + + branch_name = ref->name; + + if (git_reference_is_branch(ref)) { + branch_name += strlen(GIT_REFS_HEADS_DIR); + } else if (git_reference_is_remote(ref)) { + branch_name += strlen(GIT_REFS_REMOTES_DIR); + } else { + giterr_set(GITERR_INVALID, + "Reference '%s' is neither a local nor a remote branch.", ref->name); return -1; + } + *out = branch_name; + return 0; +} - filter.branchlist = &branchlist; - filter.branch_type = list_flags; +static int retrieve_upstream_configuration( + const char **out, + git_repository *repo, + const char *canonical_branch_name, + const char *format) +{ + git_config *config; + git_buf buf = GIT_BUF_INIT; + int error; - error = git_reference_foreach(repo, GIT_REF_LISTALL, &branch_list_cb, (void *)&filter); - if (error < 0) { - git_vector_free(&branchlist); + if (git_repository_config__weakptr(&config, repo) < 0) return -1; + + if (git_buf_printf(&buf, format, + canonical_branch_name + strlen(GIT_REFS_HEADS_DIR)) < 0) + return -1; + + error = git_config_get_string(out, config, git_buf_cstr(&buf)); + git_buf_free(&buf); + return error; +} + +int git_branch_upstream__name( + git_buf *tracking_name, + git_repository *repo, + const char *canonical_branch_name) +{ + const char *remote_name, *merge_name; + git_buf buf = GIT_BUF_INIT; + int error = -1; + git_remote *remote = NULL; + const git_refspec *refspec; + + assert(tracking_name && canonical_branch_name); + + if (!git_reference__is_branch(canonical_branch_name)) + return not_a_local_branch(canonical_branch_name); + + if ((error = retrieve_upstream_configuration( + &remote_name, repo, canonical_branch_name, "branch.%s.remote")) < 0) + goto cleanup; + + if ((error = retrieve_upstream_configuration( + &merge_name, repo, canonical_branch_name, "branch.%s.merge")) < 0) + goto cleanup; + + if (!*remote_name || !*merge_name) { + error = GIT_ENOTFOUND; + goto cleanup; } - branch_names->strings = (char **)branchlist.contents; - branch_names->count = branchlist.length; - return 0; + if (strcmp(".", remote_name) != 0) { + 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; + } + + if (git_refspec_transform_r(&buf, refspec, merge_name) < 0) + goto cleanup; + } else + if (git_buf_sets(&buf, merge_name) < 0) + goto cleanup; + + error = git_buf_set(tracking_name, git_buf_cstr(&buf), git_buf_len(&buf)); + +cleanup: + git_remote_free(remote); + git_buf_free(&buf); + return error; } -int git_branch_move(git_repository *repo, const char *old_branch_name, const char *new_branch_name, int force) +static int remote_name(git_buf *buf, git_repository *repo, const char *canonical_branch_name) { - git_reference *reference = NULL; - git_buf old_reference_name = GIT_BUF_INIT, new_reference_name = GIT_BUF_INIT; + git_strarray remote_list = {0}; + size_t i; + git_remote *remote; + const git_refspec *fetchspec; int error = 0; + char *remote_name = NULL; + + assert(buf && repo && canonical_branch_name); - if ((error = git_buf_joinpath(&old_reference_name, GIT_REFS_HEADS_DIR, old_branch_name)) < 0) + /* Verify that this is a remote branch */ + if (!git_reference__is_remote(canonical_branch_name)) { + giterr_set(GITERR_INVALID, "Reference '%s' is not a remote branch.", + canonical_branch_name); + error = GIT_ERROR; goto cleanup; + } - /* We need to be able to return GIT_ENOTFOUND */ - if ((error = git_reference_lookup(&reference, repo, git_buf_cstr(&old_reference_name))) < 0) + /* Get the remotes */ + if ((error = git_remote_list(&remote_list, repo)) < 0) goto cleanup; - if ((error = git_buf_joinpath(&new_reference_name, GIT_REFS_HEADS_DIR, new_branch_name)) < 0) + /* Find matching remotes */ + for (i = 0; i < remote_list.count; i++) { + 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)) { + /* If we have not already set out yet, then set + * it to the matching remote name. Otherwise + * multiple remotes match this reference, and it + * is ambiguous. */ + if (!remote_name) { + remote_name = remote_list.strings[i]; + } else { + git_remote_free(remote); + error = GIT_EAMBIGUOUS; + goto cleanup; + } + } + + git_remote_free(remote); + } + + if (remote_name) { + git_buf_clear(buf); + error = git_buf_puts(buf, remote_name); + } else { + error = GIT_ENOTFOUND; + } + +cleanup: + git_strarray_free(&remote_list); + return error; +} + +int git_branch_remote_name(char *buffer, size_t buffer_len, git_repository *repo, const char *refname) +{ + int ret; + git_buf buf = GIT_BUF_INIT; + + if ((ret = remote_name(&buf, repo, refname)) < 0) + return ret; + + if (buffer) + git_buf_copy_cstr(buffer, buffer_len, &buf); + + ret = git_buf_len(&buf) + 1; + git_buf_free(&buf); + + return ret; +} + +int git_branch_upstream_name( + char *tracking_branch_name_out, + size_t buffer_size, + git_repository *repo, + const char *canonical_branch_name) +{ + git_buf buf = GIT_BUF_INIT; + int error; + + assert(canonical_branch_name); + + if (tracking_branch_name_out && buffer_size) + *tracking_branch_name_out = '\0'; + + if ((error = git_branch_upstream__name( + &buf, repo, canonical_branch_name)) < 0) + goto cleanup; + + if (tracking_branch_name_out && buf.size + 1 > buffer_size) { /* +1 for NUL byte */ + giterr_set( + GITERR_INVALID, + "Buffer too short to hold the tracked reference name."); + error = -1; goto cleanup; + } + + if (tracking_branch_name_out) + git_buf_copy_cstr(tracking_branch_name_out, buffer_size, &buf); - error = git_reference_rename(reference, git_buf_cstr(&new_reference_name), force); + error = (int)buf.size + 1; cleanup: - git_reference_free(reference); - git_buf_free(&old_reference_name); - git_buf_free(&new_reference_name); + git_buf_free(&buf); + return (int)error; +} + +int git_branch_upstream( + git_reference **tracking_out, + git_reference *branch) +{ + int error; + git_buf tracking_name = GIT_BUF_INIT; + + if ((error = git_branch_upstream__name(&tracking_name, + git_reference_owner(branch), git_reference_name(branch))) < 0) + return error; + error = git_reference_lookup( + tracking_out, + git_reference_owner(branch), + git_buf_cstr(&tracking_name)); + + git_buf_free(&tracking_name); return error; } + +static int unset_upstream(git_config *config, const char *shortname) +{ + git_buf buf = GIT_BUF_INIT; + + if (git_buf_printf(&buf, "branch.%s.remote", shortname) < 0) + return -1; + + if (git_config_delete_entry(config, git_buf_cstr(&buf)) < 0) + goto on_error; + + git_buf_clear(&buf); + if (git_buf_printf(&buf, "branch.%s.merge", shortname) < 0) + goto on_error; + + if (git_config_delete_entry(config, git_buf_cstr(&buf)) < 0) + goto on_error; + + git_buf_free(&buf); + return 0; + +on_error: + git_buf_free(&buf); + return -1; +} + +int git_branch_set_upstream(git_reference *branch, const char *upstream_name) +{ + git_buf key = GIT_BUF_INIT, value = GIT_BUF_INIT; + git_reference *upstream; + git_repository *repo; + git_remote *remote = NULL; + git_config *config; + const char *name, *shortname; + int local; + const git_refspec *fetchspec; + + name = git_reference_name(branch); + if (!git_reference__is_branch(name)) + return not_a_local_branch(name); + + if (git_repository_config__weakptr(&config, git_reference_owner(branch)) < 0) + return -1; + + shortname = name + strlen(GIT_REFS_HEADS_DIR); + + if (upstream_name == NULL) + return unset_upstream(config, shortname); + + repo = git_reference_owner(branch); + + /* First we need to figure out whether it's a branch or remote-tracking */ + if (git_branch_lookup(&upstream, repo, upstream_name, GIT_BRANCH_LOCAL) == 0) + local = 1; + else if (git_branch_lookup(&upstream, repo, upstream_name, GIT_BRANCH_REMOTE) == 0) + local = 0; + else + return GIT_ENOTFOUND; + + /* + * If it's local, the remote is "." and the branch name is + * simply the refname. Otherwise we need to figure out what + * the remote-tracking branch's name on the remote is and use + * that. + */ + if (local) + git_buf_puts(&value, "."); + else + remote_name(&value, repo, git_reference_name(upstream)); + + if (git_buf_printf(&key, "branch.%s.remote", shortname) < 0) + goto on_error; + + if (git_config_set_string(config, git_buf_cstr(&key), git_buf_cstr(&value)) < 0) + goto on_error; + + if (local) { + if (git_buf_puts(&value, git_reference_name(branch)) < 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); + git_buf_clear(&value); + if (git_refspec_transform_l(&value, fetchspec, git_reference_name(upstream)) < 0) + goto on_error; + + git_remote_free(remote); + remote = NULL; + } + + git_buf_clear(&key); + if (git_buf_printf(&key, "branch.%s.merge", shortname) < 0) + goto on_error; + + if (git_config_set_string(config, git_buf_cstr(&key), git_buf_cstr(&value)) < 0) + goto on_error; + + git_reference_free(upstream); + git_buf_free(&key); + git_buf_free(&value); + + return 0; + +on_error: + git_reference_free(upstream); + git_buf_free(&key); + git_buf_free(&value); + git_remote_free(remote); + + return -1; +} + +int git_branch_is_head( + git_reference *branch) +{ + git_reference *head; + bool is_same = false; + int error; + + assert(branch); + + if (!git_reference_is_branch(branch)) + return false; + + error = git_repository_head(&head, git_reference_owner(branch)); + + if (error == GIT_EORPHANEDHEAD || error == GIT_ENOTFOUND) + return false; + + if (error < 0) + return -1; + + is_same = strcmp( + git_reference_name(branch), + git_reference_name(head)) == 0; + + git_reference_free(head); + + return is_same; +} diff --git a/src/branch.h b/src/branch.h index d0e5abc8b..d02f2af0d 100644 --- a/src/branch.h +++ b/src/branch.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -7,11 +7,11 @@ #ifndef INCLUDE_branch_h__ #define INCLUDE_branch_h__ -#include "git2/branch.h" +#include "buffer.h" -struct git_branch { - char *remote; /* TODO: Make this a git_remote */ - char *merge; -}; +int git_branch_upstream__name( + git_buf *tracking_name, + git_repository *repo, + const char *canonical_branch_name); #endif diff --git a/src/bswap.h b/src/bswap.h index 995767a14..486df82f4 100644 --- a/src/bswap.h +++ b/src/bswap.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. diff --git a/src/buf_text.c b/src/buf_text.c new file mode 100644 index 000000000..443454b5f --- /dev/null +++ b/src/buf_text.c @@ -0,0 +1,291 @@ +/* + * 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 "buf_text.h" + +int git_buf_text_puts_escaped( + git_buf *buf, + const char *string, + const char *esc_chars, + const char *esc_with) +{ + const char *scan; + size_t total = 0, esc_len = strlen(esc_with), count; + + if (!string) + return 0; + + for (scan = string; *scan; ) { + /* count run of non-escaped characters */ + count = strcspn(scan, esc_chars); + total += count; + scan += count; + /* count run of escaped characters */ + count = strspn(scan, esc_chars); + total += count * (esc_len + 1); + scan += count; + } + + if (git_buf_grow(buf, buf->size + total + 1) < 0) + return -1; + + for (scan = string; *scan; ) { + count = strcspn(scan, esc_chars); + + memmove(buf->ptr + buf->size, scan, count); + scan += count; + buf->size += count; + + for (count = strspn(scan, esc_chars); count > 0; --count) { + /* copy escape sequence */ + memmove(buf->ptr + buf->size, esc_with, esc_len); + buf->size += esc_len; + /* copy character to be escaped */ + buf->ptr[buf->size] = *scan; + buf->size++; + scan++; + } + } + + buf->ptr[buf->size] = '\0'; + + return 0; +} + +void git_buf_text_unescape(git_buf *buf) +{ + buf->size = git__unescape(buf->ptr); +} + +int git_buf_text_crlf_to_lf(git_buf *tgt, const git_buf *src) +{ + const char *scan = src->ptr; + const char *scan_end = src->ptr + src->size; + const char *next = memchr(scan, '\r', src->size); + char *out; + + assert(tgt != src); + + if (!next) + return GIT_ENOTFOUND; + + /* reduce reallocs while in the loop */ + if (git_buf_grow(tgt, src->size) < 0) + return -1; + out = tgt->ptr; + tgt->size = 0; + + /* Find the next \r and copy whole chunk up to there to tgt */ + for (; next; scan = next + 1, next = memchr(scan, '\r', scan_end - scan)) { + if (next > scan) { + size_t copylen = next - scan; + memcpy(out, scan, copylen); + out += copylen; + } + + /* Do not drop \r unless it is followed by \n */ + if (next[1] != '\n') + *out++ = '\r'; + } + + /* Copy remaining input into dest */ + memcpy(out, scan, scan_end - scan + 1); /* +1 for NUL byte */ + out += (scan_end - scan); + tgt->size = out - tgt->ptr; + + return 0; +} + +int git_buf_text_lf_to_crlf(git_buf *tgt, const git_buf *src) +{ + const char *start = src->ptr; + const char *end = start + src->size; + const char *scan = start; + const char *next = memchr(scan, '\n', src->size); + + assert(tgt != src); + + if (!next) + return GIT_ENOTFOUND; + + /* attempt to reduce reallocs while in the loop */ + if (git_buf_grow(tgt, src->size + (src->size >> 4) + 1) < 0) + return -1; + tgt->size = 0; + + for (; next; scan = next + 1, next = memchr(scan, '\n', end - scan)) { + size_t copylen = next - scan; + /* don't convert existing \r\n to \r\r\n */ + size_t extralen = (next > start && next[-1] == '\r') ? 1 : 2; + size_t needsize = tgt->size + copylen + extralen + 1; + + if (tgt->asize < needsize && git_buf_grow(tgt, needsize) < 0) + return -1; + + if (next > scan) { + memcpy(tgt->ptr + tgt->size, scan, copylen); + tgt->size += copylen; + } + if (extralen == 2) + tgt->ptr[tgt->size++] = '\r'; + tgt->ptr[tgt->size++] = '\n'; + } + + return git_buf_put(tgt, scan, end - scan); +} + +int git_buf_text_common_prefix(git_buf *buf, const git_strarray *strings) +{ + size_t i; + const char *str, *pfx; + + git_buf_clear(buf); + + if (!strings || !strings->count) + return 0; + + /* initialize common prefix to first string */ + if (git_buf_sets(buf, strings->strings[0]) < 0) + return -1; + + /* go through the rest of the strings, truncating to shared prefix */ + for (i = 1; i < strings->count; ++i) { + + for (str = strings->strings[i], pfx = buf->ptr; + *str && *str == *pfx; str++, pfx++) + /* scanning */; + + git_buf_truncate(buf, pfx - buf->ptr); + + if (!buf->size) + break; + } + + return 0; +} + +bool git_buf_text_is_binary(const git_buf *buf) +{ + const char *scan = buf->ptr, *end = buf->ptr + buf->size; + int printable = 0, nonprintable = 0; + + while (scan < end) { + unsigned char c = *scan++; + + if (c > 0x1F && c < 0x7F) + printable++; + else if (c == '\0') + return true; + else if (!git__isspace(c)) + nonprintable++; + } + + return ((printable >> 7) < nonprintable); +} + +bool git_buf_text_contains_nul(const git_buf *buf) +{ + return (memchr(buf->ptr, '\0', buf->size) != NULL); +} + +int git_buf_text_detect_bom(git_bom_t *bom, const git_buf *buf, size_t offset) +{ + const char *ptr; + size_t len; + + *bom = GIT_BOM_NONE; + /* need at least 2 bytes after offset to look for any BOM */ + if (buf->size < offset + 2) + return 0; + + ptr = buf->ptr + offset; + len = buf->size - offset; + + switch (*ptr++) { + case 0: + if (len >= 4 && ptr[0] == 0 && ptr[1] == '\xFE' && ptr[2] == '\xFF') { + *bom = GIT_BOM_UTF32_BE; + return 4; + } + break; + case '\xEF': + if (len >= 3 && ptr[0] == '\xBB' && ptr[1] == '\xBF') { + *bom = GIT_BOM_UTF8; + return 3; + } + break; + case '\xFE': + if (*ptr == '\xFF') { + *bom = GIT_BOM_UTF16_BE; + return 2; + } + break; + case '\xFF': + if (*ptr != '\xFE') + break; + if (len >= 4 && ptr[1] == 0 && ptr[2] == 0) { + *bom = GIT_BOM_UTF32_LE; + return 4; + } else { + *bom = GIT_BOM_UTF16_LE; + return 2; + } + break; + default: + break; + } + + return 0; +} + +bool git_buf_text_gather_stats( + git_buf_text_stats *stats, const git_buf *buf, bool skip_bom) +{ + const char *scan = buf->ptr, *end = buf->ptr + buf->size; + int skip; + + memset(stats, 0, sizeof(*stats)); + + /* BOM detection */ + skip = git_buf_text_detect_bom(&stats->bom, buf, 0); + if (skip_bom) + scan += skip; + + /* Ignore EOF character */ + if (buf->size > 0 && end[-1] == '\032') + end--; + + /* Counting loop */ + while (scan < end) { + unsigned char c = *scan++; + + if ((c > 0x1F && c < 0x7F) || c > 0x9f) + stats->printable++; + else switch (c) { + case '\0': + stats->nul++; + stats->nonprintable++; + break; + case '\n': + stats->lf++; + break; + case '\r': + stats->cr++; + if (scan < end && *scan == '\n') + stats->crlf++; + break; + case '\t': case '\f': case '\v': case '\b': case 0x1b: /*ESC*/ + stats->printable++; + break; + default: + stats->nonprintable++; + break; + } + } + + return (stats->nul > 0 || + ((stats->printable >> 7) < stats->nonprintable)); +} diff --git a/src/buf_text.h b/src/buf_text.h new file mode 100644 index 000000000..58e4e26a7 --- /dev/null +++ b/src/buf_text.h @@ -0,0 +1,122 @@ +/* + * 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_buf_text_h__ +#define INCLUDE_buf_text_h__ + +#include "buffer.h" + +typedef enum { + GIT_BOM_NONE = 0, + GIT_BOM_UTF8 = 1, + GIT_BOM_UTF16_LE = 2, + GIT_BOM_UTF16_BE = 3, + GIT_BOM_UTF32_LE = 4, + GIT_BOM_UTF32_BE = 5 +} git_bom_t; + +typedef struct { + git_bom_t bom; /* BOM found at head of text */ + unsigned int nul, cr, lf, crlf; /* NUL, CR, LF and CRLF counts */ + unsigned int printable, nonprintable; /* These are just approximations! */ +} git_buf_text_stats; + +/** + * Append string to buffer, prefixing each character from `esc_chars` with + * `esc_with` string. + * + * @param buf Buffer to append data to + * @param string String to escape and append + * @param esc_chars Characters to be escaped + * @param esc_with String to insert in from of each found character + * @return 0 on success, <0 on failure (probably allocation problem) + */ +extern int git_buf_text_puts_escaped( + git_buf *buf, + const char *string, + const char *esc_chars, + const char *esc_with); + +/** + * Append string escaping characters that are regex special + */ +GIT_INLINE(int) git_buf_text_puts_escape_regex(git_buf *buf, const char *string) +{ + return git_buf_text_puts_escaped(buf, string, "^.[]$()|*+?{}\\", "\\"); +} + +/** + * Unescape all characters in a buffer in place + * + * I.e. remove backslashes + */ +extern void git_buf_text_unescape(git_buf *buf); + +/** + * Replace all \r\n with \n (or do nothing if no \r\n are found) + * + * @return 0 on success, GIT_ENOTFOUND if no \r\n, -1 on memory error + */ +extern int git_buf_text_crlf_to_lf(git_buf *tgt, const git_buf *src); + +/** + * Replace all \n with \r\n (or do nothing if no \n are found) + * + * @return 0 on success, GIT_ENOTFOUND if no \n, -1 on memory error + */ +extern int git_buf_text_lf_to_crlf(git_buf *tgt, const git_buf *src); + +/** + * Fill buffer with the common prefix of a array of strings + * + * Buffer will be set to empty if there is no common prefix + */ +extern int git_buf_text_common_prefix(git_buf *buf, const git_strarray *strs); + +/** + * Check quickly if buffer looks like it contains binary data + * + * @param buf Buffer to check + * @return true if buffer looks like non-text data + */ +extern bool git_buf_text_is_binary(const git_buf *buf); + +/** + * Check quickly if buffer contains a NUL byte + * + * @param buf Buffer to check + * @return true if buffer contains a NUL byte + */ +extern bool git_buf_text_contains_nul(const git_buf *buf); + +/** + * Check if a buffer begins with a UTF BOM + * + * @param bom Set to the type of BOM detected or GIT_BOM_NONE + * @param buf Buffer in which to check the first bytes for a BOM + * @param offset Offset into buffer to look for BOM + * @return Number of bytes of BOM data (or 0 if no BOM found) + */ +extern int git_buf_text_detect_bom( + git_bom_t *bom, const git_buf *buf, size_t offset); + +/** + * Gather stats for a piece of text + * + * Fill the `stats` structure with counts of unreadable characters, carriage + * returns, etc, so it can be used in heuristics. This automatically skips + * a trailing EOF (\032 character). Also it will look for a BOM at the + * start of the text and can be told to skip that as well. + * + * @param stats Structure to be filled in + * @param buf Text to process + * @param skip_bom Exclude leading BOM from stats if true + * @return Does the buffer heuristically look like binary data + */ +extern bool git_buf_text_gather_stats( + git_buf_text_stats *stats, const git_buf *buf, bool skip_bom); + +#endif diff --git a/src/buffer.c b/src/buffer.c index 783a36eb8..6e3ffe560 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -31,15 +31,7 @@ void git_buf_init(git_buf *buf, size_t initial_size) git_buf_grow(buf, initial_size); } -int git_buf_grow(git_buf *buf, size_t target_size) -{ - int error = git_buf_try_grow(buf, target_size); - if (error != 0) - buf->ptr = git_buf__oom; - return error; -} - -int git_buf_try_grow(git_buf *buf, size_t target_size) +int git_buf_try_grow(git_buf *buf, size_t target_size, bool mark_oom) { char *new_ptr; size_t new_size; @@ -67,8 +59,12 @@ int git_buf_try_grow(git_buf *buf, size_t target_size) new_size = (new_size + 7) & ~7; new_ptr = git__realloc(new_ptr, new_size); - if (!new_ptr) + + if (!new_ptr) { + if (mark_oom) + buf->ptr = git_buf__oom; return -1; + } buf->asize = new_size; buf->ptr = new_ptr; @@ -141,11 +137,52 @@ int git_buf_puts(git_buf *buf, const char *string) return git_buf_put(buf, string, strlen(string)); } +static const char b64str[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +int git_buf_put_base64(git_buf *buf, const char *data, size_t len) +{ + size_t extra = len % 3; + uint8_t *write, a, b, c; + const uint8_t *read = (const uint8_t *)data; + + ENSURE_SIZE(buf, buf->size + 4 * ((len / 3) + !!extra) + 1); + write = (uint8_t *)&buf->ptr[buf->size]; + + /* convert each run of 3 bytes into 4 output bytes */ + for (len -= extra; len > 0; len -= 3) { + a = *read++; + b = *read++; + c = *read++; + + *write++ = b64str[a >> 2]; + *write++ = b64str[(a & 0x03) << 4 | b >> 4]; + *write++ = b64str[(b & 0x0f) << 2 | c >> 6]; + *write++ = b64str[c & 0x3f]; + } + + if (extra > 0) { + a = *read++; + b = (extra > 1) ? *read++ : 0; + + *write++ = b64str[a >> 2]; + *write++ = b64str[(a & 0x03) << 4 | b >> 4]; + *write++ = (extra > 1) ? b64str[(b & 0x0f) << 2] : '='; + *write++ = '='; + } + + buf->size = ((char *)write) - buf->ptr; + buf->ptr[buf->size] = '\0'; + + return 0; +} + int git_buf_vprintf(git_buf *buf, const char *format, va_list ap) { int len; + const size_t expected_size = buf->size + (strlen(format) * 2); - ENSURE_SIZE(buf, buf->size + (strlen(format) * 2)); + ENSURE_SIZE(buf, expected_size); while (1) { va_list args; @@ -411,51 +448,30 @@ int git_buf_cmp(const git_buf *a, const git_buf *b) (a->size < b->size) ? -1 : (a->size > b->size) ? 1 : 0; } -int git_buf_common_prefix(git_buf *buf, const git_strarray *strings) +int git_buf_splice( + git_buf *buf, + size_t where, + size_t nb_to_remove, + const char *data, + size_t nb_to_insert) { - size_t i; - const char *str, *pfx; - - git_buf_clear(buf); - - if (!strings || !strings->count) - return 0; - - /* initialize common prefix to first string */ - if (git_buf_sets(buf, strings->strings[0]) < 0) + assert(buf && + where <= git_buf_len(buf) && + where + nb_to_remove <= git_buf_len(buf)); + + /* Ported from git.git + * https://github.com/git/git/blob/16eed7c/strbuf.c#L159-176 + */ + if (git_buf_grow(buf, git_buf_len(buf) + nb_to_insert - nb_to_remove) < 0) return -1; - /* go through the rest of the strings, truncating to shared prefix */ - for (i = 1; i < strings->count; ++i) { - - for (str = strings->strings[i], pfx = buf->ptr; - *str && *str == *pfx; str++, pfx++) - /* scanning */; + memmove(buf->ptr + where + nb_to_insert, + buf->ptr + where + nb_to_remove, + buf->size - where - nb_to_remove); - git_buf_truncate(buf, pfx - buf->ptr); - - if (!buf->size) - break; - } + memcpy(buf->ptr + where, data, nb_to_insert); + buf->size = buf->size + nb_to_insert - nb_to_remove; + buf->ptr[buf->size] = '\0'; return 0; } - -bool git_buf_is_binary(const git_buf *buf) -{ - size_t i; - int printable = 0, nonprintable = 0; - - for (i = 0; i < buf->size; i++) { - unsigned char c = buf->ptr[i]; - if (c > 0x1F && c < 0x7F) - printable++; - else if (c == '\0') - return true; - else if (!git__isspace(c)) - nonprintable++; - } - - return ((printable >> 7) < nonprintable); -} - diff --git a/src/buffer.h b/src/buffer.h index 50c75f64e..5402f3827 100644 --- a/src/buffer.h +++ b/src/buffer.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -8,6 +8,7 @@ #define INCLUDE_buffer_h__ #include "common.h" +#include "git2/strarray.h" #include <stdarg.h> typedef struct { @@ -26,30 +27,35 @@ extern char git_buf__oom[]; * For the cases where GIT_BUF_INIT cannot be used to do static * initialization. */ -void git_buf_init(git_buf *buf, size_t initial_size); +extern void git_buf_init(git_buf *buf, size_t initial_size); /** - * Grow the buffer to hold at least `target_size` bytes. + * Attempt to grow the buffer to hold at least `target_size` bytes. * - * If the allocation fails, this will return an error and the buffer - * will be marked as invalid for future operations. The existing - * contents of the buffer will be preserved however. - * @return 0 on success or -1 on failure + * If the allocation fails, this will return an error. If mark_oom is true, + * this will mark the buffer as invalid for future operations; if false, + * existing buffer content will be preserved, but calling code must handle + * that buffer was not expanded. */ -int git_buf_grow(git_buf *buf, size_t target_size); +extern int git_buf_try_grow(git_buf *buf, size_t target_size, bool mark_oom); /** - * Attempt to grow the buffer to hold at least `target_size` bytes. + * Grow the buffer to hold at least `target_size` bytes. + * + * If the allocation fails, this will return an error and the buffer will be + * marked as invalid for future operations, invaliding contents. * - * This is just like `git_buf_grow` except that even if the allocation - * fails, the git_buf will still be left in a valid state. + * @return 0 on success or -1 on failure */ -int git_buf_try_grow(git_buf *buf, size_t target_size); +GIT_INLINE(int) git_buf_grow(git_buf *buf, size_t target_size) +{ + return git_buf_try_grow(buf, target_size, true); +} -void git_buf_free(git_buf *buf); -void git_buf_swap(git_buf *buf_a, git_buf *buf_b); -char *git_buf_detach(git_buf *buf); -void git_buf_attach(git_buf *buf, char *ptr, size_t asize); +extern void git_buf_free(git_buf *buf); +extern void git_buf_swap(git_buf *buf_a, git_buf *buf_b); +extern char *git_buf_detach(git_buf *buf); +extern void git_buf_attach(git_buf *buf, char *ptr, size_t asize); /** * Test if there have been any reallocation failures with this git_buf. @@ -113,7 +119,7 @@ void git_buf_copy_cstr(char *data, size_t datasize, const git_buf *buf); #define git_buf_PUTS(buf, str) git_buf_put(buf, str, sizeof(str) - 1) -GIT_INLINE(ssize_t) git_buf_rfind_next(git_buf *buf, char ch) +GIT_INLINE(ssize_t) git_buf_rfind_next(const git_buf *buf, char ch) { ssize_t idx = (ssize_t)buf->size - 1; while (idx >= 0 && buf->ptr[idx] == ch) idx--; @@ -121,15 +127,50 @@ GIT_INLINE(ssize_t) git_buf_rfind_next(git_buf *buf, char ch) return idx; } +GIT_INLINE(ssize_t) git_buf_rfind(const git_buf *buf, char ch) +{ + ssize_t idx = (ssize_t)buf->size - 1; + while (idx >= 0 && buf->ptr[idx] != ch) idx--; + return idx; +} + +GIT_INLINE(ssize_t) git_buf_find(const git_buf *buf, char ch) +{ + void *found = memchr(buf->ptr, ch, buf->size); + return found ? (ssize_t)((const char *)found - buf->ptr) : -1; +} + /* Remove whitespace from the end of the buffer */ void git_buf_rtrim(git_buf *buf); int git_buf_cmp(const git_buf *a, const git_buf *b); -/* Fill buf with the common prefix of a array of strings */ -int git_buf_common_prefix(git_buf *buf, const git_strarray *strings); +/* Write data as base64 encoded in buffer */ +int git_buf_put_base64(git_buf *buf, const char *data, size_t len); -/* Check if buffer looks like it contains binary data */ -bool git_buf_is_binary(const git_buf *buf); +/* + * Insert, remove or replace a portion of the buffer. + * + * @param buf The buffer to work with + * + * @param where The location in the buffer where the transformation + * should be applied. + * + * @param nb_to_remove The number of chars to be removed. 0 to not + * remove any character in the buffer. + * + * @param data A pointer to the data which should be inserted. + * + * @param nb_to_insert The number of chars to be inserted. 0 to not + * insert any character from the buffer. + * + * @return 0 or an error code. + */ +int git_buf_splice( + git_buf *buf, + size_t where, + size_t nb_to_remove, + const char *data, + size_t nb_to_insert); #endif diff --git a/src/cache.c b/src/cache.c index 31da3c36e..e7f333577 100644 --- a/src/cache.c +++ b/src/cache.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -11,6 +11,7 @@ #include "thread-utils.h" #include "util.h" #include "cache.h" +#include "git2/oid.h" int git_cache_init(git_cache *cache, size_t size, git_cached_obj_freeptr free_ptr) { @@ -40,6 +41,7 @@ void git_cache_free(git_cache *cache) git_cached_obj_decref(cache->nodes[i], cache->free_obj); } + git_mutex_free(&cache->lock); git__free(cache->nodes); } @@ -50,7 +52,11 @@ void *git_cache_get(git_cache *cache, const git_oid *oid) memcpy(&hash, oid->id, sizeof(hash)); - git_mutex_lock(&cache->lock); + if (git_mutex_lock(&cache->lock)) { + giterr_set(GITERR_THREAD, "unable to lock cache mutex"); + return NULL; + } + { node = cache->nodes[hash & cache->size_mask]; @@ -69,16 +75,20 @@ void *git_cache_try_store(git_cache *cache, void *_entry) git_cached_obj *entry = _entry; uint32_t hash; - memcpy(&hash, &entry->oid, sizeof(hash)); + memcpy(&hash, &entry->oid, sizeof(uint32_t)); - /* increase the refcount on this object, because - * the cache now owns it */ - git_cached_obj_incref(entry); + if (git_mutex_lock(&cache->lock)) { + giterr_set(GITERR_THREAD, "unable to lock cache mutex"); + return NULL; + } - git_mutex_lock(&cache->lock); { git_cached_obj *node = cache->nodes[hash & cache->size_mask]; + /* increase the refcount on this object, because + * the cache now owns it */ + git_cached_obj_incref(entry); + if (node == NULL) { cache->nodes[hash & cache->size_mask] = entry; } else if (git_oid_cmp(&node->oid, &entry->oid) == 0) { @@ -88,12 +98,13 @@ void *git_cache_try_store(git_cache *cache, void *_entry) git_cached_obj_decref(node, cache->free_obj); cache->nodes[hash & cache->size_mask] = entry; } + + /* increase the refcount again, because we are + * returning it to the user */ + git_cached_obj_incref(entry); + } git_mutex_unlock(&cache->lock); - /* increase the refcount again, because we are - * returning it to the user */ - git_cached_obj_incref(entry); - return entry; } diff --git a/src/cache.h b/src/cache.h index 6dc706897..7034ec268 100644 --- a/src/cache.h +++ b/src/cache.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. diff --git a/src/cc-compat.h b/src/cc-compat.h index 9f23dcae2..a5e4ce17e 100644 --- a/src/cc-compat.h +++ b/src/cc-compat.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -38,8 +38,10 @@ /* Define the printf format specifer to use for size_t output */ #if defined(_MSC_VER) || defined(__MINGW32__) # define PRIuZ "Iu" +# define PRIxZ "Ix" #else # define PRIuZ "zu" +# define PRIxZ "zx" #endif /* Micosoft Visual C/C++ */ diff --git a/src/checkout.c b/src/checkout.c new file mode 100644 index 000000000..24fa21024 --- /dev/null +++ b/src/checkout.c @@ -0,0 +1,1383 @@ +/* + * 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 <assert.h> + +#include "checkout.h" + +#include "git2/repository.h" +#include "git2/refs.h" +#include "git2/tree.h" +#include "git2/blob.h" +#include "git2/config.h" +#include "git2/diff.h" +#include "git2/submodule.h" + +#include "refs.h" +#include "repository.h" +#include "filter.h" +#include "blob.h" +#include "diff.h" +#include "pathspec.h" +#include "buf_text.h" + +/* See docs/checkout-internals.md for more information */ + +enum { + CHECKOUT_ACTION__NONE = 0, + CHECKOUT_ACTION__REMOVE = 1, + CHECKOUT_ACTION__UPDATE_BLOB = 2, + CHECKOUT_ACTION__UPDATE_SUBMODULE = 4, + CHECKOUT_ACTION__CONFLICT = 8, + CHECKOUT_ACTION__MAX = 8, + CHECKOUT_ACTION__DEFER_REMOVE = 16, + CHECKOUT_ACTION__REMOVE_AND_UPDATE = + (CHECKOUT_ACTION__UPDATE_BLOB | CHECKOUT_ACTION__REMOVE), +}; + +typedef struct { + git_repository *repo; + git_diff_list *diff; + git_checkout_opts opts; + bool opts_free_baseline; + char *pfx; + git_index *index; + git_pool pool; + git_vector removes; + git_buf path; + size_t workdir_len; + unsigned int strategy; + int can_symlink; + bool reload_submodules; + size_t total_steps; + size_t completed_steps; +} checkout_data; + +static int checkout_notify( + checkout_data *data, + git_checkout_notify_t why, + const git_diff_delta *delta, + const git_index_entry *wditem) +{ + git_diff_file wdfile; + const git_diff_file *baseline = NULL, *target = NULL, *workdir = NULL; + const char *path = NULL; + + if (!data->opts.notify_cb) + return 0; + + if ((why & data->opts.notify_flags) == 0) + return 0; + + if (wditem) { + memset(&wdfile, 0, sizeof(wdfile)); + + git_oid_cpy(&wdfile.oid, &wditem->oid); + wdfile.path = wditem->path; + wdfile.size = wditem->file_size; + wdfile.flags = GIT_DIFF_FLAG_VALID_OID; + wdfile.mode = wditem->mode; + + workdir = &wdfile; + + path = wditem->path; + } + + if (delta) { + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: + case GIT_DELTA_MODIFIED: + case GIT_DELTA_TYPECHANGE: + default: + baseline = &delta->old_file; + target = &delta->new_file; + break; + case GIT_DELTA_ADDED: + case GIT_DELTA_IGNORED: + case GIT_DELTA_UNTRACKED: + target = &delta->new_file; + break; + case GIT_DELTA_DELETED: + baseline = &delta->old_file; + break; + } + + path = delta->old_file.path; + } + + return data->opts.notify_cb( + why, path, baseline, target, workdir, data->opts.notify_payload); +} + +static bool checkout_is_workdir_modified( + checkout_data *data, + const git_diff_file *baseitem, + const git_index_entry *wditem) +{ + git_oid oid; + + /* handle "modified" submodule */ + if (wditem->mode == GIT_FILEMODE_COMMIT) { + git_submodule *sm; + unsigned int sm_status = 0; + const git_oid *sm_oid = NULL; + + if (git_submodule_lookup(&sm, data->repo, wditem->path) < 0 || + git_submodule_status(&sm_status, sm) < 0) + return true; + + if (GIT_SUBMODULE_STATUS_IS_WD_DIRTY(sm_status)) + return true; + + sm_oid = git_submodule_wd_id(sm); + if (!sm_oid) + return false; + + return (git_oid_cmp(&baseitem->oid, sm_oid) != 0); + } + + /* depending on where base is coming from, we may or may not know + * the actual size of the data, so we can't rely on this shortcut. + */ + if (baseitem->size && wditem->file_size != baseitem->size) + return true; + + if (git_diff__oid_for_file( + data->repo, wditem->path, wditem->mode, + wditem->file_size, &oid) < 0) + return false; + + return (git_oid_cmp(&baseitem->oid, &oid) != 0); +} + +#define CHECKOUT_ACTION_IF(FLAG,YES,NO) \ + ((data->strategy & GIT_CHECKOUT_##FLAG) ? CHECKOUT_ACTION__##YES : CHECKOUT_ACTION__##NO) + +static int checkout_action_common( + checkout_data *data, + int action, + const git_diff_delta *delta, + const git_index_entry *wd) +{ + git_checkout_notify_t notify = GIT_CHECKOUT_NOTIFY_NONE; + + if (action <= 0) + return action; + + if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) + action = (action & ~CHECKOUT_ACTION__REMOVE); + + if ((action & CHECKOUT_ACTION__UPDATE_BLOB) != 0) { + if (S_ISGITLINK(delta->new_file.mode)) + action = (action & ~CHECKOUT_ACTION__UPDATE_BLOB) | + CHECKOUT_ACTION__UPDATE_SUBMODULE; + + notify = GIT_CHECKOUT_NOTIFY_UPDATED; + } + + if ((action & CHECKOUT_ACTION__CONFLICT) != 0) + notify = GIT_CHECKOUT_NOTIFY_CONFLICT; + + if (notify != GIT_CHECKOUT_NOTIFY_NONE && + checkout_notify(data, notify, delta, wd) != 0) + return GIT_EUSER; + + return action; +} + +static int checkout_action_no_wd( + checkout_data *data, + const git_diff_delta *delta) +{ + int action = CHECKOUT_ACTION__NONE; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: /* case 12 */ + if (checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, NULL)) + return GIT_EUSER; + action = CHECKOUT_ACTION_IF(SAFE_CREATE, UPDATE_BLOB, NONE); + break; + case GIT_DELTA_ADDED: /* case 2 or 28 (and 5 but not really) */ + case GIT_DELTA_MODIFIED: /* case 13 (and 35 but not really) */ + action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + break; + case GIT_DELTA_TYPECHANGE: /* case 21 (B->T) and 28 (T->B)*/ + if (delta->new_file.mode == GIT_FILEMODE_TREE) + action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + break; + case GIT_DELTA_DELETED: /* case 8 or 25 */ + default: /* impossible */ + break; + } + + return checkout_action_common(data, action, delta, NULL); +} + +static int checkout_action_wd_only( + checkout_data *data, + git_iterator *workdir, + const git_index_entry *wd, + git_vector *pathspec) +{ + bool remove = false; + git_checkout_notify_t notify = GIT_CHECKOUT_NOTIFY_NONE; + + if (!git_pathspec_match_path( + pathspec, wd->path, + (data->strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH) != 0, + git_iterator_ignore_case(workdir), NULL)) + return 0; + + /* check if item is tracked in the index but not in the checkout diff */ + if (data->index != NULL) { + if (wd->mode != GIT_FILEMODE_TREE) { + int error; + + if ((error = git_index_find(NULL, data->index, wd->path)) == 0) { + notify = GIT_CHECKOUT_NOTIFY_DIRTY; + remove = ((data->strategy & GIT_CHECKOUT_FORCE) != 0); + } else if (error != GIT_ENOTFOUND) + return error; + } else { + /* for tree entries, we have to see if there are any index + * entries that are contained inside that tree + */ + size_t pos = git_index__prefix_position(data->index, wd->path); + const git_index_entry *e = git_index_get_byindex(data->index, pos); + + if (e != NULL && data->diff->pfxcomp(e->path, wd->path) == 0) { + notify = GIT_CHECKOUT_NOTIFY_DIRTY; + remove = ((data->strategy & GIT_CHECKOUT_FORCE) != 0); + } + } + } + + if (notify != GIT_CHECKOUT_NOTIFY_NONE) + /* found in index */; + else if (git_iterator_current_is_ignored(workdir)) { + notify = GIT_CHECKOUT_NOTIFY_IGNORED; + remove = ((data->strategy & GIT_CHECKOUT_REMOVE_IGNORED) != 0); + } + else { + notify = GIT_CHECKOUT_NOTIFY_UNTRACKED; + remove = ((data->strategy & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0); + } + + if (checkout_notify(data, notify, NULL, wd)) + return GIT_EUSER; + + if (remove) { + char *path = git_pool_strdup(&data->pool, wd->path); + GITERR_CHECK_ALLOC(path); + + if (git_vector_insert(&data->removes, path) < 0) + return -1; + } + + return 0; +} + +static bool submodule_is_config_only( + checkout_data *data, + const char *path) +{ + git_submodule *sm = NULL; + unsigned int sm_loc = 0; + + if (git_submodule_lookup(&sm, data->repo, path) < 0 || + git_submodule_location(&sm_loc, sm) < 0 || + sm_loc == GIT_SUBMODULE_STATUS_IN_CONFIG) + return true; + + return false; +} + +static int checkout_action_with_wd( + checkout_data *data, + const git_diff_delta *delta, + const git_index_entry *wd) +{ + int action = CHECKOUT_ACTION__NONE; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: /* case 14/15 or 33 */ + if (checkout_is_workdir_modified(data, &delta->old_file, wd)) { + if (checkout_notify( + data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, wd)) + return GIT_EUSER; + action = CHECKOUT_ACTION_IF(FORCE, UPDATE_BLOB, NONE); + } + break; + case GIT_DELTA_ADDED: /* case 3, 4 or 6 */ + action = CHECKOUT_ACTION_IF(FORCE, UPDATE_BLOB, CONFLICT); + break; + case GIT_DELTA_DELETED: /* case 9 or 10 (or 26 but not really) */ + if (checkout_is_workdir_modified(data, &delta->old_file, wd)) + action = CHECKOUT_ACTION_IF(FORCE, REMOVE, CONFLICT); + else + action = CHECKOUT_ACTION_IF(SAFE, REMOVE, NONE); + break; + case GIT_DELTA_MODIFIED: /* case 16, 17, 18 (or 36 but not really) */ + if (checkout_is_workdir_modified(data, &delta->old_file, wd)) + action = CHECKOUT_ACTION_IF(FORCE, UPDATE_BLOB, CONFLICT); + else + action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + break; + case GIT_DELTA_TYPECHANGE: /* case 22, 23, 29, 30 */ + if (delta->old_file.mode == GIT_FILEMODE_TREE) { + if (wd->mode == GIT_FILEMODE_TREE) + /* either deleting items in old tree will delete the wd dir, + * or we'll get a conflict when we attempt blob update... + */ + action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + else if (wd->mode == GIT_FILEMODE_COMMIT) { + /* workdir is possibly a "phantom" submodule - treat as a + * tree if the only submodule info came from the config + */ + if (submodule_is_config_only(data, wd->path)) + action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + else + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + } else + action = CHECKOUT_ACTION_IF(FORCE, REMOVE, CONFLICT); + } + else if (checkout_is_workdir_modified(data, &delta->old_file, wd)) + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + else + action = CHECKOUT_ACTION_IF(SAFE, REMOVE_AND_UPDATE, NONE); + + /* don't update if the typechange is to a tree */ + if (delta->new_file.mode == GIT_FILEMODE_TREE) + action = (action & ~CHECKOUT_ACTION__UPDATE_BLOB); + break; + default: /* impossible */ + break; + } + + return checkout_action_common(data, action, delta, wd); +} + +static int checkout_action_with_wd_blocker( + checkout_data *data, + const git_diff_delta *delta, + const git_index_entry *wd) +{ + int action = CHECKOUT_ACTION__NONE; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: + /* should show delta as dirty / deleted */ + if (checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, wd)) + return GIT_EUSER; + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, NONE); + break; + case GIT_DELTA_ADDED: + case GIT_DELTA_MODIFIED: + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + break; + case GIT_DELTA_DELETED: + action = CHECKOUT_ACTION_IF(FORCE, REMOVE, CONFLICT); + break; + case GIT_DELTA_TYPECHANGE: + /* not 100% certain about this... */ + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + break; + default: /* impossible */ + break; + } + + return checkout_action_common(data, action, delta, wd); +} + +static int checkout_action_with_wd_dir( + checkout_data *data, + const git_diff_delta *delta, + const git_index_entry *wd) +{ + int action = CHECKOUT_ACTION__NONE; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: /* case 19 or 24 (or 34 but not really) */ + if (checkout_notify(data, GIT_CHECKOUT_NOTIFY_DIRTY, delta, NULL) || + checkout_notify( + data, GIT_CHECKOUT_NOTIFY_UNTRACKED, NULL, wd)) + return GIT_EUSER; + break; + case GIT_DELTA_ADDED:/* case 4 (and 7 for dir) */ + case GIT_DELTA_MODIFIED: /* case 20 (or 37 but not really) */ + if (delta->old_file.mode == GIT_FILEMODE_COMMIT) + /* expected submodule (and maybe found one) */; + else if (delta->new_file.mode != GIT_FILEMODE_TREE) + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + break; + case GIT_DELTA_DELETED: /* case 11 (and 27 for dir) */ + if (delta->old_file.mode != GIT_FILEMODE_TREE && + checkout_notify( + data, GIT_CHECKOUT_NOTIFY_UNTRACKED, NULL, wd)) + return GIT_EUSER; + break; + case GIT_DELTA_TYPECHANGE: /* case 24 or 31 */ + if (delta->old_file.mode == GIT_FILEMODE_TREE) { + /* For typechange from dir, remove dir and add blob, but it is + * not safe to remove dir if it contains modified files. + * However, safely removing child files will remove the parent + * directory if is it left empty, so we can defer removing the + * dir and it will succeed if no children are left. + */ + action = CHECKOUT_ACTION_IF(SAFE, UPDATE_BLOB, NONE); + if (action != CHECKOUT_ACTION__NONE) + action |= CHECKOUT_ACTION__DEFER_REMOVE; + } + else if (delta->new_file.mode != GIT_FILEMODE_TREE) + /* For typechange to dir, dir is already created so no action */ + action = CHECKOUT_ACTION_IF(FORCE, REMOVE_AND_UPDATE, CONFLICT); + break; + default: /* impossible */ + break; + } + + return checkout_action_common(data, action, delta, wd); +} + +static int checkout_action( + checkout_data *data, + git_diff_delta *delta, + git_iterator *workdir, + const git_index_entry **wditem_ptr, + git_vector *pathspec) +{ + const git_index_entry *wd = *wditem_ptr; + int cmp = -1, act; + int (*strcomp)(const char *, const char *) = data->diff->strcomp; + int (*pfxcomp)(const char *str, const char *pfx) = data->diff->pfxcomp; + + /* move workdir iterator to follow along with deltas */ + + while (1) { + if (!wd) + return checkout_action_no_wd(data, delta); + + cmp = strcomp(wd->path, delta->old_file.path); + + /* 1. wd before delta ("a/a" before "a/b") + * 2. wd prefixes delta & should expand ("a/" before "a/b") + * 3. wd prefixes delta & cannot expand ("a/b" before "a/b/c") + * 4. wd equals delta ("a/b" and "a/b") + * 5. wd after delta & delta prefixes wd ("a/b/c" after "a/b/" or "a/b") + * 6. wd after delta ("a/c" after "a/b") + */ + + if (cmp < 0) { + cmp = pfxcomp(delta->old_file.path, wd->path); + + if (cmp == 0) { + if (wd->mode == GIT_FILEMODE_TREE) { + /* case 2 - entry prefixed by workdir tree */ + if (git_iterator_advance_into(&wd, workdir) < 0) + goto fail; + + *wditem_ptr = wd; + continue; + } + + /* case 3 maybe - wd contains non-dir where dir expected */ + if (delta->old_file.path[strlen(wd->path)] == '/') { + act = checkout_action_with_wd_blocker(data, delta, wd); + *wditem_ptr = + git_iterator_advance(&wd, workdir) ? NULL : wd; + return act; + } + } + + /* case 1 - handle wd item (if it matches pathspec) */ + if (checkout_action_wd_only(data, workdir, wd, pathspec) < 0 || + git_iterator_advance(&wd, workdir) < 0) + goto fail; + + *wditem_ptr = wd; + continue; + } + + if (cmp == 0) { + /* case 4 */ + act = checkout_action_with_wd(data, delta, wd); + *wditem_ptr = git_iterator_advance(&wd, workdir) ? NULL : wd; + return act; + } + + cmp = pfxcomp(wd->path, delta->old_file.path); + + if (cmp == 0) { /* case 5 */ + if (wd->path[strlen(delta->old_file.path)] != '/') + return checkout_action_no_wd(data, delta); + + if (delta->status == GIT_DELTA_TYPECHANGE) { + if (delta->old_file.mode == GIT_FILEMODE_TREE) { + act = checkout_action_with_wd(data, delta, wd); + if (git_iterator_advance_into(&wd, workdir) < 0) + wd = NULL; + *wditem_ptr = wd; + return act; + } + + if (delta->new_file.mode == GIT_FILEMODE_TREE || + delta->new_file.mode == GIT_FILEMODE_COMMIT || + delta->old_file.mode == GIT_FILEMODE_COMMIT) + { + act = checkout_action_with_wd(data, delta, wd); + if (git_iterator_advance(&wd, workdir) < 0) + wd = NULL; + *wditem_ptr = wd; + return act; + } + } + + return checkout_action_with_wd_dir(data, delta, wd); + } + + /* case 6 - wd is after delta */ + return checkout_action_no_wd(data, delta); + } + +fail: + *wditem_ptr = NULL; + return -1; +} + +static int checkout_remaining_wd_items( + checkout_data *data, + git_iterator *workdir, + const git_index_entry *wd, + git_vector *spec) +{ + int error = 0; + + while (wd && !error) { + if (!(error = checkout_action_wd_only(data, workdir, wd, spec))) + error = git_iterator_advance(&wd, workdir); + } + + return error; +} + +static int checkout_get_actions( + uint32_t **actions_ptr, + size_t **counts_ptr, + checkout_data *data, + git_iterator *workdir) +{ + int error = 0; + const git_index_entry *wditem; + git_vector pathspec = GIT_VECTOR_INIT, *deltas; + git_pool pathpool = GIT_POOL_INIT_STRINGPOOL; + git_diff_delta *delta; + size_t i, *counts = NULL; + uint32_t *actions = NULL; + + if (data->opts.paths.count > 0 && + git_pathspec_init(&pathspec, &data->opts.paths, &pathpool) < 0) + return -1; + + if ((error = git_iterator_current(&wditem, workdir)) < 0) + goto fail; + + deltas = &data->diff->deltas; + + *counts_ptr = counts = git__calloc(CHECKOUT_ACTION__MAX+1, sizeof(size_t)); + *actions_ptr = actions = git__calloc( + deltas->length ? deltas->length : 1, sizeof(uint32_t)); + if (!counts || !actions) { + error = -1; + goto fail; + } + + git_vector_foreach(deltas, i, delta) { + int act = checkout_action(data, delta, workdir, &wditem, &pathspec); + + if (act < 0) { + error = act; + goto fail; + } + + actions[i] = act; + + if (act & CHECKOUT_ACTION__REMOVE) + counts[CHECKOUT_ACTION__REMOVE]++; + if (act & CHECKOUT_ACTION__UPDATE_BLOB) + counts[CHECKOUT_ACTION__UPDATE_BLOB]++; + if (act & CHECKOUT_ACTION__UPDATE_SUBMODULE) + counts[CHECKOUT_ACTION__UPDATE_SUBMODULE]++; + if (act & CHECKOUT_ACTION__CONFLICT) + counts[CHECKOUT_ACTION__CONFLICT]++; + } + + error = checkout_remaining_wd_items(data, workdir, wditem, &pathspec); + if (error < 0) + goto fail; + + counts[CHECKOUT_ACTION__REMOVE] += data->removes.length; + + if (counts[CHECKOUT_ACTION__CONFLICT] > 0 && + (data->strategy & GIT_CHECKOUT_ALLOW_CONFLICTS) == 0) + { + giterr_set(GITERR_CHECKOUT, "%d conflicts prevent checkout", + (int)counts[CHECKOUT_ACTION__CONFLICT]); + error = GIT_EMERGECONFLICT; + goto fail; + } + + git_pathspec_free(&pathspec); + git_pool_clear(&pathpool); + + return 0; + +fail: + *counts_ptr = NULL; + git__free(counts); + *actions_ptr = NULL; + git__free(actions); + + git_pathspec_free(&pathspec); + git_pool_clear(&pathpool); + + return error; +} + +static int buffer_to_file( + struct stat *st, + git_buf *buffer, + const char *path, + mode_t dir_mode, + int file_open_flags, + mode_t file_mode) +{ + int fd, 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 = p_stat(path, st)) < 0) + giterr_set(GITERR_OS, "Error while statting '%s'", path); + } + + if (!error && + (file_mode & 0100) != 0 && + (error = p_chmod(path, file_mode)) < 0) + giterr_set(GITERR_OS, "Failed to set permissions on '%s'", path); + + return error; +} + +static int blob_content_to_file( + struct stat *st, + git_blob *blob, + const char *path, + mode_t entry_filemode, + git_checkout_opts *opts) +{ + int error = -1, nb_filters = 0; + mode_t file_mode = opts->file_mode; + bool dont_free_filtered; + git_buf unfiltered = GIT_BUF_INIT, filtered = GIT_BUF_INIT; + 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; + /* ... and make sure it doesn't get unexpectedly freed */ + dont_free_filtered = true; + + if (!opts->disable_filters && + !git_buf_text_is_binary(&filtered) && + (nb_filters = git_filters_load( + &filters, + git_object_owner((git_object *)blob), + path, + GIT_FILTER_TO_WORKTREE)) > 0) + { + /* reset 'filtered' so it can be a filter target */ + git_buf_init(&filtered, 0); + dont_free_filtered = false; + } + + if (nb_filters < 0) + return nb_filters; + + if (nb_filters > 0) { + if ((error = git_blob__getbuf(&unfiltered, blob)) < 0) + goto cleanup; + + if ((error = git_filters_apply(&filtered, &unfiltered, &filters)) < 0) + goto cleanup; + } + + /* Allow overriding of file mode */ + if (!file_mode) + file_mode = entry_filemode; + + error = buffer_to_file( + st, &filtered, path, opts->dir_mode, opts->file_open_flags, file_mode); + + if (!error) + st->st_mode = entry_filemode; + +cleanup: + git_filters_free(&filters); + git_buf_free(&unfiltered); + if (!dont_free_filtered) + git_buf_free(&filtered); + + return error; +} + +static int blob_content_to_link( + struct stat *st, git_blob *blob, const char *path, int can_symlink) +{ + git_buf linktarget = GIT_BUF_INIT; + int 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); + } else { + error = git_futils_fake_symlink(git_buf_cstr(&linktarget), path); + } + + if (!error) { + if ((error = p_lstat(path, st)) < 0) + giterr_set(GITERR_CHECKOUT, "Could not stat symlink %s", path); + + st->st_mode = GIT_FILEMODE_LINK; + } + + git_buf_free(&linktarget); + + return error; +} + +static int checkout_update_index( + checkout_data *data, + const git_diff_file *file, + struct stat *st) +{ + git_index_entry entry; + + if (!data->index) + return 0; + + memset(&entry, 0, sizeof(entry)); + entry.path = (char *)file->path; /* cast to prevent warning */ + git_index_entry__init_from_stat(&entry, st); + git_oid_cpy(&entry.oid, &file->oid); + + return git_index_add(data->index, &entry); +} + +static int checkout_submodule( + checkout_data *data, + const git_diff_file *file) +{ + int error = 0; + git_submodule *sm; + + /* Until submodules are supported, UPDATE_ONLY means do nothing here */ + if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) + return 0; + + if ((error = git_futils_mkdir( + file->path, git_repository_workdir(data->repo), + data->opts.dir_mode, GIT_MKDIR_PATH)) < 0) + return error; + + if ((error = git_submodule_lookup(&sm, data->repo, file->path)) < 0) + return error; + + /* TODO: Support checkout_strategy options. Two circumstances: + * 1 - submodule already checked out, but we need to move the HEAD + * to the new OID, or + * 2 - submodule not checked out and we should recursively check it out + * + * Checkout will not execute a pull on the submodule, but a clone + * command should probably be able to. Do we need a submodule callback? + */ + + /* 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; +} + +static void report_progress( + checkout_data *data, + const char *path) +{ + if (data->opts.progress_cb) + data->opts.progress_cb( + path, data->completed_steps, data->total_steps, + data->opts.progress_payload); +} + +static int checkout_safe_for_update_only(const char *path, mode_t expected_mode) +{ + struct stat st; + + if (p_lstat(path, &st) < 0) { + /* if doesn't exist, then no error and no update */ + if (errno == ENOENT || errno == ENOTDIR) + return 0; + + /* otherwise, stat error and no update */ + giterr_set(GITERR_OS, "Failed to stat file '%s'", path); + return -1; + } + + /* only safe for update if this is the same type of file */ + if ((st.st_mode & ~0777) == (expected_mode & ~0777)) + return 1; + + return 0; +} + +static int checkout_blob( + checkout_data *data, + const git_diff_file *file) +{ + int error = 0; + git_blob *blob; + struct stat st; + + git_buf_truncate(&data->path, data->workdir_len); + if (git_buf_puts(&data->path, file->path) < 0) + return -1; + + if ((data->strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0) { + int rval = checkout_safe_for_update_only( + git_buf_cstr(&data->path), file->mode); + if (rval <= 0) + return rval; + } + + if ((error = git_blob_lookup(&blob, data->repo, &file->oid)) < 0) + return error; + + if (S_ISLNK(file->mode)) + error = blob_content_to_link( + &st, blob, git_buf_cstr(&data->path), data->can_symlink); + else + error = blob_content_to_file( + &st, blob, git_buf_cstr(&data->path), file->mode, &data->opts); + + git_blob_free(blob); + + /* if we try to create the blob and an existing directory blocks it from + * being written, then there must have been a typechange conflict in a + * parent directory - suppress the error and try to continue. + */ + if ((data->strategy & GIT_CHECKOUT_ALLOW_CONFLICTS) != 0 && + (error == GIT_ENOTFOUND || error == GIT_EEXISTS)) + { + giterr_clear(); + error = 0; + } + + /* update the index unless prevented */ + if (!error && (data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0) + error = checkout_update_index(data, file, &st); + + /* update the submodule data if this was a new .gitmodules file */ + if (!error && strcmp(file->path, ".gitmodules") == 0) + data->reload_submodules = true; + + return error; +} + +static int checkout_remove_the_old( + unsigned int *actions, + checkout_data *data) +{ + int error = 0; + git_diff_delta *delta; + const char *str; + size_t i; + const char *workdir = git_buf_cstr(&data->path); + uint32_t flg = GIT_RMDIR_EMPTY_PARENTS | + GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_REMOVE_BLOCKERS; + + git_buf_truncate(&data->path, data->workdir_len); + + git_vector_foreach(&data->diff->deltas, i, delta) { + if (actions[i] & CHECKOUT_ACTION__REMOVE) { + error = git_futils_rmdir_r(delta->old_file.path, workdir, flg); + if (error < 0) + return error; + + data->completed_steps++; + report_progress(data, delta->old_file.path); + + if ((actions[i] & CHECKOUT_ACTION__UPDATE_BLOB) == 0 && + (data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0 && + data->index != NULL) + { + (void)git_index_remove(data->index, delta->old_file.path, 0); + } + } + } + + git_vector_foreach(&data->removes, i, str) { + error = git_futils_rmdir_r(str, workdir, flg); + if (error < 0) + return error; + + data->completed_steps++; + report_progress(data, str); + + if ((data->strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0 && + data->index != NULL) + { + if (str[strlen(str) - 1] == '/') + (void)git_index_remove_directory(data->index, str, 0); + else + (void)git_index_remove(data->index, str, 0); + } + } + + return 0; +} + +static int checkout_deferred_remove(git_repository *repo, const char *path) +{ +#if 0 + int error = git_futils_rmdir_r( + path, git_repository_workdir(repo), GIT_RMDIR_EMPTY_PARENTS); + + if (error == GIT_ENOTFOUND) { + error = 0; + giterr_clear(); + } + + return error; +#else + GIT_UNUSED(repo); + GIT_UNUSED(path); + assert(false); + return 0; +#endif +} + +static int checkout_create_the_new( + unsigned int *actions, + checkout_data *data) +{ + int error = 0; + git_diff_delta *delta; + size_t i; + + git_vector_foreach(&data->diff->deltas, i, delta) { + if (actions[i] & CHECKOUT_ACTION__DEFER_REMOVE) { + /* this had a blocker directory that should only be removed iff + * all of the contents of the directory were safely removed + */ + if ((error = checkout_deferred_remove( + data->repo, delta->old_file.path)) < 0) + return error; + } + + if (actions[i] & CHECKOUT_ACTION__UPDATE_BLOB) { + error = checkout_blob(data, &delta->new_file); + if (error < 0) + return error; + + data->completed_steps++; + report_progress(data, delta->new_file.path); + } + } + + return 0; +} + +static int checkout_create_submodules( + unsigned int *actions, + checkout_data *data) +{ + int error = 0; + git_diff_delta *delta; + size_t i; + + /* initial reload of submodules if .gitmodules was changed */ + if (data->reload_submodules && + (error = git_submodule_reload_all(data->repo)) < 0) + return error; + + git_vector_foreach(&data->diff->deltas, i, delta) { + if (actions[i] & CHECKOUT_ACTION__DEFER_REMOVE) { + /* this has a blocker directory that should only be removed iff + * all of the contents of the directory were safely removed + */ + if ((error = checkout_deferred_remove( + data->repo, delta->old_file.path)) < 0) + return error; + } + + if (actions[i] & CHECKOUT_ACTION__UPDATE_SUBMODULE) { + int error = checkout_submodule(data, &delta->new_file); + if (error < 0) + return error; + + data->completed_steps++; + report_progress(data, delta->new_file.path); + } + } + + /* final reload once submodules have been updated */ + return git_submodule_reload_all(data->repo); +} + +static int checkout_lookup_head_tree(git_tree **out, git_repository *repo) +{ + int error = 0; + git_reference *ref = NULL; + git_object *head; + + if (!(error = git_repository_head(&ref, repo)) && + !(error = git_reference_peel(&head, ref, GIT_OBJ_TREE))) + *out = (git_tree *)head; + + git_reference_free(ref); + + return error; +} + +static void checkout_data_clear(checkout_data *data) +{ + if (data->opts_free_baseline) { + git_tree_free(data->opts.baseline); + data->opts.baseline = NULL; + } + + git_vector_free(&data->removes); + git_pool_clear(&data->pool); + + git__free(data->pfx); + data->pfx = NULL; + + git_buf_free(&data->path); + + git_index_free(data->index); + data->index = NULL; +} + +static int checkout_data_init( + checkout_data *data, + git_iterator *target, + git_checkout_opts *proposed) +{ + int error = 0; + git_config *cfg; + git_repository *repo = git_iterator_owner(target); + + memset(data, 0, sizeof(*data)); + + if (!repo) { + giterr_set(GITERR_CHECKOUT, "Cannot checkout nothing"); + return -1; + } + + if ((error = git_repository__ensure_not_bare(repo, "checkout")) < 0) + return error; + + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + return error; + + data->repo = repo; + + GITERR_CHECK_VERSION( + proposed, GIT_CHECKOUT_OPTS_VERSION, "git_checkout_opts"); + + if (!proposed) + GIT_INIT_STRUCTURE(&data->opts, GIT_CHECKOUT_OPTS_VERSION); + else + memmove(&data->opts, proposed, sizeof(git_checkout_opts)); + + /* refresh config and index content unless NO_REFRESH is given */ + if ((data->opts.checkout_strategy & GIT_CHECKOUT_NO_REFRESH) == 0) { + if ((error = git_config_refresh(cfg)) < 0) + goto cleanup; + + /* if we are checking out the index, don't reload, + * otherwise get index and force reload + */ + if ((data->index = git_iterator_get_index(target)) != NULL) { + GIT_REFCOUNT_INC(data->index); + } else { + /* otherwise, grab and reload the index */ + if ((error = git_repository_index(&data->index, data->repo)) < 0 || + (error = git_index_read(data->index)) < 0) + goto cleanup; + + /* clear the REUC when doing a tree or commit checkout */ + git_index_reuc_clear(data->index); + } + } + + /* if you are forcing, definitely allow safe updates */ + if ((data->opts.checkout_strategy & GIT_CHECKOUT_FORCE) != 0) + data->opts.checkout_strategy |= GIT_CHECKOUT_SAFE_CREATE; + if ((data->opts.checkout_strategy & GIT_CHECKOUT_SAFE_CREATE) != 0) + data->opts.checkout_strategy |= GIT_CHECKOUT_SAFE; + + data->strategy = data->opts.checkout_strategy; + + /* opts->disable_filters is false by default */ + + if (!data->opts.dir_mode) + data->opts.dir_mode = GIT_DIR_MODE; + + if (!data->opts.file_open_flags) + data->opts.file_open_flags = O_CREAT | O_TRUNC | O_WRONLY; + + data->pfx = git_pathspec_prefix(&data->opts.paths); + + error = git_config_get_bool(&data->can_symlink, cfg, "core.symlinks"); + if (error < 0) { + if (error != GIT_ENOTFOUND) + goto cleanup; + + /* If "core.symlinks" is not found anywhere, default to true. */ + data->can_symlink = true; + giterr_clear(); + error = 0; + } + + if (!data->opts.baseline) { + data->opts_free_baseline = true; + error = checkout_lookup_head_tree(&data->opts.baseline, repo); + + if (error == GIT_EORPHANEDHEAD) { + error = 0; + giterr_clear(); + } + + if (error < 0) + goto cleanup; + } + + if ((error = git_vector_init(&data->removes, 0, git__strcmp_cb)) < 0 || + (error = git_pool_init(&data->pool, 1, 0)) < 0 || + (error = git_buf_puts(&data->path, git_repository_workdir(repo))) < 0) + goto cleanup; + + data->workdir_len = git_buf_len(&data->path); + +cleanup: + if (error < 0) + checkout_data_clear(data); + + return error; +} + +int git_checkout_iterator( + git_iterator *target, + git_checkout_opts *opts) +{ + int error = 0; + git_iterator *baseline = NULL, *workdir = NULL; + checkout_data data = {0}; + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + uint32_t *actions = NULL; + size_t *counts = NULL; + git_iterator_flag_t iterflags = 0; + + /* initialize structures and options */ + error = checkout_data_init(&data, target, opts); + if (error < 0) + return error; + + diff_opts.flags = + GIT_DIFF_INCLUDE_UNMODIFIED | + GIT_DIFF_INCLUDE_UNTRACKED | + GIT_DIFF_RECURSE_UNTRACKED_DIRS | /* needed to match baseline */ + GIT_DIFF_INCLUDE_IGNORED | + GIT_DIFF_INCLUDE_TYPECHANGE | + GIT_DIFF_INCLUDE_TYPECHANGE_TREES | + GIT_DIFF_SKIP_BINARY_CHECK; + if (data.opts.checkout_strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH) + diff_opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH; + if (data.opts.paths.count > 0) + diff_opts.pathspec = data.opts.paths; + + /* set up iterators */ + + iterflags = git_iterator_ignore_case(target) ? + 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, + data.pfx, data.pfx)) < 0 || + (error = git_iterator_for_tree( + &baseline, data.opts.baseline, iterflags, data.pfx, data.pfx)) < 0) + goto cleanup; + + /* Should not have case insensitivity mismatch */ + assert(git_iterator_ignore_case(workdir) == git_iterator_ignore_case(baseline)); + + /* Generate baseline-to-target diff which will include an entry for + * every possible update that might need to be made. + */ + if ((error = git_diff__from_iterators( + &data.diff, data.repo, baseline, target, &diff_opts)) < 0) + goto cleanup; + + /* Loop through diff (and working directory iterator) building a list of + * actions to be taken, plus look for conflicts and send notifications. + */ + if ((error = checkout_get_actions(&actions, &counts, &data, workdir)) < 0) + goto cleanup; + + data.total_steps = counts[CHECKOUT_ACTION__REMOVE] + + counts[CHECKOUT_ACTION__UPDATE_BLOB] + + counts[CHECKOUT_ACTION__UPDATE_SUBMODULE]; + + report_progress(&data, NULL); /* establish 0 baseline */ + + /* To deal with some order dependencies, perform remaining checkout + * in three passes: removes, then update blobs, then update submodules. + */ + if (counts[CHECKOUT_ACTION__REMOVE] > 0 && + (error = checkout_remove_the_old(actions, &data)) < 0) + goto cleanup; + + if (counts[CHECKOUT_ACTION__UPDATE_BLOB] > 0 && + (error = checkout_create_the_new(actions, &data)) < 0) + goto cleanup; + + if (counts[CHECKOUT_ACTION__UPDATE_SUBMODULE] > 0 && + (error = checkout_create_submodules(actions, &data)) < 0) + goto cleanup; + + assert(data.completed_steps == data.total_steps); + +cleanup: + if (error == GIT_EUSER) + giterr_clear(); + + if (!error && data.index != NULL && + (data.strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX) == 0) + error = git_index_write(data.index); + + git_diff_list_free(data.diff); + git_iterator_free(workdir); + git_iterator_free(baseline); + git__free(actions); + git__free(counts); + checkout_data_clear(&data); + + return error; +} + +int git_checkout_index( + git_repository *repo, + git_index *index, + git_checkout_opts *opts) +{ + int error; + git_iterator *index_i; + + if ((error = git_repository__ensure_not_bare(repo, "checkout index")) < 0) + return error; + + if (!index && (error = git_repository_index__weakptr(&index, repo)) < 0) + return error; + GIT_REFCOUNT_INC(index); + + if (!(error = git_iterator_for_index(&index_i, index, 0, NULL, NULL))) + error = git_checkout_iterator(index_i, opts); + + git_iterator_free(index_i); + git_index_free(index); + + return error; +} + +int git_checkout_tree( + git_repository *repo, + const git_object *treeish, + git_checkout_opts *opts) +{ + int error; + git_tree *tree = NULL; + git_iterator *tree_i = NULL; + + if ((error = git_repository__ensure_not_bare(repo, "checkout tree")) < 0) + return error; + + if (git_object_peel((git_object **)&tree, treeish, GIT_OBJ_TREE) < 0) { + giterr_set( + GITERR_CHECKOUT, "Provided object cannot be peeled to a tree"); + return -1; + } + + if (!(error = git_iterator_for_tree(&tree_i, tree, 0, NULL, NULL))) + error = git_checkout_iterator(tree_i, opts); + + git_iterator_free(tree_i); + git_tree_free(tree); + + return error; +} + +int git_checkout_head( + git_repository *repo, + git_checkout_opts *opts) +{ + int error; + git_tree *head = NULL; + git_iterator *head_i = NULL; + + if ((error = git_repository__ensure_not_bare(repo, "checkout head")) < 0) + return error; + + if (!(error = checkout_lookup_head_tree(&head, repo)) && + !(error = git_iterator_for_tree(&head_i, head, 0, NULL, NULL))) + error = git_checkout_iterator(head_i, opts); + + git_iterator_free(head_i); + git_tree_free(head); + + return error; +} diff --git a/src/checkout.h b/src/checkout.h new file mode 100644 index 000000000..b1dc80c38 --- /dev/null +++ b/src/checkout.h @@ -0,0 +1,24 @@ +/* + * 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_checkout_h__ +#define INCLUDE_checkout_h__ + +#include "git2/checkout.h" +#include "iterator.h" + +#define GIT_CHECKOUT__NOTIFY_CONFLICT_TREE (1u << 12) + +/** + * Update the working directory to match the target iterator. The + * expected baseline value can be passed in via the checkout options + * or else will default to the HEAD commit. + */ +extern int git_checkout_iterator( + git_iterator *target, + git_checkout_opts *opts); + +#endif diff --git a/src/clone.c b/src/clone.c new file mode 100644 index 000000000..0bbccd44b --- /dev/null +++ b/src/clone.c @@ -0,0 +1,466 @@ +/* + * 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 <assert.h> + +#include "git2/clone.h" +#include "git2/remote.h" +#include "git2/revparse.h" +#include "git2/branch.h" +#include "git2/config.h" +#include "git2/checkout.h" +#include "git2/commit.h" +#include "git2/tree.h" + +#include "common.h" +#include "remote.h" +#include "fileops.h" +#include "refs.h" +#include "path.h" + +static int create_branch( + git_reference **branch, + git_repository *repo, + const git_oid *target, + const char *name) +{ + git_commit *head_obj = NULL; + git_reference *branch_ref = NULL; + int error; + + /* Find the target commit */ + if ((error = git_commit_lookup(&head_obj, repo, target)) < 0) + return error; + + /* Create the new branch */ + error = git_branch_create(&branch_ref, repo, name, head_obj, 0); + + git_commit_free(head_obj); + + if (!error) + *branch = branch_ref; + else + git_reference_free(branch_ref); + + return error; +} + +static int setup_tracking_config( + git_repository *repo, + const char *branch_name, + const char *remote_name, + const char *merge_target) +{ + git_config *cfg; + git_buf remote_key = GIT_BUF_INIT, merge_key = GIT_BUF_INIT; + int error = -1; + + if (git_repository_config__weakptr(&cfg, repo) < 0) + return -1; + + if (git_buf_printf(&remote_key, "branch.%s.remote", branch_name) < 0) + goto cleanup; + + if (git_buf_printf(&merge_key, "branch.%s.merge", branch_name) < 0) + goto cleanup; + + if (git_config_set_string(cfg, git_buf_cstr(&remote_key), remote_name) < 0) + goto cleanup; + + if (git_config_set_string(cfg, git_buf_cstr(&merge_key), merge_target) < 0) + goto cleanup; + + error = 0; + +cleanup: + git_buf_free(&remote_key); + git_buf_free(&merge_key); + return error; +} + +static int create_tracking_branch( + git_reference **branch, + git_repository *repo, + const git_oid *target, + const char *branch_name) +{ + int error; + + if ((error = create_branch(branch, repo, target, branch_name)) < 0) + return error; + + return setup_tracking_config( + repo, + branch_name, + GIT_REMOTE_ORIGIN, + git_reference_name(*branch)); +} + +struct head_info { + git_repository *repo; + git_oid remote_head_oid; + git_buf branchname; + const git_refspec *refspec; + bool found; +}; + +static int reference_matches_remote_head( + const char *reference_name, + void *payload) +{ + struct head_info *head_info = (struct head_info *)payload; + git_oid oid; + + /* TODO: Should we guard against references + * which name doesn't start with refs/heads/ ? + */ + + /* Stop looking if we've already found a match */ + if (head_info->found) + return 0; + + if (git_reference_name_to_id( + &oid, + head_info->repo, + reference_name) < 0) { + /* If the reference doesn't exists, it obviously cannot match the expected oid. */ + giterr_clear(); + return 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->refspec, + reference_name) < 0) + return -1; + + if (git_buf_len(&head_info->branchname) > 0) { + if (git_buf_sets( + &head_info->branchname, + git_buf_cstr(&head_info->branchname) + strlen(GIT_REFS_HEADS_DIR)) < 0) + return -1; + + head_info->found = 1; + } + } + + return 0; +} + +static int update_head_to_new_branch( + git_repository *repo, + const git_oid *target, + const char *name) +{ + git_reference *tracking_branch = NULL; + int error; + + if ((error = create_tracking_branch( + &tracking_branch, + repo, + target, + name)) < 0) + return error; + + error = git_repository_set_head(repo, git_reference_name(tracking_branch)); + + git_reference_free(tracking_branch); + + return error; +} + +static int get_head_callback(git_remote_head *head, void *payload) +{ + git_remote_head **destination = (git_remote_head **)payload; + + /* Save the first entry, and terminate the enumeration */ + *destination = head; + return 1; +} + +static int update_head_to_remote(git_repository *repo, git_remote *remote) +{ + int retcode = -1; + git_remote_head *remote_head; + struct head_info head_info; + git_buf remote_master_name = GIT_BUF_INIT; + + /* Did we just clone an empty repository? */ + if (remote->refs.length == 0) { + return setup_tracking_config( + repo, + "master", + GIT_REMOTE_ORIGIN, + GIT_REFS_HEADS_MASTER_FILE); + } + + /* 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; + + assert(remote_head); + + 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.found = 0; + + /* Determine the remote tracking reference name from the local master */ + if (git_refspec_transform_r( + &remote_master_name, + head_info.refspec, + GIT_REFS_HEADS_MASTER_FILE) < 0) + return -1; + + /* Check to see if the remote HEAD points to the remote master */ + if (reference_matches_remote_head(git_buf_cstr(&remote_master_name), &head_info) < 0) + goto cleanup; + + if (head_info.found) { + retcode = update_head_to_new_branch( + repo, + &head_info.remote_head_oid, + git_buf_cstr(&head_info.branchname)); + + goto cleanup; + } + + /* Not master. Check all the other refs. */ + if (git_reference_foreach( + repo, + GIT_REF_LISTALL, + reference_matches_remote_head, + &head_info) < 0) + goto cleanup; + + if (head_info.found) { + retcode = update_head_to_new_branch( + repo, + &head_info.remote_head_oid, + git_buf_cstr(&head_info.branchname)); + + goto cleanup; + } else { + retcode = git_repository_set_head_detached( + repo, + &head_info.remote_head_oid); + goto cleanup; + } + +cleanup: + git_buf_free(&remote_master_name); + git_buf_free(&head_info.branchname); + return retcode; +} + +static int update_head_to_branch( + git_repository *repo, + const git_clone_options *options) +{ + 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", + options->remote_name, options->checkout_branch)) < 0 ) + goto cleanup; + + if ((retcode = git_reference_lookup(&remote_ref, repo, git_buf_cstr(&remote_branch_name))) < 0) + goto cleanup; + + retcode = update_head_to_new_branch(repo, git_reference_target(remote_ref), + options->checkout_branch); + +cleanup: + git_reference_free(remote_ref); + git_buf_free(&remote_branch_name); + return retcode; +} + +/* + * submodules? + */ + +static int create_and_configure_origin( + git_remote **out, + git_repository *repo, + const char *url, + const git_clone_options *options) +{ + int error; + git_remote *origin = NULL; + + if ((error = git_remote_create(&origin, repo, options->remote_name, url)) < 0) + goto on_error; + + git_remote_set_cred_acquire_cb(origin, options->cred_acquire_cb, + options->cred_acquire_payload); + git_remote_set_autotag(origin, options->remote_autotag); + /* + * Don't write FETCH_HEAD, we'll check out the remote tracking + * branch ourselves based on the server's default. + */ + git_remote_set_update_fetchhead(origin, 0); + + if (options->remote_callbacks && + (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->push_spec && + (error = git_remote_set_pushspec(origin, options->push_spec)) < 0) + goto on_error; + + if (options->pushurl && + (error = git_remote_set_pushurl(origin, options->pushurl)) < 0) + goto on_error; + + if ((error = git_remote_save(origin)) < 0) + goto on_error; + + *out = origin; + return 0; + +on_error: + git_remote_free(origin); + return error; +} + + +static int setup_remotes_and_fetch( + git_repository *repo, + const char *url, + const git_clone_options *options) +{ + int retcode = GIT_ERROR; + git_remote *origin; + + /* 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); + } + + return retcode; +} + + +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; +} + +static bool should_checkout( + git_repository *repo, + bool is_bare, + git_checkout_opts *opts) +{ + if (is_bare) + return false; + + if (!opts) + return false; + + if (opts->checkout_strategy == GIT_CHECKOUT_NONE) + return false; + + return !git_repository_head_orphan(repo); +} + +static void normalize_options(git_clone_options *dst, const git_clone_options *src) +{ + git_clone_options default_options = GIT_CLONE_OPTIONS_INIT; + if (!src) src = &default_options; + + *dst = *src; + + /* 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( + git_repository **out, + const char *url, + const char *local_path, + const git_clone_options *options) +{ + int retcode = GIT_ERROR; + git_repository *repo = NULL; + git_clone_options normOptions; + int remove_directory_on_failure = 0; + + assert(out && url && local_path); + + normalize_options(&normOptions, options); + GITERR_CHECK_VERSION(&normOptions, GIT_CLONE_OPTIONS_VERSION, "git_clone_options"); + + if (!path_is_okay(local_path)) { + return GIT_ERROR; + } + + /* Only remove the directory on failure if we create it */ + remove_directory_on_failure = !git_path_exists(local_path); + + if (!(retcode = git_repository_init(&repo, local_path, normOptions.bare))) { + if ((retcode = setup_remotes_and_fetch(repo, url, &normOptions)) < 0) { + /* Failed to fetch; clean up */ + git_repository_free(repo); + + if (remove_directory_on_failure) + git_futils_rmdir_r(local_path, NULL, GIT_RMDIR_REMOVE_FILES); + else + git_futils_cleanupdir_r(local_path); + + } else { + *out = repo; + retcode = 0; + } + } + + if (!retcode && should_checkout(repo, normOptions.bare, &normOptions.checkout_opts)) + retcode = git_checkout_head(*out, &normOptions.checkout_opts); + + return retcode; +} diff --git a/src/commit.c b/src/commit.c index 2bf12f3a5..c7b83ed43 100644 --- a/src/commit.c +++ b/src/commit.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -18,31 +18,22 @@ #include <stdarg.h> -#define COMMIT_BASIC_PARSE 0x0 -#define COMMIT_FULL_PARSE 0x1 - -#define COMMIT_PRINT(commit) {\ - char oid[41]; oid[40] = 0;\ - git_oid_fmt(oid, &commit->object.id);\ - printf("Oid: %s | In degree: %d | Time: %u\n", oid, commit->in_degree, commit->commit_time);\ -} - static void clear_parents(git_commit *commit) { - unsigned int i; + size_t i; - for (i = 0; i < commit->parent_oids.length; ++i) { - git_oid *parent = git_vector_get(&commit->parent_oids, i); + for (i = 0; i < commit->parent_ids.length; ++i) { + git_oid *parent = git_vector_get(&commit->parent_ids, i); git__free(parent); } - git_vector_clear(&commit->parent_oids); + git_vector_clear(&commit->parent_ids); } void git_commit__free(git_commit *commit) { clear_parents(commit); - git_vector_free(&commit->parent_oids); + git_vector_free(&commit->parent_ids); git_signature_free(commit->author); git_signature_free(commit->committer); @@ -52,11 +43,6 @@ void git_commit__free(git_commit *commit) git__free(commit); } -const git_oid *git_commit_id(git_commit *c) -{ - return git_object_id((git_object *)c); -} - int git_commit_create_v( git_oid *oid, git_repository *repo, @@ -90,66 +76,6 @@ int git_commit_create_v( return res; } -/* Update the reference named `ref_name` so it points to `oid` */ -static int update_reference(git_repository *repo, git_oid *oid, const char *ref_name) -{ - git_reference *ref; - int res; - - res = git_reference_lookup(&ref, repo, ref_name); - - /* If we haven't found the reference at all, we assume we need to create - * a new reference and that's it */ - if (res == GIT_ENOTFOUND) { - giterr_clear(); - return git_reference_create_oid(NULL, repo, ref_name, oid, 1); - } - - if (res < 0) - return -1; - - /* If we have found a reference, but it's symbolic, we need to update - * the direct reference it points to */ - if (git_reference_type(ref) == GIT_REF_SYMBOLIC) { - git_reference *aux; - const char *sym_target; - - /* The target pointed at by this reference */ - sym_target = git_reference_target(ref); - - /* resolve the reference to the target it points to */ - res = git_reference_resolve(&aux, ref); - - /* - * if the symbolic reference pointed to an inexisting ref, - * this is means we're creating a new branch, for example. - * We need to create a new direct reference with that name - */ - if (res == GIT_ENOTFOUND) { - giterr_clear(); - res = git_reference_create_oid(NULL, repo, sym_target, oid, 1); - git_reference_free(ref); - return res; - } - - /* free the original symbolic reference now; not before because - * we're using the `sym_target` pointer */ - git_reference_free(ref); - - if (res < 0) - return -1; - - /* store the newly found direct reference in its place */ - ref = aux; - } - - /* ref is made to point to `oid`: ref is either the original reference, - * or the target of the symbolic reference we've looked up */ - res = git_reference_set_oid(ref, oid); - git_reference_free(ref); - return res; -} - int git_commit_create( git_oid *oid, git_repository *repo, @@ -162,7 +88,7 @@ int git_commit_create( int parent_count, const git_commit *parents[]) { - git_buf commit = GIT_BUF_INIT, cleaned_message = GIT_BUF_INIT; + git_buf commit = GIT_BUF_INIT; int i; git_odb *odb; @@ -183,15 +109,9 @@ int git_commit_create( git_buf_putc(&commit, '\n'); - /* Remove comments by default */ - if (git_message_prettify(&cleaned_message, message, 1) < 0) + if (git_buf_puts(&commit, message) < 0) goto on_error; - if (git_buf_puts(&commit, git_buf_cstr(&cleaned_message)) < 0) - goto on_error; - - git_buf_free(&cleaned_message); - if (git_repository_odb__weakptr(&odb, repo) < 0) goto on_error; @@ -201,13 +121,12 @@ int git_commit_create( git_buf_free(&commit); if (update_ref != NULL) - return update_reference(repo, oid, update_ref); + return git_reference__update_terminal(repo, update_ref, oid); return 0; on_error: git_buf_free(&commit); - git_buf_free(&cleaned_message); giterr_set(GITERR_OBJECT, "Failed to create commit."); return -1; } @@ -216,27 +135,25 @@ int git_commit__parse_buffer(git_commit *commit, const void *data, size_t len) { const char *buffer = data; const char *buffer_end = (const char *)data + len; + git_oid parent_id; - git_oid parent_oid; - - git_vector_init(&commit->parent_oids, 4, NULL); + if (git_vector_init(&commit->parent_ids, 4, NULL) < 0) + return -1; - if (git_oid__parse(&commit->tree_oid, &buffer, buffer_end, "tree ") < 0) + if (git_oid__parse(&commit->tree_id, &buffer, buffer_end, "tree ") < 0) goto bad_buffer; /* * TODO: commit grafts! */ - while (git_oid__parse(&parent_oid, &buffer, buffer_end, "parent ") == 0) { - git_oid *new_oid; - - new_oid = git__malloc(sizeof(git_oid)); - GITERR_CHECK_ALLOC(new_oid); + while (git_oid__parse(&parent_id, &buffer, buffer_end, "parent ") == 0) { + git_oid *new_id = git__malloc(sizeof(git_oid)); + GITERR_CHECK_ALLOC(new_id); - git_oid_cpy(new_oid, &parent_oid); + git_oid_cpy(new_id, &parent_id); - if (git_vector_insert(&commit->parent_oids, new_oid) < 0) + if (git_vector_insert(&commit->parent_ids, new_id) < 0) return -1; } @@ -253,24 +170,30 @@ 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; - if (git__prefixcmp(buffer, "encoding ") == 0) { - const char *encoding_end; - buffer += strlen("encoding "); + /* Parse add'l header entries until blank line found */ + while (buffer < buffer_end && *buffer != '\n') { + const char *eoln = buffer; + while (eoln < buffer_end && *eoln != '\n') + ++eoln; + + if (git__prefixcmp(buffer, "encoding ") == 0) { + buffer += strlen("encoding "); - encoding_end = buffer; - while (encoding_end < buffer_end && *encoding_end != '\n') - encoding_end++; + commit->message_encoding = git__strndup(buffer, eoln - buffer); + GITERR_CHECK_ALLOC(commit->message_encoding); + } - commit->message_encoding = git__strndup(buffer, encoding_end - buffer); - GITERR_CHECK_ALLOC(commit->message_encoding); + if (eoln < buffer_end && *eoln == '\n') + ++eoln; - buffer = encoding_end; + buffer = eoln; } - /* parse commit message */ - while (buffer < buffer_end - 1 && *buffer == '\n') + /* buffer is now at the end of the header, double-check and move forward into the message */ + if (buffer < buffer_end && *buffer == '\n') buffer++; + /* parse commit message */ if (buffer <= buffer_end) { commit->message = git__strndup(buffer, buffer_end - buffer); GITERR_CHECK_ALLOC(commit->message); @@ -290,7 +213,7 @@ int git_commit__parse(git_commit *commit, git_odb_object *obj) } #define GIT_COMMIT_GETTER(_rvalue, _name, _return) \ - _rvalue git_commit_##_name(git_commit *commit) \ + _rvalue git_commit_##_name(const git_commit *commit) \ {\ assert(commit); \ return _return; \ @@ -302,33 +225,66 @@ GIT_COMMIT_GETTER(const char *, message, commit->message) GIT_COMMIT_GETTER(const char *, message_encoding, commit->message_encoding) 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, commit->parent_oids.length) -GIT_COMMIT_GETTER(const git_oid *, tree_oid, &commit->tree_oid); +GIT_COMMIT_GETTER(unsigned int, parentcount, (unsigned int)commit->parent_ids.length) +GIT_COMMIT_GETTER(const git_oid *, tree_id, &commit->tree_id); +int git_commit_tree(git_tree **tree_out, const git_commit *commit) +{ + assert(commit); + return git_tree_lookup(tree_out, commit->object.repo, &commit->tree_id); +} -int git_commit_tree(git_tree **tree_out, git_commit *commit) +const git_oid *git_commit_parent_id(git_commit *commit, unsigned int n) { assert(commit); - return git_tree_lookup(tree_out, commit->object.repo, &commit->tree_oid); + + return git_vector_get(&commit->parent_ids, n); } int git_commit_parent(git_commit **parent, git_commit *commit, unsigned int n) { - git_oid *parent_oid; + const git_oid *parent_id; assert(commit); - parent_oid = git_vector_get(&commit->parent_oids, n); - if (parent_oid == NULL) { + parent_id = git_commit_parent_id(commit, n); + if (parent_id == NULL) { giterr_set(GITERR_INVALID, "Parent %u does not exist", n); return GIT_ENOTFOUND; } - return git_commit_lookup(parent, commit->object.repo, parent_oid); + return git_commit_lookup(parent, commit->object.repo, parent_id); } -const git_oid *git_commit_parent_oid(git_commit *commit, unsigned int n) +int git_commit_nth_gen_ancestor( + git_commit **ancestor, + const git_commit *commit, + unsigned int n) { - assert(commit); + git_commit *current, *parent; + int error; + + assert(ancestor && commit); + + current = (git_commit *)commit; + + if (n == 0) + return git_commit_lookup( + ancestor, + commit->object.repo, + git_object_id((const git_object *)commit)); + + while (n--) { + error = git_commit_parent(&parent, (git_commit *)current, 0); - return git_vector_get(&commit->parent_oids, n); + if (current != commit) + git_commit_free(current); + + if (error < 0) + return error; + + current = parent; + } + + *ancestor = parent; + return 0; } diff --git a/src/commit.h b/src/commit.h index d9f492862..1ab164c0b 100644 --- a/src/commit.h +++ b/src/commit.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -17,8 +17,8 @@ struct git_commit { git_object object; - git_vector parent_oids; - git_oid tree_oid; + git_vector parent_ids; + git_oid tree_id; git_signature *author; git_signature *committer; diff --git a/src/commit_list.c b/src/commit_list.c new file mode 100644 index 000000000..603dd754a --- /dev/null +++ b/src/commit_list.c @@ -0,0 +1,194 @@ +/* + * 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 "commit_list.h" +#include "common.h" +#include "revwalk.h" +#include "pool.h" +#include "odb.h" + +int git_commit_list_time_cmp(void *a, void *b) +{ + git_commit_list_node *commit_a = (git_commit_list_node *)a; + git_commit_list_node *commit_b = (git_commit_list_node *)b; + + return (commit_a->time < commit_b->time); +} + +git_commit_list *git_commit_list_insert(git_commit_list_node *item, git_commit_list **list_p) +{ + git_commit_list *new_list = git__malloc(sizeof(git_commit_list)); + if (new_list != NULL) { + new_list->item = item; + new_list->next = *list_p; + } + *list_p = new_list; + return new_list; +} + +git_commit_list *git_commit_list_insert_by_date(git_commit_list_node *item, git_commit_list **list_p) +{ + git_commit_list **pp = list_p; + git_commit_list *p; + + while ((p = *pp) != NULL) { + if (git_commit_list_time_cmp(p->item, item) < 0) + break; + + pp = &p->next; + } + + return git_commit_list_insert(item, pp); +} + +git_commit_list_node *git_commit_list_alloc_node(git_revwalk *walk) +{ + return (git_commit_list_node *)git_pool_malloc(&walk->commit_pool, COMMIT_ALLOC); +} + +static int commit_error(git_commit_list_node *commit, const char *msg) +{ + char commit_oid[GIT_OID_HEXSZ + 1]; + git_oid_fmt(commit_oid, &commit->oid); + commit_oid[GIT_OID_HEXSZ] = '\0'; + + giterr_set(GITERR_ODB, "Failed to parse commit %s - %s", commit_oid, msg); + + return -1; +} + +static git_commit_list_node **alloc_parents( + git_revwalk *walk, git_commit_list_node *commit, size_t n_parents) +{ + if (n_parents <= PARENTS_PER_COMMIT) + return (git_commit_list_node **)((char *)commit + sizeof(git_commit_list_node)); + + return (git_commit_list_node **)git_pool_malloc( + &walk->commit_pool, (uint32_t)(n_parents * sizeof(git_commit_list_node *))); +} + + +void git_commit_list_free(git_commit_list **list_p) +{ + git_commit_list *list = *list_p; + + if (list == NULL) + return; + + while (list) { + git_commit_list *temp = list; + list = temp->next; + git__free(temp); + } + + *list_p = NULL; +} + +git_commit_list_node *git_commit_list_pop(git_commit_list **stack) +{ + git_commit_list *top = *stack; + git_commit_list_node *item = top ? top->item : NULL; + + if (top) { + *stack = top->next; + git__free(top); + } + return item; +} + +static int commit_quick_parse(git_revwalk *walk, git_commit_list_node *commit, git_rawobj *raw) +{ + 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; + int i, parents = 0; + int commit_time; + + buffer += strlen("tree ") + GIT_OID_HEXSZ + 1; + + parents_start = buffer; + while (buffer + parent_len < buffer_end && memcmp(buffer, "parent ", strlen("parent ")) == 0) { + parents++; + buffer += parent_len; + } + + commit->parents = alloc_parents(walk, commit, parents); + GITERR_CHECK_ALLOC(commit->parents); + + buffer = parents_start; + for (i = 0; i < parents; ++i) { + git_oid oid; + + if (git_oid_fromstr(&oid, (char *)buffer + strlen("parent ")) < 0) + return -1; + + commit->parents[i] = git_revwalk__commit_lookup(walk, &oid); + if (commit->parents[i] == NULL) + return -1; + + buffer += parent_len; + } + + commit->out_degree = (unsigned short)parents; + + if ((committer_start = buffer = memchr(buffer, '\n', buffer_end - buffer)) == NULL) + return commit_error(commit, "object is corrupted"); + + buffer++; + + if ((buffer = memchr(buffer, '\n', buffer_end - buffer)) == NULL) + return commit_error(commit, "object is corrupted"); + + /* Skip trailing spaces */ + while (buffer > committer_start && git__isspace(*buffer)) + buffer--; + + /* Seek for the begining of the pack of digits */ + while (buffer > committer_start && git__isdigit(*buffer)) + buffer--; + + /* Skip potential timezone offset */ + if ((buffer > committer_start) && (*buffer == '+' || *buffer == '-')) { + buffer--; + + while (buffer > committer_start && git__isspace(*buffer)) + buffer--; + + while (buffer > committer_start && git__isdigit(*buffer)) + buffer--; + } + + if ((buffer == committer_start) || (git__strtol32(&commit_time, (char *)(buffer + 1), NULL, 10) < 0)) + return commit_error(commit, "cannot parse commit time"); + + commit->time = (time_t)commit_time; + commit->parsed = 1; + return 0; +} + +int git_commit_list_parse(git_revwalk *walk, git_commit_list_node *commit) +{ + git_odb_object *obj; + int error; + + if (commit->parsed) + return 0; + + if ((error = git_odb_read(&obj, walk->odb, &commit->oid)) < 0) + return error; + + if (obj->raw.type != GIT_OBJ_COMMIT) { + giterr_set(GITERR_INVALID, "Object is no commit object"); + error = -1; + } else + error = commit_quick_parse(walk, commit, &obj->raw); + + git_odb_object_free(obj); + return error; +} + diff --git a/src/commit_list.h b/src/commit_list.h new file mode 100644 index 000000000..d2f54b3ca --- /dev/null +++ b/src/commit_list.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_commit_list_h__ +#define INCLUDE_commit_list_h__ + +#include "git2/oid.h" + +#define PARENT1 (1 << 0) +#define PARENT2 (1 << 1) +#define RESULT (1 << 2) +#define STALE (1 << 3) + +#define PARENTS_PER_COMMIT 2 +#define COMMIT_ALLOC \ + (sizeof(git_commit_list_node) + PARENTS_PER_COMMIT * sizeof(git_commit_list_node *)) + +typedef struct git_commit_list_node { + git_oid oid; + uint32_t time; + unsigned int seen:1, + uninteresting:1, + topo_delay:1, + parsed:1, + flags : 4; + + unsigned short in_degree; + unsigned short out_degree; + + struct git_commit_list_node **parents; +} git_commit_list_node; + +typedef struct git_commit_list { + git_commit_list_node *item; + struct git_commit_list *next; +} git_commit_list; + +git_commit_list_node *git_commit_list_alloc_node(git_revwalk *walk); +int git_commit_list_time_cmp(void *a, void *b); +void git_commit_list_free(git_commit_list **list_p); +git_commit_list *git_commit_list_insert(git_commit_list_node *item, git_commit_list **list_p); +git_commit_list *git_commit_list_insert_by_date(git_commit_list_node *item, git_commit_list **list_p); +int git_commit_list_parse(git_revwalk *walk, git_commit_list_node *commit); +git_commit_list_node *git_commit_list_pop(git_commit_list **stack); + +#endif diff --git a/src/common.h b/src/common.h index 30757de70..02d9ce9b6 100644 --- a/src/common.h +++ b/src/common.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -24,21 +24,24 @@ # include <io.h> # include <direct.h> +# include <winsock2.h> # include <windows.h> # include "win32/msvc-compat.h" # include "win32/mingw-compat.h" +# include "win32/error.h" +# include "win32/version.h" # ifdef GIT_THREADS # include "win32/pthread.h" -#endif - -# define snprintf _snprintf +# endif #else -# include <unistd.h> +# include <unistd.h> # ifdef GIT_THREADS # include <pthread.h> # endif +#define GIT_STDLIB_CALL + #endif #include "git2/types.h" @@ -48,25 +51,59 @@ #include <regex.h> -extern void git___throw(const char *, ...) GIT_FORMAT_PRINTF(1, 2); -#define git__throw(error, ...) \ - (git___throw(__VA_ARGS__), error) +/** + * Check a pointer allocation result, returning -1 if it failed. + */ +#define GITERR_CHECK_ALLOC(ptr) if (ptr == NULL) { return -1; } -extern void git___rethrow(const char *, ...) GIT_FORMAT_PRINTF(1, 2); -#define git__rethrow(error, ...) \ - (git___rethrow(__VA_ARGS__), error) +/** + * Check a return value and propogate result if non-zero. + */ +#define GITERR_CHECK_ERROR(code) \ + do { int _err = (code); if (_err < 0) return _err; } while (0) +/** + * Set the error message for this thread, formatting as needed. + */ +void giterr_set(int error_class, const char *string, ...); -#define GITERR_CHECK_ALLOC(ptr) if (ptr == NULL) { return -1; } +/** + * Set the error message for a regex failure, using the internal regex + * error code lookup and return a libgit error code. + */ +int giterr_set_regex(const regex_t *regex, int error_code); -void giterr_set_oom(void); -void giterr_set(int error_class, const char *string, ...); -void giterr_clear(void); -void giterr_set_str(int error_class, const char *string); -void giterr_set_regex(const regex_t *regex, int error_code); +/** + * Check a versioned structure for validity + */ +GIT_INLINE(int) giterr__check_version(const void *structure, unsigned int expected_max, const char *name) +{ + unsigned int actual; + if (!structure) + return 0; -#include "util.h" + actual = *(const unsigned int*)structure; + if (actual > 0 && actual <= expected_max) + return 0; + giterr_set(GITERR_INVALID, "Invalid version %d on %s", actual, name); + return -1; +} +#define GITERR_CHECK_VERSION(S,V,N) if (giterr__check_version(S,V,N) < 0) return -1 + +/** + * Initialize a structure with a version. + */ +GIT_INLINE(void) git__init_structure(void *structure, size_t len, unsigned int version) +{ + memset(structure, 0, len); + *((int*)structure) = version; +} +#define GIT_INIT_STRUCTURE(S,V) git__init_structure(S, sizeof(*S), V) + +/* NOTE: other giterr functions are in the public errors.h header file */ + +#include "util.h" #endif /* INCLUDE_common_h__ */ diff --git a/src/compress.c b/src/compress.c new file mode 100644 index 000000000..14b79404c --- /dev/null +++ b/src/compress.c @@ -0,0 +1,53 @@ +/* + * 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 "compress.h" + +#include <zlib.h> + +#define BUFFER_SIZE (1024 * 1024) + +int git__compress(git_buf *buf, const void *buff, size_t len) +{ + z_stream zs; + char *zb; + size_t have; + + memset(&zs, 0, sizeof(zs)); + if (deflateInit(&zs, Z_DEFAULT_COMPRESSION) != Z_OK) + return -1; + + zb = git__malloc(BUFFER_SIZE); + GITERR_CHECK_ALLOC(zb); + + zs.next_in = (void *)buff; + zs.avail_in = (uInt)len; + + do { + zs.next_out = (unsigned char *)zb; + zs.avail_out = BUFFER_SIZE; + + if (deflate(&zs, Z_FINISH) == Z_STREAM_ERROR) { + git__free(zb); + return -1; + } + + have = BUFFER_SIZE - (size_t)zs.avail_out; + + if (git_buf_put(buf, zb, have) < 0) { + git__free(zb); + return -1; + } + + } while (zs.avail_out == 0); + + assert(zs.avail_in == 0); + + deflateEnd(&zs); + git__free(zb); + return 0; +} diff --git a/src/compress.h b/src/compress.h new file mode 100644 index 000000000..49e6f4749 --- /dev/null +++ b/src/compress.h @@ -0,0 +1,16 @@ +/* + * 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_compress_h__ +#define INCLUDE_compress_h__ + +#include "common.h" + +#include "buffer.h" + +int git__compress(git_buf *buf, const void *buff, size_t len); + +#endif /* INCLUDE_compress_h__ */ diff --git a/src/config.c b/src/config.c index 618202c34..5379b0ec5 100644 --- a/src/config.c +++ b/src/config.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -10,6 +10,8 @@ #include "config.h" #include "git2/config.h" #include "vector.h" +#include "buf_text.h" +#include "config_file.h" #if GIT_WIN32 # include <windows.h> #endif @@ -17,21 +19,29 @@ #include <ctype.h> typedef struct { - git_config_file *file; - int priority; + git_refcount rc; + + git_config_backend *file; + unsigned int level; } file_internal; +static void file_internal_free(file_internal *internal) +{ + git_config_backend *file; + + file = internal->file; + file->free(file); + git__free(internal); +} + static void config_free(git_config *cfg) { - unsigned int i; - git_config_file *file; + size_t i; file_internal *internal; for(i = 0; i < cfg->files.length; ++i){ internal = git_vector_get(&cfg->files, i); - file = internal->file; - file->free(file); - git__free(internal); + GIT_REFCOUNT_DEC(internal, file_internal_free); } git_vector_free(&cfg->files); @@ -51,7 +61,7 @@ static int config_backend_cmp(const void *a, const void *b) const file_internal *bk_a = (const file_internal *)(a); const file_internal *bk_b = (const file_internal *)(b); - return bk_b->priority - bk_a->priority; + return bk_b->level - bk_a->level; } int git_config_new(git_config **out) @@ -73,91 +83,252 @@ int git_config_new(git_config **out) return 0; } -int git_config_add_file_ondisk(git_config *cfg, const char *path, int priority) +int git_config_add_file_ondisk( + git_config *cfg, + const char *path, + unsigned int level, + int force) { - git_config_file *file = NULL; + git_config_backend *file = NULL; + int res; + + assert(cfg && path); + + if (!git_path_isfile(path)) { + giterr_set(GITERR_CONFIG, "Cannot find config file '%s'", path); + return GIT_ENOTFOUND; + } if (git_config_file__ondisk(&file, path) < 0) return -1; - if (git_config_add_file(cfg, file, priority) < 0) { + if ((res = git_config_add_backend(cfg, file, level, force)) < 0) { /* * free manually; the file is not owned by the config * instance yet and will not be freed on cleanup */ file->free(file); - return -1; + return res; } return 0; } -int git_config_open_ondisk(git_config **cfg, const char *path) +int git_config_open_ondisk(git_config **out, const char *path) { - if (git_config_new(cfg) < 0) - return -1; + int error; + git_config *config; - if (git_config_add_file_ondisk(*cfg, path, 1) < 0) { - git_config_free(*cfg); + *out = NULL; + + if (git_config_new(&config) < 0) return -1; + + if ((error = git_config_add_file_ondisk(config, path, GIT_CONFIG_LEVEL_LOCAL, 0)) < 0) + git_config_free(config); + else + *out = config; + + return error; +} + +static int find_internal_file_by_level( + file_internal **internal_out, + const git_config *cfg, + int level) +{ + int pos = -1; + file_internal *internal; + unsigned int 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 + * sorted by decreasing order of level, getting the file at position 0 + * will do the job. + */ + if (level == GIT_CONFIG_HIGHEST_LEVEL) { + pos = 0; + } else { + git_vector_foreach(&cfg->files, i, internal) { + if (internal->level == (unsigned int)level) + pos = i; + } + } + + if (pos == -1) { + giterr_set(GITERR_CONFIG, + "No config file exists for the given level '%i'", level); + return GIT_ENOTFOUND; + } + + *internal_out = git_vector_get(&cfg->files, pos); + + return 0; +} + +static int duplicate_level(void **old_raw, void *new_raw) +{ + file_internal **old = (file_internal **)old_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); + return GIT_EEXISTS; +} + +static void try_remove_existing_file_internal( + git_config *cfg, + unsigned int level) +{ + int pos = -1; + file_internal *internal; + unsigned int i; + + git_vector_foreach(&cfg->files, i, internal) { + if (internal->level == level) + pos = i; + } + + if (pos == -1) + return; + + internal = git_vector_get(&cfg->files, pos); + + if (git_vector_remove(&cfg->files, pos) < 0) + return; + + GIT_REFCOUNT_DEC(internal, file_internal_free); +} + +static int git_config__add_internal( + git_config *cfg, + file_internal *internal, + unsigned int level, + int force) +{ + int result; + + /* delete existing config file for level if it exists */ + if (force) + try_remove_existing_file_internal(cfg, level); + + if ((result = git_vector_insert_sorted(&cfg->files, + internal, &duplicate_level)) < 0) + return result; + + git_vector_sort(&cfg->files); + internal->file->cfg = cfg; + + GIT_REFCOUNT_INC(internal); + + return 0; +} + +int git_config_open_level( + git_config **cfg_out, + const git_config *cfg_parent, + unsigned int level) +{ + git_config *cfg; + file_internal *internal; + int res; + + if ((res = find_internal_file_by_level(&internal, cfg_parent, level)) < 0) + return res; + + if ((res = git_config_new(&cfg)) < 0) + return res; + + if ((res = git_config__add_internal(cfg, internal, level, true)) < 0) { + git_config_free(cfg); + return res; } + *cfg_out = cfg; + return 0; } -int git_config_add_file(git_config *cfg, git_config_file *file, int priority) +int git_config_add_backend( + git_config *cfg, + git_config_backend *file, + unsigned int level, + int force) { file_internal *internal; int result; assert(cfg && file); - if ((result = file->open(file)) < 0) + GITERR_CHECK_VERSION(file, GIT_CONFIG_BACKEND_VERSION, "git_config_backend"); + + if ((result = file->open(file, level)) < 0) return result; internal = git__malloc(sizeof(file_internal)); GITERR_CHECK_ALLOC(internal); + memset(internal, 0x0, sizeof(file_internal)); + internal->file = file; - internal->priority = priority; + internal->level = level; - if (git_vector_insert(&cfg->files, internal) < 0) { + if ((result = git_config__add_internal(cfg, internal, level, force)) < 0) { git__free(internal); - return -1; + return result; } - git_vector_sort(&cfg->files); - internal->file->cfg = cfg; - return 0; } +int git_config_refresh(git_config *cfg) +{ + int error = 0; + size_t i; + + for (i = 0; i < cfg->files.length && !error; ++i) { + file_internal *internal = git_vector_get(&cfg->files, i); + git_config_backend *file = internal->file; + error = file->refresh(file); + } + + return error; +} + /* * Loop over all the variables */ -int git_config_foreach(git_config *cfg, int (*fn)(const char *, const char *, void *), void *data) +int git_config_foreach( + const git_config *cfg, git_config_foreach_cb cb, void *payload) +{ + return git_config_foreach_match(cfg, NULL, cb, payload); +} + +int git_config_foreach_match( + const git_config *cfg, + const char *regexp, + git_config_foreach_cb cb, + void *payload) { int ret = 0; - unsigned int i; + size_t i; file_internal *internal; - git_config_file *file; + git_config_backend *file; - for(i = 0; i < cfg->files.length && ret == 0; ++i) { + for (i = 0; i < cfg->files.length && ret == 0; ++i) { internal = git_vector_get(&cfg->files, i); file = internal->file; - ret = file->foreach(file, fn, data); + ret = file->foreach(file, regexp, cb, payload); } return ret; } -int git_config_delete(git_config *cfg, const char *name) +int git_config_delete_entry(git_config *cfg, const char *name) { + git_config_backend *file; file_internal *internal; - git_config_file *file; - - assert(cfg->files.length); internal = git_vector_get(&cfg->files, 0); file = internal->file; @@ -188,10 +359,13 @@ 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) { + git_config_backend *file; file_internal *internal; - git_config_file *file; - assert(cfg->files.length); + if (!value) { + giterr_set(GITERR_CONFIG, "The value to set cannot be NULL"); + return -1; + } internal = git_vector_get(&cfg->files, 0); file = internal->file; @@ -199,211 +373,121 @@ int git_config_set_string(git_config *cfg, const char *name, const char *value) return file->set(file, name, value); } -static int parse_int64(int64_t *out, const char *value) -{ - const char *num_end; - int64_t num; - - if (git__strtol64(&num, value, &num_end, 0) < 0) - return -1; - - switch (*num_end) { - case 'g': - case 'G': - num *= 1024; - /* fallthrough */ - - case 'm': - case 'M': - num *= 1024; - /* fallthrough */ - - case 'k': - case 'K': - num *= 1024; - - /* check that that there are no more characters after the - * given modifier suffix */ - if (num_end[1] != '\0') - return -1; - - /* fallthrough */ - - case '\0': - *out = num; - return 0; - - default: - return -1; - } -} - -static int parse_int32(int32_t *out, const char *value) -{ - int64_t tmp; - int32_t truncate; - - if (parse_int64(&tmp, value) < 0) - return -1; - - truncate = tmp & 0xFFFFFFFF; - if (truncate != tmp) - return -1; - - *out = truncate; - return 0; -} - /*********** * Getters ***********/ -int git_config_lookup_map_value( - git_cvar_map *maps, size_t map_n, const char *value, int *out) -{ - size_t i; - - if (!value) - return GIT_ENOTFOUND; - - for (i = 0; i < map_n; ++i) { - git_cvar_map *m = maps + i; - - switch (m->cvar_type) { - case GIT_CVAR_FALSE: - case GIT_CVAR_TRUE: { - int bool_val; - - if (git__parse_bool(&bool_val, value) == 0 && - bool_val == (int)m->cvar_type) { - *out = m->map_value; - return 0; - } - break; - } - - case GIT_CVAR_INT32: - if (parse_int32(out, value) == 0) - return 0; - break; - - case GIT_CVAR_STRING: - if (strcasecmp(value, m->str_match) == 0) { - *out = m->map_value; - return 0; - } - break; - } - } - - return GIT_ENOTFOUND; -} - int git_config_get_mapped( int *out, - git_config *cfg, + const git_config *cfg, const char *name, - git_cvar_map *maps, + const git_cvar_map *maps, size_t map_n) { const char *value; int ret; - ret = git_config_get_string(&value, cfg, name); - if (ret < 0) + if ((ret = git_config_get_string(&value, cfg, name)) < 0) return ret; - if (!git_config_lookup_map_value(maps, map_n, value, out)) - return 0; - - giterr_set(GITERR_CONFIG, - "Failed to map the '%s' config variable with a valid value", name); - return -1; + return git_config_lookup_map_value(out, maps, map_n, value); } -int git_config_get_int64(int64_t *out, git_config *cfg, const char *name) +int git_config_get_int64(int64_t *out, const git_config *cfg, const char *name) { const char *value; int ret; - ret = git_config_get_string(&value, cfg, name); - if (ret < 0) + if ((ret = git_config_get_string(&value, cfg, name)) < 0) return ret; - if (parse_int64(out, value) < 0) { - giterr_set(GITERR_CONFIG, "Failed to parse '%s' as an integer", value); - return -1; - } - - return 0; + return git_config_parse_int64(out, value); } -int git_config_get_int32(int32_t *out, git_config *cfg, const char *name) +int git_config_get_int32(int32_t *out, const git_config *cfg, const char *name) { const char *value; int ret; - ret = git_config_get_string(&value, cfg, name); - if (ret < 0) + if ((ret = git_config_get_string(&value, cfg, name)) < 0) return ret; - if (parse_int32(out, value) < 0) { - giterr_set(GITERR_CONFIG, "Failed to parse '%s' as a 32-bit integer", value); - return -1; + return git_config_parse_int32(out, value); +} + +static int get_string_at_file(const char **out, const git_config_backend *file, const char *name) +{ + const git_config_entry *entry; + int res; + + res = file->get(file, name, &entry); + if (!res) + *out = entry->value; + + return res; +} + +static int get_string(const char **out, const git_config *cfg, const char *name) +{ + file_internal *internal; + unsigned int i; + + git_vector_foreach(&cfg->files, i, internal) { + int res = get_string_at_file(out, internal->file, name); + + if (res != GIT_ENOTFOUND) + return res; } - return 0; + return GIT_ENOTFOUND; } -int git_config_get_bool(int *out, git_config *cfg, const char *name) +int git_config_get_bool(int *out, const git_config *cfg, const char *name) { - const char *value; + const char *value = NULL; int ret; - ret = git_config_get_string(&value, cfg, name); - if (ret < 0) + if ((ret = get_string(&value, cfg, name)) < 0) return ret; - if (git__parse_bool(out, value) == 0) - return 0; + return git_config_parse_bool(out, value); +} - if (parse_int32(out, value) == 0) { - *out = !!(*out); - return 0; - } +int git_config_get_string(const char **out, const git_config *cfg, const char *name) +{ + int ret; + const char *str = NULL; - giterr_set(GITERR_CONFIG, "Failed to parse '%s' as a boolean value", value); - return -1; + if ((ret = get_string(&str, cfg, name)) < 0) + return ret; + + *out = str == NULL ? "" : str; + return 0; } -int git_config_get_string(const char **out, git_config *cfg, const char *name) +int git_config_get_entry(const git_config_entry **out, const git_config *cfg, const char *name) { file_internal *internal; unsigned int i; - assert(cfg->files.length); - *out = NULL; git_vector_foreach(&cfg->files, i, internal) { - git_config_file *file = internal->file; + git_config_backend *file = internal->file; int ret = file->get(file, name, out); if (ret != GIT_ENOTFOUND) return ret; } - giterr_set(GITERR_CONFIG, "Config variable '%s' not found", name); return GIT_ENOTFOUND; } -int git_config_get_multivar(git_config *cfg, const char *name, const char *regexp, - int (*fn)(const char *value, void *data), void *data) +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_file *file; + git_config_backend *file; int ret = GIT_ENOTFOUND; - unsigned int i; - - assert(cfg->files.length); + size_t i; /* * This loop runs the "wrong" way 'round because we need to @@ -412,7 +496,7 @@ int git_config_get_multivar(git_config *cfg, const char *name, const char *regex for (i = cfg->files.length; i > 0; --i) { internal = git_vector_get(&cfg->files, i - 1); file = internal->file; - ret = file->get_multivar(file, name, regexp, fn, data); + ret = file->get_multivar(file, name, regexp, cb, payload); if (ret < 0 && ret != GIT_ENOTFOUND) return ret; } @@ -422,47 +506,57 @@ int git_config_get_multivar(git_config *cfg, const char *name, const char *regex int git_config_set_multivar(git_config *cfg, const char *name, const char *regexp, const char *value) { + git_config_backend *file; file_internal *internal; - git_config_file *file; - int ret = GIT_ENOTFOUND; - unsigned int i; - for (i = cfg->files.length; i > 0; --i) { - internal = git_vector_get(&cfg->files, i - 1); - file = internal->file; - ret = file->set_multivar(file, name, regexp, value); - if (ret < 0 && ret != GIT_ENOTFOUND) - return ret; + internal = git_vector_get(&cfg->files, 0); + file = internal->file; + + return file->set_multivar(file, name, regexp, value); +} + +static int git_config__find_file_to_path( + char *out, size_t outlen, int (*find)(git_buf *buf)) +{ + int error = 0; + git_buf path = GIT_BUF_INIT; + + if ((error = find(&path)) < 0) + goto done; + + if (path.size >= outlen) { + giterr_set(GITERR_NOMEMORY, "Buffer is too short for the path"); + error = GIT_EBUFS; + goto done; } - return 0; + git_buf_copy_cstr(out, outlen, &path); + +done: + git_buf_free(&path); + return error; } int git_config_find_global_r(git_buf *path) { - return git_futils_find_global_file(path, GIT_CONFIG_FILENAME); + return git_futils_find_global_file(path, GIT_CONFIG_FILENAME_GLOBAL); } int git_config_find_global(char *global_config_path, size_t length) { - git_buf path = GIT_BUF_INIT; - int ret = git_config_find_global_r(&path); - - if (ret < 0) { - git_buf_free(&path); - return ret; - } + return git_config__find_file_to_path( + global_config_path, length, git_config_find_global_r); +} - if (path.size >= length) { - git_buf_free(&path); - giterr_set(GITERR_NOMEMORY, - "Path is to long to fit on the given buffer"); - return -1; - } +int git_config_find_xdg_r(git_buf *path) +{ + return git_futils_find_xdg_file(path, GIT_CONFIG_FILENAME_XDG); +} - git_buf_copy_cstr(global_config_path, length, &path); - git_buf_free(&path); - return 0; +int git_config_find_xdg(char *xdg_config_path, size_t length) +{ + return git_config__find_file_to_path( + xdg_config_path, length, git_config_find_xdg_r); } int git_config_find_system_r(git_buf *path) @@ -472,37 +566,244 @@ int git_config_find_system_r(git_buf *path) int git_config_find_system(char *system_config_path, size_t length) { - git_buf path = GIT_BUF_INIT; - int ret = git_config_find_system_r(&path); + return git_config__find_file_to_path( + system_config_path, length, git_config_find_system_r); +} - if (ret < 0) { - git_buf_free(&path); - return ret; +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_find_global_r(&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, + GIT_CONFIG_LEVEL_XDG, 0); + + if (!error && !git_config_find_system_r(&buf)) + error = git_config_add_file_ondisk(cfg, buf.ptr, + GIT_CONFIG_LEVEL_SYSTEM, 0); + + git_buf_free(&buf); + + if (error && cfg) { + git_config_free(cfg); + cfg = NULL; } - if (path.size >= length) { - git_buf_free(&path); - giterr_set(GITERR_NOMEMORY, - "Path is to long to fit on the given buffer"); - return -1; + *out = cfg; + + return error; +} + +/*********** + * Parsers + ***********/ +int git_config_lookup_map_value( + int *out, + const git_cvar_map *maps, + size_t map_n, + const char *value) +{ + size_t i; + + if (!value) + goto fail_parse; + + for (i = 0; i < map_n; ++i) { + const git_cvar_map *m = maps + i; + + switch (m->cvar_type) { + case GIT_CVAR_FALSE: + case GIT_CVAR_TRUE: { + int bool_val; + + if (git__parse_bool(&bool_val, value) == 0 && + bool_val == (int)m->cvar_type) { + *out = m->map_value; + return 0; + } + break; + } + + case GIT_CVAR_INT32: + if (git_config_parse_int32(out, value) == 0) + return 0; + break; + + case GIT_CVAR_STRING: + if (strcasecmp(value, m->str_match) == 0) { + *out = m->map_value; + return 0; + } + break; + } } - git_buf_copy_cstr(system_config_path, length, &path); - git_buf_free(&path); +fail_parse: + giterr_set(GITERR_CONFIG, "Failed to map '%s'", value); + return -1; +} + +int git_config_parse_bool(int *out, const char *value) +{ + if (git__parse_bool(out, value) == 0) + return 0; + + if (git_config_parse_int32(out, value) == 0) { + *out = !!(*out); + return 0; + } + + giterr_set(GITERR_CONFIG, "Failed to parse '%s' as a boolean value", value); + return -1; +} + +int git_config_parse_int64(int64_t *out, const char *value) +{ + const char *num_end; + int64_t num; + + if (git__strtol64(&num, value, &num_end, 0) < 0) + goto fail_parse; + + switch (*num_end) { + case 'g': + case 'G': + num *= 1024; + /* fallthrough */ + + case 'm': + case 'M': + num *= 1024; + /* fallthrough */ + + case 'k': + case 'K': + num *= 1024; + + /* check that that there are no more characters after the + * given modifier suffix */ + if (num_end[1] != '\0') + return -1; + + /* fallthrough */ + + case '\0': + *out = num; + return 0; + + default: + goto fail_parse; + } + +fail_parse: + giterr_set(GITERR_CONFIG, "Failed to parse '%s' as an integer", value); + return -1; +} + +int git_config_parse_int32(int32_t *out, const char *value) +{ + int64_t tmp; + int32_t truncate; + + if (git_config_parse_int64(&tmp, value) < 0) + goto fail_parse; + + truncate = tmp & 0xFFFFFFFF; + if (truncate != tmp) + goto fail_parse; + + *out = truncate; return 0; + +fail_parse: + giterr_set(GITERR_CONFIG, "Failed to parse '%s' as a 32-bit integer", value); + return -1; } -int git_config_open_global(git_config **out) +struct rename_data { + git_config *config; + git_buf *name; + size_t old_len; + int actual_error; +}; + +static int rename_config_entries_cb( + const git_config_entry *entry, + void *payload) { - int error; - git_buf path = GIT_BUF_INIT; + int error = 0; + struct rename_data *data = (struct rename_data *)payload; + size_t base_len = git_buf_len(data->name); - if ((error = git_config_find_global_r(&path)) < 0) - return error; + if (base_len > 0 && + !(error = git_buf_puts(data->name, entry->name + data->old_len))) + { + error = git_config_set_string( + data->config, git_buf_cstr(data->name), entry->value); - error = git_config_open_ondisk(out, git_buf_cstr(&path)); - git_buf_free(&path); + git_buf_truncate(data->name, base_len); + } + + if (!error) + error = git_config_delete_entry(data->config, entry->name); + + data->actual_error = error; /* preserve actual error code */ return error; } +int git_config_rename_section( + git_repository *repo, + const char *old_section_name, + const char *new_section_name) +{ + git_config *config; + git_buf pattern = GIT_BUF_INIT, replace = GIT_BUF_INIT; + int error = 0; + struct rename_data data; + + git_buf_text_puts_escape_regex(&pattern, old_section_name); + + if ((error = git_buf_puts(&pattern, "\\..+")) < 0) + goto cleanup; + + if ((error = git_repository_config__weakptr(&config, repo)) < 0) + goto cleanup; + + data.config = config; + data.name = &replace; + data.old_len = strlen(old_section_name) + 1; + data.actual_error = 0; + + if ((error = git_buf_join(&replace, '.', new_section_name, "")) < 0) + goto cleanup; + + if (new_section_name != NULL && + (error = git_config_file_normalize_section( + replace.ptr, strchr(replace.ptr, '.'))) < 0) + { + giterr_set( + GITERR_CONFIG, "Invalid config section '%s'", new_section_name); + goto cleanup; + } + + error = git_config_foreach_match( + config, git_buf_cstr(&pattern), rename_config_entries_cb, &data); + + if (error == GIT_EUSER) + error = data.actual_error; + +cleanup: + git_buf_free(&pattern); + git_buf_free(&replace); + + return error; +} diff --git a/src/config.h b/src/config.h index 82e98ce51..c43e47e82 100644 --- a/src/config.h +++ b/src/config.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -12,9 +12,11 @@ #include "vector.h" #include "repository.h" -#define GIT_CONFIG_FILENAME ".gitconfig" -#define GIT_CONFIG_FILENAME_INREPO "config" #define GIT_CONFIG_FILENAME_SYSTEM "gitconfig" +#define GIT_CONFIG_FILENAME_GLOBAL ".gitconfig" +#define GIT_CONFIG_FILENAME_XDG "config" + +#define GIT_CONFIG_FILENAME_INREPO "config" #define GIT_CONFIG_FILE_MODE 0666 struct git_config { @@ -23,11 +25,25 @@ struct git_config { }; 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_parse_bool(int *out, const char *bool_string); +extern int git_config_rename_section( + git_repository *repo, + const char *old_section_name, /* eg "branch.dummy" */ + const char *new_section_name); /* NULL to drop the old section */ -extern int git_config_lookup_map_value( - git_cvar_map *maps, size_t map_n, const char *value, int *out); +/** + * Create a configuration file backend for ondisk files + * + * These are the normal `.gitconfig` files that Core Git + * processes. Note that you first have to add this file to a + * configuration object before you can query it for configuration + * variables. + * + * @param out the new backend + * @param path where the config file is located + */ +extern int git_config_file__ondisk(struct git_config_backend **out, const char *path); #endif diff --git a/src/config_cache.c b/src/config_cache.c index ca9602e56..2f36df7d1 100644 --- a/src/config_cache.c +++ b/src/config_cache.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -24,7 +24,7 @@ struct map_data { * core.eol * Sets the line ending type to use in the working directory for * files that have the text property set. Alternatives are lf, crlf - * and native, which uses the platform’s native line ending. The default + * 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. */ diff --git a/src/config_file.c b/src/config_file.c index cbc48bcd9..8b51ab21b 100644 --- a/src/config_file.c +++ b/src/config_file.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -10,6 +10,7 @@ #include "fileops.h" #include "filebuf.h" #include "buffer.h" +#include "buf_text.h" #include "git2/config.h" #include "git2/types.h" #include "strmap.h" @@ -22,15 +23,9 @@ GIT__USE_STRMAP; typedef struct cvar_t { struct cvar_t *next; - char *key; /* TODO: we might be able to get rid of this */ - char *value; + git_config_entry *entry; } cvar_t; -typedef struct { - struct cvar_t *head; - struct cvar_t *tail; -} cvar_t_list; - #define CVAR_LIST_HEAD(list) ((list)->head) #define CVAR_LIST_TAIL(list) ((list)->tail) @@ -70,7 +65,7 @@ typedef struct { (iter) = (tmp)) typedef struct { - git_config_file parent; + git_config_backend parent; git_strmap *values; @@ -81,12 +76,17 @@ typedef struct { int eof; } reader; - char *file_path; + char *file_path; + time_t file_mtime; + size_t file_size; + + unsigned int level; } diskfile_backend; -static int config_parse(diskfile_backend *cfg_file); +static int config_parse(diskfile_backend *cfg_file, unsigned int 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); static void set_parse_error(diskfile_backend *backend, int col, const char *error_str) { @@ -99,11 +99,35 @@ static void cvar_free(cvar_t *var) if (var == NULL) return; - git__free(var->key); - git__free(var->value); + git__free((char*)var->entry->name); + git__free((char *)var->entry->value); + git__free(var->entry); git__free(var); } +int git_config_file_normalize_section(char *start, char *end) +{ + char *scan; + + if (start == end) + return GIT_EINVALIDSPEC; + + /* Validate and downcase range */ + for (scan = start; *scan; ++scan) { + if (end && scan >= end) + break; + if (isalnum(*scan)) + *scan = tolower(*scan); + else if (*scan != '-' || scan == start) + return GIT_EINVALIDSPEC; + } + + if (scan == start) + return GIT_EINVALIDSPEC; + + return 0; +} + /* Take something the user gave us and make it nice for our hash function */ static int normalize_name(const char *in, char **out) { @@ -117,19 +141,26 @@ static int normalize_name(const char *in, char **out) fdot = strchr(name, '.'); ldot = strrchr(name, '.'); - if (fdot == NULL || ldot == NULL) { - git__free(name); - giterr_set(GITERR_CONFIG, - "Invalid variable name: '%s'", in); - return -1; - } + if (fdot == NULL || fdot == name || ldot == NULL || !ldot[1]) + goto invalid; + + /* Validate and downcase up to first dot and after last dot */ + if (git_config_file_normalize_section(name, fdot) < 0 || + git_config_file_normalize_section(ldot + 1, NULL) < 0) + goto invalid; - /* Downcase up to the first dot and after the last one */ - git__strntolower(name, fdot - name); - git__strtolower(ldot); + /* If there is a middle range, make sure it doesn't have newlines */ + while (fdot < ldot) + if (*fdot++ == '\n') + goto invalid; *out = name; return 0; + +invalid: + git__free(name); + giterr_set(GITERR_CONFIG, "Invalid config item name '%s'", in); + return GIT_EINVALIDSPEC; } static void free_vars(git_strmap *values) @@ -149,33 +180,61 @@ static void free_vars(git_strmap *values) git_strmap_free(values); } -static int config_open(git_config_file *cfg) +static int config_open(git_config_backend *cfg, unsigned int level) { int res; diskfile_backend *b = (diskfile_backend *)cfg; + b->level = level; + b->values = git_strmap_alloc(); GITERR_CHECK_ALLOC(b->values); git_buf_init(&b->reader.buffer, 0); - res = git_futils_readbuffer(&b->reader.buffer, b->file_path); + res = git_futils_readbuffer_updated( + &b->reader.buffer, b->file_path, &b->file_mtime, &b->file_size, NULL); /* It's fine if the file doesn't exist */ if (res == GIT_ENOTFOUND) return 0; - if (res < 0 || config_parse(b) < 0) { + if (res < 0 || (res = config_parse(b, level)) < 0) { free_vars(b->values); b->values = NULL; - git_buf_free(&b->reader.buffer); - return -1; } git_buf_free(&b->reader.buffer); - return 0; + return res; } -static void backend_free(git_config_file *_backend) +static int config_refresh(git_config_backend *cfg) +{ + int res, updated = 0; + diskfile_backend *b = (diskfile_backend *)cfg; + git_strmap *old_values; + + res = git_futils_readbuffer_updated( + &b->reader.buffer, b->file_path, &b->file_mtime, &b->file_size, &updated); + if (res < 0 || !updated) + return (res == GIT_ENOTFOUND) ? 0 : res; + + /* need to reload - store old values and prep for reload */ + old_values = b->values; + b->values = git_strmap_alloc(); + GITERR_CHECK_ALLOC(b->values); + + if ((res = config_parse(b, b->level)) < 0) { + free_vars(b->values); + b->values = old_values; + } else { + free_vars(old_values); + } + + git_buf_free(&b->reader.buffer); + return res; +} + +static void backend_free(git_config_backend *_backend) { diskfile_backend *backend = (diskfile_backend *)_backend; @@ -187,37 +246,63 @@ static void backend_free(git_config_file *_backend) git__free(backend); } -static int file_foreach(git_config_file *backend, int (*fn)(const char *, const char *, void *), void *data) +static int file_foreach( + git_config_backend *backend, + const char *regexp, + int (*fn)(const git_config_entry *, void *), + void *data) { diskfile_backend *b = (diskfile_backend *)backend; - cvar_t *var; + cvar_t *var, *next_var; const char *key; + regex_t regex; + int result = 0; if (!b->values) return 0; + if (regexp != NULL) { + if ((result = regcomp(®ex, regexp, REG_EXTENDED)) < 0) { + giterr_set_regex(®ex, result); + regfree(®ex); + return -1; + } + } + git_strmap_foreach(b->values, key, var, - do { - if (fn(key, var->value, data) < 0) - break; + for (; var != NULL; var = next_var) { + next_var = CVAR_LIST_NEXT(var); - var = CVAR_LIST_NEXT(var); - } while (var != NULL); + /* skip non-matching keys if regexp was provided */ + if (regexp && regexec(®ex, key, 0, NULL, 0) != 0) + continue; + + /* abort iterator on non-zero return value */ + if (fn(var->entry, data)) { + giterr_clear(); + result = GIT_EUSER; + goto cleanup; + } + } ); - return 0; +cleanup: + if (regexp != NULL) + regfree(®ex); + + return result; } -static int config_set(git_config_file *cfg, const char *name, const char *value) +static int config_set(git_config_backend *cfg, const char *name, const char *value) { cvar_t *var = NULL, *old_var; diskfile_backend *b = (diskfile_backend *)cfg; - char *key; + char *key, *esc_value = NULL; khiter_t pos; - int rval; + int rval, ret; - if (normalize_name(name, &key) < 0) - return -1; + if ((rval = normalize_name(name, &key)) < 0) + return rval; /* * Try to find it in the existing values and update it if it @@ -229,40 +314,57 @@ static int config_set(git_config_file *cfg, const char *name, const char *value) char *tmp = NULL; git__free(key); + if (existing->next != NULL) { giterr_set(GITERR_CONFIG, "Multivar incompatible with simple set"); return -1; } + /* don't update if old and new values already match */ + if ((!existing->entry->value && !value) || + (existing->entry->value && value && !strcmp(existing->entry->value, value))) + return 0; + if (value) { tmp = git__strdup(value); GITERR_CHECK_ALLOC(tmp); + esc_value = escape_value(value); + GITERR_CHECK_ALLOC(esc_value); } - git__free(existing->value); - existing->value = tmp; + git__free((void *)existing->entry->value); + existing->entry->value = tmp; + + ret = config_write(b, existing->entry->name, NULL, esc_value); - return config_write(b, existing->key, NULL, value); + git__free(esc_value); + return ret; } var = git__malloc(sizeof(cvar_t)); GITERR_CHECK_ALLOC(var); - memset(var, 0x0, sizeof(cvar_t)); + var->entry = git__malloc(sizeof(git_config_entry)); + GITERR_CHECK_ALLOC(var->entry); + memset(var->entry, 0x0, sizeof(git_config_entry)); - var->key = key; - var->value = NULL; + var->entry->name = key; + var->entry->value = NULL; if (value) { - var->value = git__strdup(value); - GITERR_CHECK_ALLOC(var->value); + var->entry->value = git__strdup(value); + GITERR_CHECK_ALLOC(var->entry->value); + esc_value = escape_value(value); + GITERR_CHECK_ALLOC(esc_value); } - if (config_write(b, key, NULL, value) < 0) { + if (config_write(b, key, NULL, esc_value) < 0) { + git__free(esc_value); cvar_free(var); return -1; } + git__free(esc_value); git_strmap_insert2(b->values, key, var, old_var, rval); if (rval < 0) return -1; @@ -275,14 +377,15 @@ static int config_set(git_config_file *cfg, const char *name, const char *value) /* * Internal function that actually gets the value in string form */ -static int config_get(git_config_file *cfg, const char *name, const char **out) +static int config_get(const git_config_backend *cfg, const char *name, const git_config_entry **out) { diskfile_backend *b = (diskfile_backend *)cfg; char *key; khiter_t pos; + int error; - if (normalize_name(name, &key) < 0) - return -1; + if ((error = normalize_name(name, &key)) < 0) + return error; pos = git_strmap_lookup_index(b->values, key); git__free(key); @@ -291,25 +394,26 @@ static int config_get(git_config_file *cfg, const char *name, const char **out) if (!git_strmap_valid_index(b->values, pos)) return GIT_ENOTFOUND; - *out = ((cvar_t *)git_strmap_value_at(b->values, pos))->value; + *out = ((cvar_t *)git_strmap_value_at(b->values, pos))->entry; return 0; } static int config_get_multivar( - git_config_file *cfg, + git_config_backend *cfg, const char *name, const char *regex_str, - int (*fn)(const char *, void *), + int (*fn)(const git_config_entry *, void *), void *data) { cvar_t *var; diskfile_backend *b = (diskfile_backend *)cfg; char *key; khiter_t pos; + int error; - if (normalize_name(name, &key) < 0) - return -1; + if ((error = normalize_name(name, &key)) < 0) + return error; pos = git_strmap_lookup_index(b->values, key); git__free(key); @@ -327,16 +431,17 @@ static int config_get_multivar( result = regcomp(®ex, regex_str, REG_EXTENDED); if (result < 0) { giterr_set_regex(®ex, result); + regfree(®ex); return -1; } /* and throw the callback only on the variables that * match the regex */ do { - if (regexec(®ex, var->value, 0, NULL, 0) == 0) { + if (regexec(®ex, var->entry->value, 0, NULL, 0) == 0) { /* early termination by the user is not an error; * just break and return successfully */ - if (fn(var->value, data) < 0) + if (fn(var->entry, data) < 0) break; } @@ -348,7 +453,7 @@ static int config_get_multivar( do { /* early termination by the user is not an error; * just break and return successfully */ - if (fn(var->value, data) < 0) + if (fn(var->entry, data) < 0) break; var = var->next; @@ -359,7 +464,7 @@ static int config_get_multivar( } static int config_set_multivar( - git_config_file *cfg, const char *name, const char *regexp, const char *value) + git_config_backend *cfg, const char *name, const char *regexp, const char *value) { int replaced = 0; cvar_t *var, *newvar; @@ -371,8 +476,8 @@ static int config_set_multivar( assert(regexp); - if (normalize_name(name, &key) < 0) - return -1; + if ((result = normalize_name(name, &key)) < 0) + return result; pos = git_strmap_lookup_index(b->values, key); if (!git_strmap_valid_index(b->values, pos)) { @@ -386,16 +491,17 @@ static int config_set_multivar( if (result < 0) { git__free(key); giterr_set_regex(&preg, result); + regfree(&preg); return -1; } for (;;) { - if (regexec(&preg, var->value, 0, NULL, 0) == 0) { + if (regexec(&preg, var->entry->value, 0, NULL, 0) == 0) { char *tmp = git__strdup(value); GITERR_CHECK_ALLOC(tmp); - git__free(var->value); - var->value = tmp; + git__free((void *)var->entry->value); + var->entry->value = tmp; replaced = 1; } @@ -409,14 +515,18 @@ static int config_set_multivar( if (!replaced) { newvar = git__malloc(sizeof(cvar_t)); GITERR_CHECK_ALLOC(newvar); - memset(newvar, 0x0, sizeof(cvar_t)); + newvar->entry = git__malloc(sizeof(git_config_entry)); + GITERR_CHECK_ALLOC(newvar->entry); + memset(newvar->entry, 0x0, sizeof(git_config_entry)); + + newvar->entry->name = git__strdup(var->entry->name); + GITERR_CHECK_ALLOC(newvar->entry->name); - newvar->key = git__strdup(var->key); - GITERR_CHECK_ALLOC(newvar->key); + newvar->entry->value = git__strdup(value); + GITERR_CHECK_ALLOC(newvar->entry->value); - newvar->value = git__strdup(value); - GITERR_CHECK_ALLOC(newvar->value); + newvar->entry->level = var->entry->level; var->next = newvar; } @@ -429,7 +539,7 @@ static int config_set_multivar( return result; } -static int config_delete(git_config_file *cfg, const char *name) +static int config_delete(git_config_backend *cfg, const char *name) { cvar_t *var; diskfile_backend *b = (diskfile_backend *)cfg; @@ -437,14 +547,16 @@ static int config_delete(git_config_file *cfg, const char *name) int result; khiter_t pos; - if (normalize_name(name, &key) < 0) - return -1; + if ((result = normalize_name(name, &key)) < 0) + return result; pos = git_strmap_lookup_index(b->values, key); git__free(key); - if (!git_strmap_valid_index(b->values, pos)) + if (!git_strmap_valid_index(b->values, pos)) { + giterr_set(GITERR_CONFIG, "Could not find key '%s' to delete", name); return GIT_ENOTFOUND; + } var = git_strmap_value_at(b->values, pos); @@ -455,20 +567,20 @@ static int config_delete(git_config_file *cfg, const char *name) git_strmap_delete_at(b->values, pos); - result = config_write(b, var->key, NULL, NULL); + result = config_write(b, var->entry->name, NULL, NULL); cvar_free(var); return result; } -int git_config_file__ondisk(git_config_file **out, const char *path) +int git_config_file__ondisk(git_config_backend **out, const char *path) { diskfile_backend *backend; - backend = git__malloc(sizeof(diskfile_backend)); + backend = git__calloc(1, sizeof(diskfile_backend)); GITERR_CHECK_ALLOC(backend); - memset(backend, 0x0, sizeof(diskfile_backend)); + backend->parent.version = GIT_CONFIG_BACKEND_VERSION; backend->file_path = git__strdup(path); GITERR_CHECK_ALLOC(backend->file_path); @@ -480,9 +592,10 @@ int git_config_file__ondisk(git_config_file **out, const char *path) backend->parent.set_multivar = config_set_multivar; backend->parent.del = config_delete; backend->parent.foreach = file_foreach; + backend->parent.refresh = config_refresh; backend->parent.free = backend_free; - *out = (git_config_file *)backend; + *out = (git_config_backend *)backend; return 0; } @@ -774,17 +887,14 @@ fail_parse: static int skip_bom(diskfile_backend *cfg) { - static const char utf8_bom[] = "\xef\xbb\xbf"; - - if (cfg->reader.buffer.size < sizeof(utf8_bom)) - return 0; + git_bom_t bom; + int bom_offset = git_buf_text_detect_bom(&bom, + &cfg->reader.buffer, cfg->reader.read_ptr - cfg->reader.buffer.ptr); - if (memcmp(cfg->reader.read_ptr, utf8_bom, sizeof(utf8_bom)) == 0) - cfg->reader.read_ptr += sizeof(utf8_bom); + if (bom == GIT_BOM_UTF8) + cfg->reader.read_ptr += bom_offset; - /* TODO: the reference implementation does pretty stupid - shit with the BoM - */ + /* TODO: reference implementation is pretty stupid with BoM */ return 0; } @@ -844,7 +954,7 @@ static int strip_comments(char *line, int in_quotes) } /* skip any space at the end */ - if (git__isspace(ptr[-1])) { + if (ptr > line && git__isspace(ptr[-1])) { ptr--; } ptr[0] = '\0'; @@ -852,7 +962,7 @@ static int strip_comments(char *line, int in_quotes) return quote_count; } -static int config_parse(diskfile_backend *cfg_file) +static int config_parse(diskfile_backend *cfg_file, unsigned int level) { int c; char *current_section = NULL; @@ -900,8 +1010,10 @@ static int config_parse(diskfile_backend *cfg_file) var = git__malloc(sizeof(cvar_t)); GITERR_CHECK_ALLOC(var); - memset(var, 0x0, sizeof(cvar_t)); + var->entry = git__malloc(sizeof(git_config_entry)); + GITERR_CHECK_ALLOC(var->entry); + memset(var->entry, 0x0, sizeof(git_config_entry)); git__strtolower(var_name); git_buf_printf(&buf, "%s.%s", current_section, var_name); @@ -910,13 +1022,14 @@ static int config_parse(diskfile_backend *cfg_file) if (git_buf_oom(&buf)) return -1; - var->key = git_buf_detach(&buf); - var->value = var_value; + var->entry->name = git_buf_detach(&buf); + var->entry->value = var_value; + var->entry->level = level; /* Add or append the new config option */ - pos = git_strmap_lookup_index(cfg_file->values, var->key); + pos = git_strmap_lookup_index(cfg_file->values, var->entry->name); if (!git_strmap_valid_index(cfg_file->values, pos)) { - git_strmap_insert(cfg_file->values, var->key, var, result); + git_strmap_insert(cfg_file->values, var->entry->name, var, result); if (result < 0) break; result = 0; @@ -948,9 +1061,12 @@ static int write_section(git_filebuf *file, const char *key) if (dot == NULL) { git_buf_puts(&buf, key); } else { + char *escaped; git_buf_put(&buf, key, dot - key); - /* TODO: escape */ - git_buf_printf(&buf, " \"%s\"", dot + 1); + escaped = escape_value(dot + 1); + GITERR_CHECK_ALLOC(escaped); + git_buf_printf(&buf, " \"%s\"", escaped); + git__free(escaped); } git_buf_puts(&buf, "]\n"); @@ -1133,6 +1249,10 @@ static int config_write(diskfile_backend *cfg, const char *key, const regex_t *p goto rewrite_fail; } + /* If we are here, there is at least a section line */ + if (cfg->reader.buffer.size > 0 && *(cfg->reader.buffer.ptr + cfg->reader.buffer.size - 1) != '\n') + git_filebuf_write(&file, "\n", 1); + git_filebuf_printf(&file, "\t%s = %s\n", name, value); } } @@ -1140,8 +1260,12 @@ static int config_write(diskfile_backend *cfg, const char *key, const regex_t *p git__free(section); git__free(current_section); + /* refresh stats - if this errors, then commit will error too */ + (void)git_filebuf_stats(&cfg->file_mtime, &cfg->file_size, &file); + result = git_filebuf_commit(&file, GIT_CONFIG_FILE_MODE); git_buf_free(&cfg->reader.buffer); + return result; rewrite_fail: @@ -1153,13 +1277,44 @@ rewrite_fail: return -1; } +static const char *escapes = "ntb\"\\"; +static const char *escaped = "\n\t\b\"\\"; + +/* Escape the values to write them to the file */ +static char *escape_value(const char *ptr) +{ + git_buf buf = GIT_BUF_INIT; + size_t len; + const char *esc; + + assert(ptr); + + len = strlen(ptr); + git_buf_grow(&buf, len); + + while (*ptr != '\0') { + if ((esc = strchr(escaped, *ptr)) != NULL) { + git_buf_putc(&buf, '\\'); + git_buf_putc(&buf, escapes[esc - escaped]); + } else { + git_buf_putc(&buf, *ptr); + } + ptr++; + } + + if (git_buf_oom(&buf)) { + git_buf_free(&buf); + return NULL; + } + + return git_buf_detach(&buf); +} + /* '\"' -> '"' etc */ static char *fixup_line(const char *ptr, int quote_count) { char *str = git__malloc(strlen(ptr) + 1); char *out = str, *esc; - const char *escapes = "ntb\"\\"; - const char *escaped = "\n\t\b\"\\"; if (str == NULL) return NULL; @@ -1196,8 +1351,15 @@ out: static int is_multiline_var(const char *str) { + int count = 0; const char *end = str + strlen(str); - return (end > str) && (end[-1] == '\\'); + while (end > str && end[-1] == '\\') { + count++; + end--; + } + + /* An odd number means last backslash wasn't escaped, so it's multiline */ + return (end > str) && (count & 1); } static int parse_multiline_variable(diskfile_backend *cfg, git_buf *value, int in_quotes) @@ -1272,10 +1434,8 @@ static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_val else value_start = var_end + 1; - if (git__isspace(var_end[-1])) { - do var_end--; - while (git__isspace(var_end[0])); - } + do var_end--; + while (var_end>line && git__isspace(*var_end)); *var_name = git__strndup(line, var_end - line + 1); GITERR_CHECK_ALLOC(*var_name); @@ -1309,8 +1469,10 @@ static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_val else if (value_start[0] != '\0') { *var_value = fixup_line(value_start, 0); GITERR_CHECK_ALLOC(*var_value); + } else { /* equals sign but missing rhs */ + *var_value = git__strdup(""); + GITERR_CHECK_ALLOC(*var_value); } - } git__free(line); diff --git a/src/config_file.h b/src/config_file.h index 0080b5713..7445859c4 100644 --- a/src/config_file.h +++ b/src/config_file.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -9,23 +9,52 @@ #include "git2/config.h" -GIT_INLINE(int) git_config_file_open(git_config_file *cfg) +GIT_INLINE(int) git_config_file_open(git_config_backend *cfg, unsigned int level) { - return cfg->open(cfg); + return cfg->open(cfg, level); } -GIT_INLINE(void) git_config_file_free(git_config_file *cfg) +GIT_INLINE(void) git_config_file_free(git_config_backend *cfg) { cfg->free(cfg); } +GIT_INLINE(int) git_config_file_get_string( + const git_config_entry **out, git_config_backend *cfg, const char *name) +{ + return cfg->get(cfg, name, out); +} + +GIT_INLINE(int) git_config_file_set_string( + git_config_backend *cfg, const char *name, const char *value) +{ + return cfg->set(cfg, name, value); +} + +GIT_INLINE(int) git_config_file_delete( + git_config_backend *cfg, const char *name) +{ + return cfg->del(cfg, name); +} + GIT_INLINE(int) git_config_file_foreach( - git_config_file *cfg, - int (*fn)(const char *key, const char *value, void *data), + git_config_backend *cfg, + int (*fn)(const git_config_entry *entry, void *data), void *data) { - return cfg->foreach(cfg, fn, data); + return cfg->foreach(cfg, NULL, fn, data); } +GIT_INLINE(int) git_config_file_foreach_match( + git_config_backend *cfg, + const char *regexp, + int (*fn)(const git_config_entry *entry, void *data), + void *data) +{ + return cfg->foreach(cfg, regexp, fn, data); +} + +extern int git_config_file_normalize_section(char *start, char *end); + #endif diff --git a/src/crlf.c b/src/crlf.c index 303a46d3b..81268da83 100644 --- a/src/crlf.c +++ b/src/crlf.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -9,9 +9,10 @@ #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; @@ -21,6 +22,8 @@ struct crlf_attrs { struct crlf_filter { git_filter f; struct crlf_attrs attrs; + git_repository *repo; + char path[GIT_FLEX_ARRAY]; }; static int check_crlf(const char *value) @@ -103,36 +106,46 @@ static int crlf_load_attributes(struct crlf_attrs *ca, git_repository *repo, con return -1; } -static int drop_crlf(git_buf *dest, const git_buf *source) +static int has_cr_in_index(git_filter *self) { - const char *scan = source->ptr, *next; - const char *scan_end = git_buf_cstr(source) + git_buf_len(source); + struct crlf_filter *filter = (struct crlf_filter *)self; + git_index *index; + const git_index_entry *entry; + git_blob *blob; + const void *blobcontent; + git_off_t blobsize; + bool found_cr; + + if (git_repository_index__weakptr(&index, filter->repo) < 0) { + giterr_clear(); + return false; + } - /* Main scan loop. Find the next carriage return and copy the - * whole chunk up to that point to the destination buffer. - */ - while ((next = memchr(scan, '\r', scan_end - scan)) != NULL) { - /* copy input up to \r */ - if (next > scan) - git_buf_put(dest, scan, next - scan); + if (!(entry = git_index_get_bypath(index, filter->path, 0)) && + !(entry = git_index_get_bypath(index, filter->path, 1))) + return false; - /* Do not drop \r unless it is followed by \n */ - if (*(next + 1) != '\n') - git_buf_putc(dest, '\r'); + if (!S_ISREG(entry->mode)) /* don't crlf filter non-blobs */ + return true; - scan = next + 1; - } + if (git_blob_lookup(&blob, filter->repo, &entry->oid) < 0) + return false; - /* If there was no \r, then tell the library to skip this filter */ - if (scan == source->ptr) - return -1; + blobcontent = git_blob_rawcontent(blob); + blobsize = git_blob_rawsize(blob); + if (!git__is_sizet(blobsize)) + blobsize = (size_t)-1; - /* Copy remaining input into dest */ - git_buf_put(dest, scan, scan_end - scan); - return 0; + found_cr = (blobcontent != NULL && + blobsize > 0 && + memchr(blobcontent, '\r', (size_t)blobsize) != NULL); + + git_blob_free(blob); + return found_cr; } -static int crlf_apply_to_odb(git_filter *self, git_buf *dest, const git_buf *source) +static int crlf_apply_to_odb( + git_filter *self, git_buf *dest, const git_buf *source) { struct crlf_filter *filter = (struct crlf_filter *)self; @@ -148,8 +161,11 @@ static int crlf_apply_to_odb(git_filter *self, git_buf *dest, const git_buf *sou if (filter->attrs.crlf_action == GIT_CRLF_AUTO || filter->attrs.crlf_action == GIT_CRLF_GUESS) { - git_text_stats stats; - git_text_gather_stats(&stats, source); + git_buf_text_stats stats; + + /* Check heuristics for binary vs text... */ + if (git_buf_text_gather_stats(&stats, source, false)) + return -1; /* * We're currently not going to even try to convert stuff @@ -159,35 +175,94 @@ static int crlf_apply_to_odb(git_filter *self, git_buf *dest, const git_buf *sou if (stats.cr != stats.crlf) return -1; - /* - * And add some heuristics for binary vs text, of course... - */ - if (git_text_is_binary(&stats)) - return -1; - -#if 0 - if (crlf_action == CRLF_GUESS) { + if (filter->attrs.crlf_action == GIT_CRLF_GUESS) { /* * If the file in the index has any CR in it, do not convert. * This is the new safer autocrlf handling. */ - if (has_cr_in_index(path)) - return 0; + if (has_cr_in_index(self)) + return -1; } -#endif if (!stats.cr) return -1; } /* Actually drop the carriage returns */ - return drop_crlf(dest, source); + return git_buf_text_crlf_to_lf(dest, source); +} + +static const char *line_ending(struct crlf_filter *filter) +{ + switch (filter->attrs.crlf_action) { + case GIT_CRLF_BINARY: + case GIT_CRLF_INPUT: + return "\n"; + + case GIT_CRLF_CRLF: + return "\r\n"; + + case GIT_CRLF_AUTO: + case GIT_CRLF_TEXT: + case GIT_CRLF_GUESS: + break; + + default: + goto line_ending_error; + } + + switch (filter->attrs.eol) { + case GIT_EOL_UNSET: + return GIT_EOL_NATIVE == GIT_EOL_CRLF + ? "\r\n" + : "\n"; + + case GIT_EOL_CRLF: + return "\r\n"; + + case GIT_EOL_LF: + return "\n"; + + default: + goto line_ending_error; + } + +line_ending_error: + giterr_set(GITERR_INVALID, "Invalid input to line ending filter"); + return NULL; +} + +static int crlf_apply_to_workdir( + git_filter *self, git_buf *dest, const git_buf *source) +{ + struct crlf_filter *filter = (struct crlf_filter *)self; + const char *workdir_ending = NULL; + + assert(self && dest && source); + + /* Empty file? Nothing to do. */ + if (git_buf_len(source) == 0) + return -1; + + /* Determine proper line ending */ + workdir_ending = line_ending(filter); + if (!workdir_ending) + return -1; + if (!strcmp("\n", workdir_ending)) /* do nothing for \n ending */ + return -1; + + /* for now, only lf->crlf conversion is supported here */ + assert(!strcmp("\r\n", workdir_ending)); + return git_buf_text_lf_to_crlf(dest, source); } -int git_filter_add__crlf_to_odb(git_vector *filters, git_repository *repo, const char *path) +static int find_and_add_filter( + git_vector *filters, git_repository *repo, const char *path, + int (*apply)(struct git_filter *self, git_buf *dest, const git_buf *source)) { struct crlf_attrs ca; struct crlf_filter *filter; + size_t pathlen; int error; /* Load gitattributes for the path */ @@ -196,7 +271,7 @@ int git_filter_add__crlf_to_odb(git_vector *filters, git_repository *repo, const /* * Use the core Git logic to see if we should perform CRLF for this file - * based on its attributes & the value of `core.auto_crlf` + * based on its attributes & the value of `core.autocrlf` */ ca.crlf_action = crlf_input_action(&ca); @@ -206,8 +281,7 @@ int git_filter_add__crlf_to_odb(git_vector *filters, git_repository *repo, const if (ca.crlf_action == GIT_CRLF_GUESS) { int auto_crlf; - if ((error = git_repository__cvar( - &auto_crlf, repo, GIT_CVAR_AUTO_CRLF)) < 0) + if ((error = git_repository__cvar(&auto_crlf, repo, GIT_CVAR_AUTO_CRLF)) < 0) return error; if (auto_crlf == GIT_AUTO_CRLF_FALSE) @@ -216,13 +290,27 @@ int git_filter_add__crlf_to_odb(git_vector *filters, git_repository *repo, const /* If we're good, we create a new filter object and push it * into the filters array */ - filter = git__malloc(sizeof(struct crlf_filter)); + pathlen = strlen(path); + filter = git__malloc(sizeof(struct crlf_filter) + pathlen + 1); GITERR_CHECK_ALLOC(filter); - filter->f.apply = &crlf_apply_to_odb; + filter->f.apply = apply; filter->f.do_free = NULL; memcpy(&filter->attrs, &ca, sizeof(struct crlf_attrs)); + filter->repo = repo; + memcpy(filter->path, path, pathlen + 1); return git_vector_insert(filters, filter); } +int git_filter_add__crlf_to_odb( + git_vector *filters, git_repository *repo, const char *path) +{ + return find_and_add_filter(filters, repo, path, &crlf_apply_to_odb); +} + +int git_filter_add__crlf_to_workdir( + git_vector *filters, git_repository *repo, const char *path) +{ + return find_and_add_filter(filters, repo, path, &crlf_apply_to_workdir); +} diff --git a/src/date.c b/src/date.c new file mode 100644 index 000000000..ce1721a0b --- /dev/null +++ b/src/date.c @@ -0,0 +1,876 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ + +#include "common.h" + +#ifndef GIT_WIN32 +#include <sys/time.h> +#endif + +#include "util.h" +#include "cache.h" +#include "posix.h" + +#include <ctype.h> +#include <time.h> + +typedef enum { + DATE_NORMAL = 0, + DATE_RELATIVE, + DATE_SHORT, + DATE_LOCAL, + DATE_ISO8601, + DATE_RFC2822, + DATE_RAW +} date_mode; + +/* + * This is like mktime, but without normalization of tm_wday and tm_yday. + */ +static git_time_t tm_to_time_t(const struct tm *tm) +{ + static const int mdays[] = { + 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 + }; + int year = tm->tm_year - 70; + int month = tm->tm_mon; + int day = tm->tm_mday; + + if (year < 0 || year > 129) /* algo only works for 1970-2099 */ + return -1; + if (month < 0 || month > 11) /* array bounds */ + return -1; + if (month < 2 || (year + 2) % 4) + day--; + if (tm->tm_hour < 0 || tm->tm_min < 0 || tm->tm_sec < 0) + return -1; + return (year * 365 + (year + 1) / 4 + mdays[month] + day) * 24*60*60UL + + tm->tm_hour * 60*60 + tm->tm_min * 60 + tm->tm_sec; +} + +static const char *month_names[] = { + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" +}; + +static const char *weekday_names[] = { + "Sundays", "Mondays", "Tuesdays", "Wednesdays", "Thursdays", "Fridays", "Saturdays" +}; + + + +/* + * Check these. And note how it doesn't do the summer-time conversion. + * + * In my world, it's always summer, and things are probably a bit off + * in other ways too. + */ +static const struct { + const char *name; + int offset; + int dst; +} timezone_names[] = { + { "IDLW", -12, 0, }, /* International Date Line West */ + { "NT", -11, 0, }, /* Nome */ + { "CAT", -10, 0, }, /* Central Alaska */ + { "HST", -10, 0, }, /* Hawaii Standard */ + { "HDT", -10, 1, }, /* Hawaii Daylight */ + { "YST", -9, 0, }, /* Yukon Standard */ + { "YDT", -9, 1, }, /* Yukon Daylight */ + { "PST", -8, 0, }, /* Pacific Standard */ + { "PDT", -8, 1, }, /* Pacific Daylight */ + { "MST", -7, 0, }, /* Mountain Standard */ + { "MDT", -7, 1, }, /* Mountain Daylight */ + { "CST", -6, 0, }, /* Central Standard */ + { "CDT", -6, 1, }, /* Central Daylight */ + { "EST", -5, 0, }, /* Eastern Standard */ + { "EDT", -5, 1, }, /* Eastern Daylight */ + { "AST", -3, 0, }, /* Atlantic Standard */ + { "ADT", -3, 1, }, /* Atlantic Daylight */ + { "WAT", -1, 0, }, /* West Africa */ + + { "GMT", 0, 0, }, /* Greenwich Mean */ + { "UTC", 0, 0, }, /* Universal (Coordinated) */ + { "Z", 0, 0, }, /* Zulu, alias for UTC */ + + { "WET", 0, 0, }, /* Western European */ + { "BST", 0, 1, }, /* British Summer */ + { "CET", +1, 0, }, /* Central European */ + { "MET", +1, 0, }, /* Middle European */ + { "MEWT", +1, 0, }, /* Middle European Winter */ + { "MEST", +1, 1, }, /* Middle European Summer */ + { "CEST", +1, 1, }, /* Central European Summer */ + { "MESZ", +1, 1, }, /* Middle European Summer */ + { "FWT", +1, 0, }, /* French Winter */ + { "FST", +1, 1, }, /* French Summer */ + { "EET", +2, 0, }, /* Eastern Europe */ + { "EEST", +2, 1, }, /* Eastern European Daylight */ + { "WAST", +7, 0, }, /* West Australian Standard */ + { "WADT", +7, 1, }, /* West Australian Daylight */ + { "CCT", +8, 0, }, /* China Coast */ + { "JST", +9, 0, }, /* Japan Standard */ + { "EAST", +10, 0, }, /* Eastern Australian Standard */ + { "EADT", +10, 1, }, /* Eastern Australian Daylight */ + { "GST", +10, 0, }, /* Guam Standard */ + { "NZT", +12, 0, }, /* New Zealand */ + { "NZST", +12, 0, }, /* New Zealand Standard */ + { "NZDT", +12, 1, }, /* New Zealand Daylight */ + { "IDLE", +12, 0, }, /* International Date Line East */ +}; + +static size_t match_string(const char *date, const char *str) +{ + size_t i = 0; + + for (i = 0; *date; date++, str++, i++) { + if (*date == *str) + continue; + if (toupper(*date) == toupper(*str)) + continue; + if (!isalnum(*date)) + break; + return 0; + } + return i; +} + +static int skip_alpha(const char *date) +{ + int i = 0; + do { + i++; + } while (isalpha(date[i])); + return i; +} + +/* +* Parse month, weekday, or timezone name +*/ +static size_t match_alpha(const char *date, struct tm *tm, int *offset) +{ + unsigned int i; + + for (i = 0; i < 12; i++) { + size_t match = match_string(date, month_names[i]); + if (match >= 3) { + tm->tm_mon = i; + return match; + } + } + + for (i = 0; i < 7; i++) { + size_t match = match_string(date, weekday_names[i]); + if (match >= 3) { + tm->tm_wday = i; + return match; + } + } + + for (i = 0; i < ARRAY_SIZE(timezone_names); i++) { + size_t match = match_string(date, timezone_names[i].name); + if (match >= 3 || match == strlen(timezone_names[i].name)) { + int off = timezone_names[i].offset; + + /* This is bogus, but we like summer */ + off += timezone_names[i].dst; + + /* Only use the tz name offset if we don't have anything better */ + if (*offset == -1) + *offset = 60*off; + + return match; + } + } + + if (match_string(date, "PM") == 2) { + tm->tm_hour = (tm->tm_hour % 12) + 12; + return 2; + } + + if (match_string(date, "AM") == 2) { + tm->tm_hour = (tm->tm_hour % 12) + 0; + return 2; + } + + /* BAD */ + return skip_alpha(date); +} + +static int is_date(int year, int month, int day, struct tm *now_tm, time_t now, struct tm *tm) +{ + if (month > 0 && month < 13 && day > 0 && day < 32) { + struct tm check = *tm; + struct tm *r = (now_tm ? &check : tm); + time_t specified; + + r->tm_mon = month - 1; + r->tm_mday = day; + if (year == -1) { + if (!now_tm) + return 1; + r->tm_year = now_tm->tm_year; + } + else if (year >= 1970 && year < 2100) + r->tm_year = year - 1900; + else if (year > 70 && year < 100) + r->tm_year = year; + else if (year < 38) + r->tm_year = year + 100; + else + return 0; + if (!now_tm) + return 1; + + specified = tm_to_time_t(r); + + /* Be it commit time or author time, it does not make + * sense to specify timestamp way into the future. Make + * sure it is not later than ten days from now... + */ + if (now + 10*24*3600 < specified) + return 0; + tm->tm_mon = r->tm_mon; + tm->tm_mday = r->tm_mday; + if (year != -1) + tm->tm_year = r->tm_year; + return 1; + } + return 0; +} + +static size_t match_multi_number(unsigned long num, char c, const char *date, char *end, struct tm *tm) +{ + time_t now; + struct tm now_tm; + struct tm *refuse_future; + long num2, num3; + + num2 = strtol(end+1, &end, 10); + num3 = -1; + if (*end == c && isdigit(end[1])) + num3 = strtol(end+1, &end, 10); + + /* Time? Date? */ + switch (c) { + case ':': + if (num3 < 0) + num3 = 0; + if (num < 25 && num2 >= 0 && num2 < 60 && num3 >= 0 && num3 <= 60) { + tm->tm_hour = num; + tm->tm_min = num2; + tm->tm_sec = num3; + break; + } + return 0; + + case '-': + case '/': + case '.': + now = time(NULL); + refuse_future = NULL; + if (p_gmtime_r(&now, &now_tm)) + refuse_future = &now_tm; + + if (num > 70) { + /* yyyy-mm-dd? */ + if (is_date(num, num2, num3, refuse_future, now, tm)) + break; + /* yyyy-dd-mm? */ + if (is_date(num, num3, num2, refuse_future, now, tm)) + break; + } + /* Our eastern European friends say dd.mm.yy[yy] + * is the norm there, so giving precedence to + * mm/dd/yy[yy] form only when separator is not '.' + */ + if (c != '.' && + is_date(num3, num, num2, refuse_future, now, tm)) + break; + /* European dd.mm.yy[yy] or funny US dd/mm/yy[yy] */ + if (is_date(num3, num2, num, refuse_future, now, tm)) + break; + /* Funny European mm.dd.yy */ + if (c == '.' && + is_date(num3, num, num2, refuse_future, now, tm)) + break; + return 0; + } + return end - date; +} + +/* + * Have we filled in any part of the time/date yet? + * We just do a binary 'and' to see if the sign bit + * is set in all the values. + */ +static int nodate(struct tm *tm) +{ + return (tm->tm_year & + tm->tm_mon & + tm->tm_mday & + tm->tm_hour & + tm->tm_min & + tm->tm_sec) < 0; +} + +/* + * We've seen a digit. Time? Year? Date? + */ +static size_t match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt) +{ + size_t n; + char *end; + unsigned long num; + + num = strtoul(date, &end, 10); + + /* + * Seconds since 1970? We trigger on that for any numbers with + * more than 8 digits. This is because we don't want to rule out + * numbers like 20070606 as a YYYYMMDD date. + */ + if (num >= 100000000 && nodate(tm)) { + time_t time = num; + if (p_gmtime_r(&time, tm)) { + *tm_gmt = 1; + return end - date; + } + } + + /* + * Check for special formats: num[-.:/]num[same]num + */ + switch (*end) { + case ':': + case '.': + case '/': + case '-': + if (isdigit(end[1])) { + size_t match = match_multi_number(num, *end, date, end, tm); + if (match) + return match; + } + } + + /* + * None of the special formats? Try to guess what + * the number meant. We use the number of digits + * to make a more educated guess.. + */ + n = 0; + do { + n++; + } while (isdigit(date[n])); + + /* Four-digit year or a timezone? */ + if (n == 4) { + if (num <= 1400 && *offset == -1) { + unsigned int minutes = num % 100; + unsigned int hours = num / 100; + *offset = hours*60 + minutes; + } else if (num > 1900 && num < 2100) + tm->tm_year = num - 1900; + return n; + } + + /* + * Ignore lots of numerals. We took care of 4-digit years above. + * Days or months must be one or two digits. + */ + if (n > 2) + return n; + + /* + * NOTE! We will give precedence to day-of-month over month or + * year numbers in the 1-12 range. So 05 is always "mday 5", + * unless we already have a mday.. + * + * IOW, 01 Apr 05 parses as "April 1st, 2005". + */ + if (num > 0 && num < 32 && tm->tm_mday < 0) { + tm->tm_mday = num; + return n; + } + + /* Two-digit year? */ + if (n == 2 && tm->tm_year < 0) { + if (num < 10 && tm->tm_mday >= 0) { + tm->tm_year = num + 100; + return n; + } + if (num >= 70) { + tm->tm_year = num; + return n; + } + } + + if (num > 0 && num < 13 && tm->tm_mon < 0) + tm->tm_mon = num-1; + + return n; +} + +static size_t match_tz(const char *date, int *offp) +{ + char *end; + int hour = strtoul(date + 1, &end, 10); + size_t n = end - (date + 1); + int min = 0; + + if (n == 4) { + /* hhmm */ + min = hour % 100; + hour = hour / 100; + } else if (n != 2) { + min = 99; /* random stuff */ + } else if (*end == ':') { + /* hh:mm? */ + min = strtoul(end + 1, &end, 10); + if (end - (date + 1) != 5) + min = 99; /* random stuff */ + } /* otherwise we parsed "hh" */ + + /* + * Don't accept any random stuff. Even though some places have + * offset larger than 12 hours (e.g. Pacific/Kiritimati is at + * UTC+14), there is something wrong if hour part is much + * larger than that. We might also want to check that the + * minutes are divisible by 15 or something too. (Offset of + * Kathmandu, Nepal is UTC+5:45) + */ + if (min < 60 && hour < 24) { + int offset = hour * 60 + min; + if (*date == '-') + offset = -offset; + *offp = offset; + } + return end - date; +} + +/* + * Parse a string like "0 +0000" as ancient timestamp near epoch, but + * only when it appears not as part of any other string. + */ +static int match_object_header_date(const char *date, git_time_t *timestamp, int *offset) +{ + char *end; + unsigned long stamp; + int ofs; + + if (*date < '0' || '9' <= *date) + return -1; + stamp = strtoul(date, &end, 10); + if (*end != ' ' || stamp == ULONG_MAX || (end[1] != '+' && end[1] != '-')) + return -1; + date = end + 2; + ofs = strtol(date, &end, 10); + if ((*end != '\0' && (*end != '\n')) || end != date + 4) + return -1; + ofs = (ofs / 100) * 60 + (ofs % 100); + if (date[-1] == '-') + ofs = -ofs; + *timestamp = stamp; + *offset = ofs; + return 0; +} + +/* Gr. strptime is crap for this; it doesn't have a way to require RFC2822 + (i.e. English) day/month names, and it doesn't work correctly with %z. */ +static int parse_date_basic(const char *date, git_time_t *timestamp, int *offset) +{ + struct tm tm; + int tm_gmt; + git_time_t dummy_timestamp; + int dummy_offset; + + if (!timestamp) + timestamp = &dummy_timestamp; + if (!offset) + offset = &dummy_offset; + + memset(&tm, 0, sizeof(tm)); + tm.tm_year = -1; + tm.tm_mon = -1; + tm.tm_mday = -1; + tm.tm_isdst = -1; + tm.tm_hour = -1; + tm.tm_min = -1; + tm.tm_sec = -1; + *offset = -1; + tm_gmt = 0; + + if (*date == '@' && + !match_object_header_date(date + 1, timestamp, offset)) + return 0; /* success */ + for (;;) { + size_t match = 0; + unsigned char c = *date; + + /* Stop at end of string or newline */ + if (!c || c == '\n') + break; + + if (isalpha(c)) + match = match_alpha(date, &tm, offset); + else if (isdigit(c)) + match = match_digit(date, &tm, offset, &tm_gmt); + else if ((c == '-' || c == '+') && isdigit(date[1])) + match = match_tz(date, offset); + + if (!match) { + /* BAD */ + match = 1; + } + + date += match; + } + + /* mktime uses local timezone */ + *timestamp = tm_to_time_t(&tm); + if (*offset == -1) + *offset = (int)((time_t)*timestamp - mktime(&tm)) / 60; + + if (*timestamp == (git_time_t)-1) + return -1; + + if (!tm_gmt) + *timestamp -= *offset * 60; + return 0; /* success */ +} + + +/* + * Relative time update (eg "2 days ago"). If we haven't set the time + * yet, we need to set it from current time. + */ +static git_time_t update_tm(struct tm *tm, struct tm *now, unsigned long sec) +{ + time_t n; + + if (tm->tm_mday < 0) + tm->tm_mday = now->tm_mday; + if (tm->tm_mon < 0) + tm->tm_mon = now->tm_mon; + if (tm->tm_year < 0) { + tm->tm_year = now->tm_year; + if (tm->tm_mon > now->tm_mon) + tm->tm_year--; + } + + n = mktime(tm) - sec; + p_localtime_r(&n, tm); + return n; +} + +static void date_now(struct tm *tm, struct tm *now, int *num) +{ + GIT_UNUSED(num); + update_tm(tm, now, 0); +} + +static void date_yesterday(struct tm *tm, struct tm *now, int *num) +{ + GIT_UNUSED(num); + update_tm(tm, now, 24*60*60); +} + +static void date_time(struct tm *tm, struct tm *now, int hour) +{ + if (tm->tm_hour < hour) + date_yesterday(tm, now, NULL); + tm->tm_hour = hour; + tm->tm_min = 0; + tm->tm_sec = 0; +} + +static void date_midnight(struct tm *tm, struct tm *now, int *num) +{ + GIT_UNUSED(num); + date_time(tm, now, 0); +} + +static void date_noon(struct tm *tm, struct tm *now, int *num) +{ + GIT_UNUSED(num); + date_time(tm, now, 12); +} + +static void date_tea(struct tm *tm, struct tm *now, int *num) +{ + GIT_UNUSED(num); + date_time(tm, now, 17); +} + +static void date_pm(struct tm *tm, struct tm *now, int *num) +{ + int hour, n = *num; + *num = 0; + GIT_UNUSED(now); + + hour = tm->tm_hour; + if (n) { + hour = n; + tm->tm_min = 0; + tm->tm_sec = 0; + } + tm->tm_hour = (hour % 12) + 12; +} + +static void date_am(struct tm *tm, struct tm *now, int *num) +{ + int hour, n = *num; + *num = 0; + GIT_UNUSED(now); + + hour = tm->tm_hour; + if (n) { + hour = n; + tm->tm_min = 0; + tm->tm_sec = 0; + } + tm->tm_hour = (hour % 12); +} + +static void date_never(struct tm *tm, struct tm *now, int *num) +{ + time_t n = 0; + GIT_UNUSED(now); + GIT_UNUSED(num); + p_localtime_r(&n, tm); +} + +static const struct special { + const char *name; + void (*fn)(struct tm *, struct tm *, int *); +} special[] = { + { "yesterday", date_yesterday }, + { "noon", date_noon }, + { "midnight", date_midnight }, + { "tea", date_tea }, + { "PM", date_pm }, + { "AM", date_am }, + { "never", date_never }, + { "now", date_now }, + { NULL } +}; + +static const char *number_name[] = { + "zero", "one", "two", "three", "four", + "five", "six", "seven", "eight", "nine", "ten", +}; + +static const struct typelen { + const char *type; + int length; +} typelen[] = { + { "seconds", 1 }, + { "minutes", 60 }, + { "hours", 60*60 }, + { "days", 24*60*60 }, + { "weeks", 7*24*60*60 }, + { NULL } +}; + +static const char *approxidate_alpha(const char *date, struct tm *tm, struct tm *now, int *num, int *touched) +{ + const struct typelen *tl; + const struct special *s; + const char *end = date; + int i; + + while (isalpha(*++end)) + /* scan to non-alpha */; + + for (i = 0; i < 12; i++) { + size_t match = match_string(date, month_names[i]); + if (match >= 3) { + tm->tm_mon = i; + *touched = 1; + return end; + } + } + + for (s = special; s->name; s++) { + size_t len = strlen(s->name); + if (match_string(date, s->name) == len) { + s->fn(tm, now, num); + *touched = 1; + return end; + } + } + + if (!*num) { + for (i = 1; i < 11; i++) { + size_t len = strlen(number_name[i]); + if (match_string(date, number_name[i]) == len) { + *num = i; + *touched = 1; + return end; + } + } + if (match_string(date, "last") == 4) { + *num = 1; + *touched = 1; + } + return end; + } + + tl = typelen; + while (tl->type) { + size_t len = strlen(tl->type); + if (match_string(date, tl->type) >= len-1) { + update_tm(tm, now, tl->length * *num); + *num = 0; + *touched = 1; + return end; + } + tl++; + } + + for (i = 0; i < 7; i++) { + size_t match = match_string(date, weekday_names[i]); + if (match >= 3) { + int diff, n = *num -1; + *num = 0; + + diff = tm->tm_wday - i; + if (diff <= 0) + n++; + diff += 7*n; + + update_tm(tm, now, diff * 24 * 60 * 60); + *touched = 1; + return end; + } + } + + if (match_string(date, "months") >= 5) { + int n; + update_tm(tm, now, 0); /* fill in date fields if needed */ + n = tm->tm_mon - *num; + *num = 0; + while (n < 0) { + n += 12; + tm->tm_year--; + } + tm->tm_mon = n; + *touched = 1; + return end; + } + + if (match_string(date, "years") >= 4) { + update_tm(tm, now, 0); /* fill in date fields if needed */ + tm->tm_year -= *num; + *num = 0; + *touched = 1; + return end; + } + + return end; +} + +static const char *approxidate_digit(const char *date, struct tm *tm, int *num) +{ + char *end; + unsigned long number = strtoul(date, &end, 10); + + switch (*end) { + case ':': + case '.': + case '/': + case '-': + if (isdigit(end[1])) { + size_t match = match_multi_number(number, *end, date, end, tm); + if (match) + return date + match; + } + } + + /* Accept zero-padding only for small numbers ("Dec 02", never "Dec 0002") */ + if (date[0] != '0' || end - date <= 2) + *num = number; + return end; +} + +/* + * Do we have a pending number at the end, or when + * we see a new one? Let's assume it's a month day, + * as in "Dec 6, 1992" + */ +static void pending_number(struct tm *tm, int *num) +{ + int number = *num; + + if (number) { + *num = 0; + if (tm->tm_mday < 0 && number < 32) + tm->tm_mday = number; + else if (tm->tm_mon < 0 && number < 13) + tm->tm_mon = number-1; + else if (tm->tm_year < 0) { + if (number > 1969 && number < 2100) + tm->tm_year = number - 1900; + else if (number > 69 && number < 100) + tm->tm_year = number; + else if (number < 38) + tm->tm_year = 100 + number; + /* We mess up for number = 00 ? */ + } + } +} + +static git_time_t approxidate_str(const char *date, + const struct timeval *tv, + int *error_ret) +{ + int number = 0; + int touched = 0; + struct tm tm = {0}, now; + time_t time_sec; + + time_sec = tv->tv_sec; + p_localtime_r(&time_sec, &tm); + now = tm; + + tm.tm_year = -1; + tm.tm_mon = -1; + tm.tm_mday = -1; + + for (;;) { + unsigned char c = *date; + if (!c) + break; + date++; + if (isdigit(c)) { + pending_number(&tm, &number); + date = approxidate_digit(date-1, &tm, &number); + touched = 1; + continue; + } + if (isalpha(c)) + date = approxidate_alpha(date-1, &tm, &now, &number, &touched); + } + pending_number(&tm, &number); + if (!touched) + *error_ret = 1; + return update_tm(&tm, &now, 0); +} + +int git__date_parse(git_time_t *out, const char *date) +{ + struct timeval tv; + git_time_t timestamp; + int offset, error_ret=0; + + if (!parse_date_basic(date, ×tamp, &offset)) { + *out = timestamp; + return 0; + } + + p_gettimeofday(&tv, NULL); + *out = approxidate_str(date, &tv, &error_ret); + return error_ret; +} diff --git a/src/delta-apply.c b/src/delta-apply.c index 815ca8f16..a39c7af5c 100644 --- a/src/delta-apply.c +++ b/src/delta-apply.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -36,6 +36,19 @@ static int hdr_sz( return 0; } +int git__delta_read_header( + const unsigned char *delta, + size_t delta_len, + size_t *base_sz, + size_t *res_sz) +{ + const unsigned char *delta_end = delta + delta_len; + if ((hdr_sz(base_sz, &delta, delta_end) < 0) || + (hdr_sz(res_sz, &delta, delta_end) < 0)) + return -1; + return 0; +} + int git__delta_apply( git_rawobj *out, const unsigned char *base, diff --git a/src/delta-apply.h b/src/delta-apply.h index 66fa76d43..d7d99d04c 100644 --- a/src/delta-apply.h +++ b/src/delta-apply.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -30,4 +30,21 @@ extern int git__delta_apply( const unsigned char *delta, size_t delta_len); +/** + * Read the header of a git binary delta. + * + * @param delta the delta to execute copy/insert instructions from. + * @param delta_len total number of bytes in the delta. + * @param base_sz pointer to store the base size field. + * @param res_sz pointer to store the result size field. + * @return + * - 0 on a successful decoding the header. + * - GIT_ERROR if the delta is corrupt. + */ +extern int git__delta_read_header( + const unsigned char *delta, + size_t delta_len, + size_t *base_sz, + size_t *res_sz); + #endif diff --git a/src/delta.c b/src/delta.c new file mode 100644 index 000000000..3252dbf14 --- /dev/null +++ b/src/delta.c @@ -0,0 +1,424 @@ +/* + * 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 "delta.h" + +/* maximum hash entry list for the same hash bucket */ +#define HASH_LIMIT 64 + +#define RABIN_SHIFT 23 +#define RABIN_WINDOW 16 + +static const unsigned int T[256] = { + 0x00000000, 0xab59b4d1, 0x56b369a2, 0xfdeadd73, 0x063f6795, 0xad66d344, + 0x508c0e37, 0xfbd5bae6, 0x0c7ecf2a, 0xa7277bfb, 0x5acda688, 0xf1941259, + 0x0a41a8bf, 0xa1181c6e, 0x5cf2c11d, 0xf7ab75cc, 0x18fd9e54, 0xb3a42a85, + 0x4e4ef7f6, 0xe5174327, 0x1ec2f9c1, 0xb59b4d10, 0x48719063, 0xe32824b2, + 0x1483517e, 0xbfdae5af, 0x423038dc, 0xe9698c0d, 0x12bc36eb, 0xb9e5823a, + 0x440f5f49, 0xef56eb98, 0x31fb3ca8, 0x9aa28879, 0x6748550a, 0xcc11e1db, + 0x37c45b3d, 0x9c9defec, 0x6177329f, 0xca2e864e, 0x3d85f382, 0x96dc4753, + 0x6b369a20, 0xc06f2ef1, 0x3bba9417, 0x90e320c6, 0x6d09fdb5, 0xc6504964, + 0x2906a2fc, 0x825f162d, 0x7fb5cb5e, 0xd4ec7f8f, 0x2f39c569, 0x846071b8, + 0x798aaccb, 0xd2d3181a, 0x25786dd6, 0x8e21d907, 0x73cb0474, 0xd892b0a5, + 0x23470a43, 0x881ebe92, 0x75f463e1, 0xdeadd730, 0x63f67950, 0xc8afcd81, + 0x354510f2, 0x9e1ca423, 0x65c91ec5, 0xce90aa14, 0x337a7767, 0x9823c3b6, + 0x6f88b67a, 0xc4d102ab, 0x393bdfd8, 0x92626b09, 0x69b7d1ef, 0xc2ee653e, + 0x3f04b84d, 0x945d0c9c, 0x7b0be704, 0xd05253d5, 0x2db88ea6, 0x86e13a77, + 0x7d348091, 0xd66d3440, 0x2b87e933, 0x80de5de2, 0x7775282e, 0xdc2c9cff, + 0x21c6418c, 0x8a9ff55d, 0x714a4fbb, 0xda13fb6a, 0x27f92619, 0x8ca092c8, + 0x520d45f8, 0xf954f129, 0x04be2c5a, 0xafe7988b, 0x5432226d, 0xff6b96bc, + 0x02814bcf, 0xa9d8ff1e, 0x5e738ad2, 0xf52a3e03, 0x08c0e370, 0xa39957a1, + 0x584ced47, 0xf3155996, 0x0eff84e5, 0xa5a63034, 0x4af0dbac, 0xe1a96f7d, + 0x1c43b20e, 0xb71a06df, 0x4ccfbc39, 0xe79608e8, 0x1a7cd59b, 0xb125614a, + 0x468e1486, 0xedd7a057, 0x103d7d24, 0xbb64c9f5, 0x40b17313, 0xebe8c7c2, + 0x16021ab1, 0xbd5bae60, 0x6cb54671, 0xc7ecf2a0, 0x3a062fd3, 0x915f9b02, + 0x6a8a21e4, 0xc1d39535, 0x3c394846, 0x9760fc97, 0x60cb895b, 0xcb923d8a, + 0x3678e0f9, 0x9d215428, 0x66f4eece, 0xcdad5a1f, 0x3047876c, 0x9b1e33bd, + 0x7448d825, 0xdf116cf4, 0x22fbb187, 0x89a20556, 0x7277bfb0, 0xd92e0b61, + 0x24c4d612, 0x8f9d62c3, 0x7836170f, 0xd36fa3de, 0x2e857ead, 0x85dcca7c, + 0x7e09709a, 0xd550c44b, 0x28ba1938, 0x83e3ade9, 0x5d4e7ad9, 0xf617ce08, + 0x0bfd137b, 0xa0a4a7aa, 0x5b711d4c, 0xf028a99d, 0x0dc274ee, 0xa69bc03f, + 0x5130b5f3, 0xfa690122, 0x0783dc51, 0xacda6880, 0x570fd266, 0xfc5666b7, + 0x01bcbbc4, 0xaae50f15, 0x45b3e48d, 0xeeea505c, 0x13008d2f, 0xb85939fe, + 0x438c8318, 0xe8d537c9, 0x153feaba, 0xbe665e6b, 0x49cd2ba7, 0xe2949f76, + 0x1f7e4205, 0xb427f6d4, 0x4ff24c32, 0xe4abf8e3, 0x19412590, 0xb2189141, + 0x0f433f21, 0xa41a8bf0, 0x59f05683, 0xf2a9e252, 0x097c58b4, 0xa225ec65, + 0x5fcf3116, 0xf49685c7, 0x033df00b, 0xa86444da, 0x558e99a9, 0xfed72d78, + 0x0502979e, 0xae5b234f, 0x53b1fe3c, 0xf8e84aed, 0x17bea175, 0xbce715a4, + 0x410dc8d7, 0xea547c06, 0x1181c6e0, 0xbad87231, 0x4732af42, 0xec6b1b93, + 0x1bc06e5f, 0xb099da8e, 0x4d7307fd, 0xe62ab32c, 0x1dff09ca, 0xb6a6bd1b, + 0x4b4c6068, 0xe015d4b9, 0x3eb80389, 0x95e1b758, 0x680b6a2b, 0xc352defa, + 0x3887641c, 0x93ded0cd, 0x6e340dbe, 0xc56db96f, 0x32c6cca3, 0x999f7872, + 0x6475a501, 0xcf2c11d0, 0x34f9ab36, 0x9fa01fe7, 0x624ac294, 0xc9137645, + 0x26459ddd, 0x8d1c290c, 0x70f6f47f, 0xdbaf40ae, 0x207afa48, 0x8b234e99, + 0x76c993ea, 0xdd90273b, 0x2a3b52f7, 0x8162e626, 0x7c883b55, 0xd7d18f84, + 0x2c043562, 0x875d81b3, 0x7ab75cc0, 0xd1eee811 +}; + +static const unsigned int U[256] = { + 0x00000000, 0x7eb5200d, 0x5633f4cb, 0x2886d4c6, 0x073e5d47, 0x798b7d4a, + 0x510da98c, 0x2fb88981, 0x0e7cba8e, 0x70c99a83, 0x584f4e45, 0x26fa6e48, + 0x0942e7c9, 0x77f7c7c4, 0x5f711302, 0x21c4330f, 0x1cf9751c, 0x624c5511, + 0x4aca81d7, 0x347fa1da, 0x1bc7285b, 0x65720856, 0x4df4dc90, 0x3341fc9d, + 0x1285cf92, 0x6c30ef9f, 0x44b63b59, 0x3a031b54, 0x15bb92d5, 0x6b0eb2d8, + 0x4388661e, 0x3d3d4613, 0x39f2ea38, 0x4747ca35, 0x6fc11ef3, 0x11743efe, + 0x3eccb77f, 0x40799772, 0x68ff43b4, 0x164a63b9, 0x378e50b6, 0x493b70bb, + 0x61bda47d, 0x1f088470, 0x30b00df1, 0x4e052dfc, 0x6683f93a, 0x1836d937, + 0x250b9f24, 0x5bbebf29, 0x73386bef, 0x0d8d4be2, 0x2235c263, 0x5c80e26e, + 0x740636a8, 0x0ab316a5, 0x2b7725aa, 0x55c205a7, 0x7d44d161, 0x03f1f16c, + 0x2c4978ed, 0x52fc58e0, 0x7a7a8c26, 0x04cfac2b, 0x73e5d470, 0x0d50f47d, + 0x25d620bb, 0x5b6300b6, 0x74db8937, 0x0a6ea93a, 0x22e87dfc, 0x5c5d5df1, + 0x7d996efe, 0x032c4ef3, 0x2baa9a35, 0x551fba38, 0x7aa733b9, 0x041213b4, + 0x2c94c772, 0x5221e77f, 0x6f1ca16c, 0x11a98161, 0x392f55a7, 0x479a75aa, + 0x6822fc2b, 0x1697dc26, 0x3e1108e0, 0x40a428ed, 0x61601be2, 0x1fd53bef, + 0x3753ef29, 0x49e6cf24, 0x665e46a5, 0x18eb66a8, 0x306db26e, 0x4ed89263, + 0x4a173e48, 0x34a21e45, 0x1c24ca83, 0x6291ea8e, 0x4d29630f, 0x339c4302, + 0x1b1a97c4, 0x65afb7c9, 0x446b84c6, 0x3adea4cb, 0x1258700d, 0x6ced5000, + 0x4355d981, 0x3de0f98c, 0x15662d4a, 0x6bd30d47, 0x56ee4b54, 0x285b6b59, + 0x00ddbf9f, 0x7e689f92, 0x51d01613, 0x2f65361e, 0x07e3e2d8, 0x7956c2d5, + 0x5892f1da, 0x2627d1d7, 0x0ea10511, 0x7014251c, 0x5facac9d, 0x21198c90, + 0x099f5856, 0x772a785b, 0x4c921c31, 0x32273c3c, 0x1aa1e8fa, 0x6414c8f7, + 0x4bac4176, 0x3519617b, 0x1d9fb5bd, 0x632a95b0, 0x42eea6bf, 0x3c5b86b2, + 0x14dd5274, 0x6a687279, 0x45d0fbf8, 0x3b65dbf5, 0x13e30f33, 0x6d562f3e, + 0x506b692d, 0x2ede4920, 0x06589de6, 0x78edbdeb, 0x5755346a, 0x29e01467, + 0x0166c0a1, 0x7fd3e0ac, 0x5e17d3a3, 0x20a2f3ae, 0x08242768, 0x76910765, + 0x59298ee4, 0x279caee9, 0x0f1a7a2f, 0x71af5a22, 0x7560f609, 0x0bd5d604, + 0x235302c2, 0x5de622cf, 0x725eab4e, 0x0ceb8b43, 0x246d5f85, 0x5ad87f88, + 0x7b1c4c87, 0x05a96c8a, 0x2d2fb84c, 0x539a9841, 0x7c2211c0, 0x029731cd, + 0x2a11e50b, 0x54a4c506, 0x69998315, 0x172ca318, 0x3faa77de, 0x411f57d3, + 0x6ea7de52, 0x1012fe5f, 0x38942a99, 0x46210a94, 0x67e5399b, 0x19501996, + 0x31d6cd50, 0x4f63ed5d, 0x60db64dc, 0x1e6e44d1, 0x36e89017, 0x485db01a, + 0x3f77c841, 0x41c2e84c, 0x69443c8a, 0x17f11c87, 0x38499506, 0x46fcb50b, + 0x6e7a61cd, 0x10cf41c0, 0x310b72cf, 0x4fbe52c2, 0x67388604, 0x198da609, + 0x36352f88, 0x48800f85, 0x6006db43, 0x1eb3fb4e, 0x238ebd5d, 0x5d3b9d50, + 0x75bd4996, 0x0b08699b, 0x24b0e01a, 0x5a05c017, 0x728314d1, 0x0c3634dc, + 0x2df207d3, 0x534727de, 0x7bc1f318, 0x0574d315, 0x2acc5a94, 0x54797a99, + 0x7cffae5f, 0x024a8e52, 0x06852279, 0x78300274, 0x50b6d6b2, 0x2e03f6bf, + 0x01bb7f3e, 0x7f0e5f33, 0x57888bf5, 0x293dabf8, 0x08f998f7, 0x764cb8fa, + 0x5eca6c3c, 0x207f4c31, 0x0fc7c5b0, 0x7172e5bd, 0x59f4317b, 0x27411176, + 0x1a7c5765, 0x64c97768, 0x4c4fa3ae, 0x32fa83a3, 0x1d420a22, 0x63f72a2f, + 0x4b71fee9, 0x35c4dee4, 0x1400edeb, 0x6ab5cde6, 0x42331920, 0x3c86392d, + 0x133eb0ac, 0x6d8b90a1, 0x450d4467, 0x3bb8646a +}; + +struct index_entry { + const unsigned char *ptr; + unsigned int val; + struct index_entry *next; +}; + +struct git_delta_index { + unsigned long memsize; + const void *src_buf; + unsigned long src_size; + unsigned int hash_mask; + struct index_entry *hash[GIT_FLEX_ARRAY]; +}; + +struct git_delta_index * +git_delta_create_index(const void *buf, unsigned long bufsize) +{ + unsigned int i, hsize, hmask, entries, prev_val, *hash_count; + const unsigned char *data, *buffer = buf; + struct git_delta_index *index; + struct index_entry *entry, **hash; + void *mem; + unsigned long memsize; + + if (!buf || !bufsize) + return NULL; + + /* Determine index hash size. Note that indexing skips the + first byte to allow for optimizing the rabin polynomial + initialization in create_delta(). */ + entries = (unsigned int)(bufsize - 1) / RABIN_WINDOW; + if (bufsize >= 0xffffffffUL) { + /* + * Current delta format can't encode offsets into + * reference buffer with more than 32 bits. + */ + entries = 0xfffffffeU / RABIN_WINDOW; + } + hsize = entries / 4; + for (i = 4; (1u << i) < hsize && i < 31; i++); + hsize = 1 << i; + hmask = hsize - 1; + + /* allocate lookup index */ + memsize = sizeof(*index) + + sizeof(*hash) * hsize + + sizeof(*entry) * entries; + mem = git__malloc(memsize); + if (!mem) + return NULL; + index = mem; + mem = index->hash; + hash = mem; + mem = hash + hsize; + entry = mem; + + index->memsize = memsize; + index->src_buf = buf; + index->src_size = bufsize; + index->hash_mask = hmask; + memset(hash, 0, hsize * sizeof(*hash)); + + /* allocate an array to count hash entries */ + hash_count = calloc(hsize, sizeof(*hash_count)); + if (!hash_count) { + git__free(index); + return NULL; + } + + /* then populate the index */ + prev_val = ~0; + for (data = buffer + entries * RABIN_WINDOW - RABIN_WINDOW; + data >= buffer; + data -= RABIN_WINDOW) { + unsigned int val = 0; + for (i = 1; i <= RABIN_WINDOW; i++) + val = ((val << 8) | data[i]) ^ T[val >> RABIN_SHIFT]; + if (val == prev_val) { + /* keep the lowest of consecutive identical blocks */ + entry[-1].ptr = data + RABIN_WINDOW; + } else { + prev_val = val; + i = val & hmask; + entry->ptr = data + RABIN_WINDOW; + entry->val = val; + entry->next = hash[i]; + hash[i] = entry++; + hash_count[i]++; + } + } + + /* + * Determine a limit on the number of entries in the same hash + * bucket. This guard us against patological data sets causing + * really bad hash distribution with most entries in the same hash + * bucket that would bring us to O(m*n) computing costs (m and n + * corresponding to reference and target buffer sizes). + * + * Make sure none of the hash buckets has more entries than + * we're willing to test. Otherwise we cull the entry list + * uniformly to still preserve a good repartition across + * the reference buffer. + */ + for (i = 0; i < hsize; i++) { + if (hash_count[i] < HASH_LIMIT) + continue; + + entry = hash[i]; + do { + struct index_entry *keep = entry; + int skip = hash_count[i] / HASH_LIMIT / 2; + do { + entry = entry->next; + } while(--skip && entry); + keep->next = entry; + } while (entry); + } + git__free(hash_count); + + return index; +} + +void git_delta_free_index(struct git_delta_index *index) +{ + git__free(index); +} + +unsigned long git_delta_sizeof_index(struct git_delta_index *index) +{ + if (index) + return index->memsize; + else + return 0; +} + +/* + * The maximum size for any opcode sequence, including the initial header + * plus rabin window plus biggest copy. + */ +#define MAX_OP_SIZE (5 + 5 + 1 + RABIN_WINDOW + 7) + +void * +git_delta_create( + const struct git_delta_index *index, + const void *trg_buf, + unsigned long trg_size, + unsigned long *delta_size, + unsigned long max_size) +{ + unsigned int i, outpos, outsize, moff, msize, val; + int inscnt; + const unsigned char *ref_data, *ref_top, *data, *top; + unsigned char *out; + + if (!trg_buf || !trg_size) + return NULL; + + outpos = 0; + outsize = 8192; + if (max_size && outsize >= max_size) + outsize = (unsigned int)(max_size + MAX_OP_SIZE + 1); + out = git__malloc(outsize); + if (!out) + return NULL; + + /* store reference buffer size */ + i = index->src_size; + while (i >= 0x80) { + out[outpos++] = i | 0x80; + i >>= 7; + } + out[outpos++] = i; + + /* store target buffer size */ + i = trg_size; + while (i >= 0x80) { + out[outpos++] = i | 0x80; + i >>= 7; + } + out[outpos++] = i; + + ref_data = index->src_buf; + ref_top = ref_data + index->src_size; + data = trg_buf; + top = (const unsigned char *) trg_buf + trg_size; + + outpos++; + val = 0; + for (i = 0; i < RABIN_WINDOW && data < top; i++, data++) { + out[outpos++] = *data; + val = ((val << 8) | *data) ^ T[val >> RABIN_SHIFT]; + } + inscnt = i; + + moff = 0; + msize = 0; + while (data < top) { + if (msize < 4096) { + struct index_entry *entry; + val ^= U[data[-RABIN_WINDOW]]; + val = ((val << 8) | *data) ^ T[val >> RABIN_SHIFT]; + i = val & index->hash_mask; + for (entry = index->hash[i]; entry; entry = entry->next) { + const unsigned char *ref = entry->ptr; + const unsigned char *src = data; + unsigned int ref_size = (unsigned int)(ref_top - ref); + if (entry->val != val) + continue; + if (ref_size > (unsigned int)(top - src)) + ref_size = (unsigned int)(top - src); + if (ref_size <= msize) + break; + while (ref_size-- && *src++ == *ref) + ref++; + if (msize < (unsigned int)(ref - entry->ptr)) { + /* this is our best match so far */ + msize = (unsigned int)(ref - entry->ptr); + moff = (unsigned int)(entry->ptr - ref_data); + if (msize >= 4096) /* good enough */ + break; + } + } + } + + if (msize < 4) { + if (!inscnt) + outpos++; + out[outpos++] = *data++; + inscnt++; + if (inscnt == 0x7f) { + out[outpos - inscnt - 1] = inscnt; + inscnt = 0; + } + msize = 0; + } else { + unsigned int left; + unsigned char *op; + + if (inscnt) { + while (moff && ref_data[moff-1] == data[-1]) { + /* we can match one byte back */ + msize++; + moff--; + data--; + outpos--; + if (--inscnt) + continue; + outpos--; /* remove count slot */ + inscnt--; /* make it -1 */ + break; + } + out[outpos - inscnt - 1] = inscnt; + inscnt = 0; + } + + /* A copy op is currently limited to 64KB (pack v2) */ + left = (msize < 0x10000) ? 0 : (msize - 0x10000); + msize -= left; + + op = out + outpos++; + i = 0x80; + + if (moff & 0x000000ff) + out[outpos++] = moff >> 0, i |= 0x01; + if (moff & 0x0000ff00) + out[outpos++] = moff >> 8, i |= 0x02; + if (moff & 0x00ff0000) + out[outpos++] = moff >> 16, i |= 0x04; + if (moff & 0xff000000) + out[outpos++] = moff >> 24, i |= 0x08; + + if (msize & 0x00ff) + out[outpos++] = msize >> 0, i |= 0x10; + if (msize & 0xff00) + out[outpos++] = msize >> 8, i |= 0x20; + + *op = i; + + data += msize; + moff += msize; + msize = left; + + if (msize < 4096) { + int j; + val = 0; + for (j = -RABIN_WINDOW; j < 0; j++) + val = ((val << 8) | data[j]) + ^ T[val >> RABIN_SHIFT]; + } + } + + if (outpos >= outsize - MAX_OP_SIZE) { + void *tmp = out; + outsize = outsize * 3 / 2; + if (max_size && outsize >= max_size) + outsize = max_size + MAX_OP_SIZE + 1; + if (max_size && outpos > max_size) + break; + out = git__realloc(out, outsize); + if (!out) { + git__free(tmp); + return NULL; + } + } + } + + if (inscnt) + out[outpos - inscnt - 1] = inscnt; + + if (max_size && outpos > max_size) { + git__free(out); + return NULL; + } + + *delta_size = outpos; + return out; +} diff --git a/src/delta.h b/src/delta.h new file mode 100644 index 000000000..4ca327992 --- /dev/null +++ b/src/delta.h @@ -0,0 +1,114 @@ +/* + * diff-delta code taken from git.git. See diff-delta.c for details. + * + */ +#ifndef INCLUDE_git_delta_h__ +#define INCLUDE_git_delta_h__ + +#include "common.h" + +/* opaque object for delta index */ +struct git_delta_index; + +/* + * create_delta_index: compute index data from given buffer + * + * This returns a pointer to a struct delta_index that should be passed to + * subsequent create_delta() calls, or to free_delta_index(). A NULL pointer + * is returned on failure. The given buffer must not be freed nor altered + * before free_delta_index() is called. The returned pointer must be freed + * using free_delta_index(). + */ +extern struct git_delta_index * +git_delta_create_index(const void *buf, unsigned long bufsize); + +/* + * free_delta_index: free the index created by create_delta_index() + * + * Given pointer must be what create_delta_index() returned, or NULL. + */ +extern void git_delta_free_index(struct git_delta_index *index); + +/* + * sizeof_delta_index: returns memory usage of delta index + * + * Given pointer must be what create_delta_index() returned, or NULL. + */ +extern unsigned long git_delta_sizeof_index(struct git_delta_index *index); + +/* + * create_delta: create a delta from given index for the given buffer + * + * This function may be called multiple times with different buffers using + * the same delta_index pointer. If max_delta_size is non-zero and the + * resulting delta is to be larger than max_delta_size then NULL is returned. + * On success, a non-NULL pointer to the buffer with the delta data is + * returned and *delta_size is updated with its size. The returned buffer + * must be freed by the caller. + */ +extern void *git_delta_create( + const struct git_delta_index *index, + const void *buf, + unsigned long bufsize, + unsigned long *delta_size, + unsigned long max_delta_size); + +/* + * diff_delta: create a delta from source buffer to target buffer + * + * If max_delta_size is non-zero and the resulting delta is to be larger + * than max_delta_size then NULL is returned. On success, a non-NULL + * pointer to the buffer with the delta data is returned and *delta_size is + * updated with its size. The returned buffer must be freed by the caller. + */ +GIT_INLINE(void *) git_delta( + const void *src_buf, unsigned long src_bufsize, + const void *trg_buf, unsigned long trg_bufsize, + unsigned long *delta_size, + unsigned long max_delta_size) +{ + struct git_delta_index *index = git_delta_create_index(src_buf, src_bufsize); + if (index) { + void *delta = git_delta_create( + index, trg_buf, trg_bufsize, delta_size, max_delta_size); + git_delta_free_index(index); + return delta; + } + return NULL; +} + +/* + * patch_delta: recreate target buffer given source buffer and delta data + * + * On success, a non-NULL pointer to the target buffer is returned and + * *trg_bufsize is updated with its size. On failure a NULL pointer is + * returned. The returned buffer must be freed by the caller. + */ +extern void *git_delta_patch( + const void *src_buf, unsigned long src_size, + const void *delta_buf, unsigned long delta_size, + unsigned long *dst_size); + +/* the smallest possible delta size is 4 bytes */ +#define GIT_DELTA_SIZE_MIN 4 + +/* + * This must be called twice on the delta data buffer, first to get the + * expected source buffer size, and again to get the target buffer size. + */ +GIT_INLINE(unsigned long) git_delta_get_hdr_size( + const unsigned char **datap, const unsigned char *top) +{ + const unsigned char *data = *datap; + unsigned long cmd, size = 0; + int i = 0; + do { + cmd = *data++; + size |= (cmd & 0x7f) << i; + i += 7; + } while (cmd & 0x80 && data < top); + *datap = data; + return size; +} + +#endif diff --git a/src/diff.c b/src/diff.c index 0b2f8fb50..37c89f3f1 100644 --- a/src/diff.c +++ b/src/diff.c @@ -1,74 +1,19 @@ /* - * Copyright (C) 2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ #include "common.h" -#include "git2/diff.h" #include "diff.h" #include "fileops.h" #include "config.h" #include "attr_file.h" +#include "filter.h" +#include "pathspec.h" -static char *diff_prefix_from_pathspec(const git_strarray *pathspec) -{ - git_buf prefix = GIT_BUF_INIT; - const char *scan; - - if (git_buf_common_prefix(&prefix, pathspec) < 0) - return NULL; - - /* diff prefix will only be leading non-wildcards */ - for (scan = prefix.ptr; *scan && !git__iswildcard(*scan); ++scan); - git_buf_truncate(&prefix, scan - prefix.ptr); - - if (prefix.size > 0) - return git_buf_detach(&prefix); - - git_buf_free(&prefix); - return NULL; -} - -static bool diff_pathspec_is_interesting(const git_strarray *pathspec) -{ - const char *str; - - if (pathspec == NULL || pathspec->count == 0) - return false; - if (pathspec->count > 1) - return true; - - str = pathspec->strings[0]; - if (!str || !str[0] || (!str[1] && (str[0] == '*' || str[0] == '.'))) - return false; - return true; -} - -static bool diff_path_matches_pathspec(git_diff_list *diff, const char *path) -{ - unsigned int i; - git_attr_fnmatch *match; - - if (!diff->pathspec.length) - return true; - - git_vector_foreach(&diff->pathspec, i, match) { - int result = p_fnmatch(match->pattern, path, 0); - - /* if we didn't match, look for exact dirname prefix match */ - if (result == FNM_NOMATCH && - (match->flags & GIT_ATTR_FNMATCH_HASWILD) == 0 && - strncmp(path, match->pattern, match->length) == 0 && - path[match->length] == '/') - result = 0; - - if (result == 0) - return (match->flags & GIT_ATTR_FNMATCH_NEGATIVE) ? false : true; - } - - return false; -} +#define DIFF_FLAG_IS_SET(DIFF,FLAG) (((DIFF)->opts.flags & (FLAG)) != 0) +#define DIFF_FLAG_ISNT_SET(DIFF,FLAG) (((DIFF)->opts.flags & (FLAG)) == 0) static git_diff_delta *diff_delta__alloc( git_diff_list *diff, @@ -87,7 +32,7 @@ static git_diff_delta *diff_delta__alloc( delta->new_file.path = delta->old_file.path; - if (diff->opts.flags & GIT_DIFF_REVERSE) { + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { switch (status) { case GIT_DELTA_ADDED: status = GIT_DELTA_DELETED; break; case GIT_DELTA_DELETED: status = GIT_DELTA_ADDED; break; @@ -99,70 +44,16 @@ static git_diff_delta *diff_delta__alloc( return delta; } -static git_diff_delta *diff_delta__dup( - const git_diff_delta *d, git_pool *pool) -{ - git_diff_delta *delta = git__malloc(sizeof(git_diff_delta)); - if (!delta) - return NULL; - - memcpy(delta, d, sizeof(git_diff_delta)); - - delta->old_file.path = git_pool_strdup(pool, d->old_file.path); - if (delta->old_file.path == NULL) - goto fail; - - if (d->new_file.path != d->old_file.path) { - delta->new_file.path = git_pool_strdup(pool, d->new_file.path); - if (delta->new_file.path == NULL) - goto fail; - } else { - delta->new_file.path = delta->old_file.path; - } - - return delta; - -fail: - git__free(delta); - return NULL; -} - -static git_diff_delta *diff_delta__merge_like_cgit( - const git_diff_delta *a, const git_diff_delta *b, git_pool *pool) +static int diff_notify( + const git_diff_list *diff, + const git_diff_delta *delta, + const char *matched_pathspec) { - git_diff_delta *dup = diff_delta__dup(a, pool); - if (!dup) - return NULL; - - if (git_oid_cmp(&dup->new_file.oid, &b->new_file.oid) == 0) - return dup; - - git_oid_cpy(&dup->new_file.oid, &b->new_file.oid); - - dup->new_file.mode = b->new_file.mode; - dup->new_file.size = b->new_file.size; - dup->new_file.flags = b->new_file.flags; - - /* Emulate C git for merging two diffs (a la 'git diff <sha>'). - * - * When C git does a diff between the work dir and a tree, it actually - * diffs with the index but uses the workdir contents. This emulates - * those choices so we can emulate the type of diff. - */ - if (git_oid_cmp(&dup->old_file.oid, &dup->new_file.oid) == 0) { - if (dup->status == GIT_DELTA_DELETED) - /* preserve pending delete info */; - else if (b->status == GIT_DELTA_UNTRACKED || - b->status == GIT_DELTA_IGNORED) - dup->status = b->status; - else - dup->status = GIT_DELTA_UNMODIFIED; - } - else if (dup->status == GIT_DELTA_UNMODIFIED || - b->status == GIT_DELTA_DELETED) - dup->status = b->status; + if (!diff->opts.notify_cb) + return 0; - return dup; + return diff->opts.notify_cb( + diff, delta, matched_pathspec, diff->opts.notify_payload); } static int diff_delta__from_one( @@ -171,16 +62,26 @@ static int diff_delta__from_one( const git_index_entry *entry) { git_diff_delta *delta; + const char *matched_pathspec; + int notify_res; if (status == GIT_DELTA_IGNORED && - (diff->opts.flags & GIT_DIFF_INCLUDE_IGNORED) == 0) + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_IGNORED)) return 0; if (status == GIT_DELTA_UNTRACKED && - (diff->opts.flags & GIT_DIFF_INCLUDE_UNTRACKED) == 0) + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNTRACKED)) + return 0; + + if (entry->mode == GIT_FILEMODE_COMMIT && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) return 0; - if (!diff_path_matches_pathspec(diff, entry->path)) + if (!git_pathspec_match_path( + &diff->pathspec, entry->path, + DIFF_FLAG_IS_SET(diff, GIT_DIFF_DISABLE_PATHSPEC_MATCH), + DIFF_FLAG_IS_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE), + &matched_pathspec)) return 0; delta = diff_delta__alloc(diff, status, entry->path); @@ -199,54 +100,122 @@ static int diff_delta__from_one( git_oid_cpy(&delta->new_file.oid, &entry->oid); } - delta->old_file.flags |= GIT_DIFF_FILE_VALID_OID; - delta->new_file.flags |= GIT_DIFF_FILE_VALID_OID; + delta->old_file.flags |= GIT_DIFF_FLAG_VALID_OID; + + if (delta->status == GIT_DELTA_DELETED || + !git_oid_iszero(&delta->new_file.oid)) + delta->new_file.flags |= GIT_DIFF_FLAG_VALID_OID; - if (git_vector_insert(&diff->deltas, delta) < 0) { + notify_res = diff_notify(diff, delta, matched_pathspec); + + if (notify_res) + git__free(delta); + else if (git_vector_insert(&diff->deltas, delta) < 0) { git__free(delta); return -1; } - return 0; + return notify_res < 0 ? GIT_EUSER : 0; } static int diff_delta__from_two( git_diff_list *diff, git_delta_t status, const git_index_entry *old_entry, + uint32_t old_mode, const git_index_entry *new_entry, - git_oid *new_oid) + uint32_t new_mode, + git_oid *new_oid, + const char *matched_pathspec) { git_diff_delta *delta; + int notify_res; if (status == GIT_DELTA_UNMODIFIED && - (diff->opts.flags & GIT_DIFF_INCLUDE_UNMODIFIED) == 0) + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNMODIFIED)) + return 0; + + if (old_entry->mode == GIT_FILEMODE_COMMIT && + new_entry->mode == GIT_FILEMODE_COMMIT && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) return 0; - if ((diff->opts.flags & GIT_DIFF_REVERSE) != 0) { - const git_index_entry *temp = old_entry; + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { + uint32_t temp_mode = old_mode; + const git_index_entry *temp_entry = old_entry; old_entry = new_entry; - new_entry = temp; + new_entry = temp_entry; + old_mode = new_mode; + new_mode = temp_mode; } delta = diff_delta__alloc(diff, status, old_entry->path); GITERR_CHECK_ALLOC(delta); - delta->old_file.mode = old_entry->mode; git_oid_cpy(&delta->old_file.oid, &old_entry->oid); - delta->old_file.flags |= GIT_DIFF_FILE_VALID_OID; + delta->old_file.size = old_entry->file_size; + delta->old_file.mode = old_mode; + delta->old_file.flags |= GIT_DIFF_FLAG_VALID_OID; + + git_oid_cpy(&delta->new_file.oid, &new_entry->oid); + delta->new_file.size = new_entry->file_size; + delta->new_file.mode = new_mode; + + if (new_oid) { + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) + git_oid_cpy(&delta->old_file.oid, new_oid); + else + git_oid_cpy(&delta->new_file.oid, new_oid); + } - delta->new_file.mode = new_entry->mode; - git_oid_cpy(&delta->new_file.oid, new_oid ? new_oid : &new_entry->oid); if (new_oid || !git_oid_iszero(&new_entry->oid)) - delta->new_file.flags |= GIT_DIFF_FILE_VALID_OID; + delta->new_file.flags |= GIT_DIFF_FLAG_VALID_OID; + + notify_res = diff_notify(diff, delta, matched_pathspec); - if (git_vector_insert(&diff->deltas, delta) < 0) { + if (notify_res) + git__free(delta); + else if (git_vector_insert(&diff->deltas, delta) < 0) { git__free(delta); return -1; } - return 0; + return notify_res < 0 ? GIT_EUSER : 0; +} + +static git_diff_delta *diff_delta__last_for_item( + git_diff_list *diff, + const git_index_entry *item) +{ + git_diff_delta *delta = git_vector_last(&diff->deltas); + if (!delta) + return NULL; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: + case GIT_DELTA_DELETED: + if (git_oid_cmp(&delta->old_file.oid, &item->oid) == 0) + return delta; + break; + case GIT_DELTA_ADDED: + if (git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) + return delta; + break; + case GIT_DELTA_UNTRACKED: + if (diff->strcomp(delta->new_file.path, item->path) == 0 && + git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) + return delta; + break; + case GIT_DELTA_MODIFIED: + if (git_oid_cmp(&delta->old_file.oid, &item->oid) == 0 || + git_oid_cmp(&delta->new_file.oid, &item->oid) == 0) + return delta; + break; + default: + break; + } + + return NULL; } static char *diff_strdup_prefix(git_pool *pool, const char *prefix) @@ -260,13 +229,34 @@ static char *diff_strdup_prefix(git_pool *pool, const char *prefix) return git_pool_strndup(pool, prefix, len + 1); } -static int diff_delta__cmp(const void *a, const void *b) +int git_diff_delta__cmp(const void *a, const void *b) { const git_diff_delta *da = a, *db = b; int val = strcmp(da->old_file.path, db->old_file.path); return val ? val : ((int)da->status - (int)db->status); } +bool git_diff_delta__should_skip( + const git_diff_options *opts, const git_diff_delta *delta) +{ + uint32_t flags = opts ? opts->flags : 0; + + if (delta->status == GIT_DELTA_UNMODIFIED && + (flags & GIT_DIFF_INCLUDE_UNMODIFIED) == 0) + return true; + + if (delta->status == GIT_DELTA_IGNORED && + (flags & GIT_DIFF_INCLUDE_IGNORED) == 0) + return true; + + if (delta->status == GIT_DELTA_UNTRACKED && + (flags & GIT_DIFF_INCLUDE_UNTRACKED) == 0) + return true; + + return false; +} + + static int config_bool(git_config *cfg, const char *name, int defvalue) { int val = defvalue; @@ -281,14 +271,14 @@ static git_diff_list *git_diff_list_alloc( git_repository *repo, const git_diff_options *opts) { git_config *cfg; - size_t i; git_diff_list *diff = git__calloc(1, sizeof(git_diff_list)); if (diff == NULL) return NULL; + GIT_REFCOUNT_INC(diff); diff->repo = repo; - if (git_vector_init(&diff->deltas, 0, diff_delta__cmp) < 0 || + if (git_vector_init(&diff->deltas, 0, git_diff_delta__cmp) < 0 || git_pool_init(&diff->pool, 1, 0) < 0) goto fail; @@ -300,16 +290,36 @@ static git_diff_list *git_diff_list_alloc( if (config_bool(cfg, "core.ignorestat", 0)) diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_ASSUME_UNCHANGED; if (config_bool(cfg, "core.filemode", 1)) - diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_TRUST_EXEC_BIT; + diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_TRUST_MODE_BITS; if (config_bool(cfg, "core.trustctime", 1)) diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_TRUST_CTIME; /* Don't set GIT_DIFFCAPS_USE_DEV - compile time option in core git */ - if (opts == NULL) + /* TODO: there are certain config settings where even if we were + * not given an options structure, we need the diff list to have one + * so that we can store the altered default values. + * + * - diff.ignoreSubmodules + * - diff.mnemonicprefix + * - diff.noprefix + */ + + if (opts == NULL) { + /* Make sure we default to 3 lines */ + diff->opts.context_lines = 3; return diff; + } memcpy(&diff->opts, opts, sizeof(git_diff_options)); - memset(&diff->opts.pathspec, 0, sizeof(diff->opts.pathspec)); + + if(opts->flags & GIT_DIFF_IGNORE_FILEMODE) + diff->diffcaps = diff->diffcaps & ~GIT_DIFFCAPS_TRUST_MODE_BITS; + + /* pathspec init will do nothing for empty pathspec */ + if (git_pathspec_init(&diff->pathspec, &opts->pathspec, &diff->pool) < 0) + goto fail; + + /* TODO: handle config diff.mnemonicprefix, diff.noprefix */ diff->opts.old_prefix = diff_strdup_prefix(&diff->pool, opts->old_prefix ? opts->old_prefix : DIFF_OLD_PREFIX_DEFAULT); @@ -319,39 +329,15 @@ static git_diff_list *git_diff_list_alloc( if (!diff->opts.old_prefix || !diff->opts.new_prefix) goto fail; - if (diff->opts.flags & GIT_DIFF_REVERSE) { - char *swap = diff->opts.old_prefix; + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { + const char *swap = diff->opts.old_prefix; diff->opts.old_prefix = diff->opts.new_prefix; diff->opts.new_prefix = swap; } - /* only copy pathspec if it is "interesting" so we can test - * diff->pathspec.length > 0 to know if it is worth calling - * fnmatch as we iterate. - */ - if (!diff_pathspec_is_interesting(&opts->pathspec)) - return diff; - - if (git_vector_init( - &diff->pathspec, (unsigned int)opts->pathspec.count, NULL) < 0) - goto fail; - - for (i = 0; i < opts->pathspec.count; ++i) { - int ret; - const char *pattern = opts->pathspec.strings[i]; - git_attr_fnmatch *match = git__calloc(1, sizeof(git_attr_fnmatch)); - if (!match) - goto fail; - ret = git_attr_fnmatch__parse(match, &diff->pool, NULL, &pattern); - if (ret == GIT_ENOTFOUND) { - git__free(match); - continue; - } else if (ret < 0) - goto fail; - - if (git_vector_insert(&diff->pathspec, match) < 0) - goto fail; - } + /* INCLUDE_TYPECHANGE_TREES implies INCLUDE_TYPECHANGE */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES)) + diff->opts.flags |= GIT_DIFF_INCLUDE_TYPECHANGE; return diff; @@ -360,65 +346,106 @@ fail: return NULL; } -void git_diff_list_free(git_diff_list *diff) +static void diff_list_free(git_diff_list *diff) { git_diff_delta *delta; - git_attr_fnmatch *match; unsigned int i; - if (!diff) - return; - git_vector_foreach(&diff->deltas, i, delta) { git__free(delta); diff->deltas.contents[i] = NULL; } git_vector_free(&diff->deltas); - git_vector_foreach(&diff->pathspec, i, match) { - git__free(match); - diff->pathspec.contents[i] = NULL; - } - git_vector_free(&diff->pathspec); - + git_pathspec_free(&diff->pathspec); git_pool_clear(&diff->pool); git__free(diff); } -static int oid_for_workdir_item( +void git_diff_list_free(git_diff_list *diff) +{ + if (!diff) + return; + + GIT_REFCOUNT_DEC(diff, diff_list_free); +} + +void git_diff_list_addref(git_diff_list *diff) +{ + GIT_REFCOUNT_INC(diff); +} + +int git_diff__oid_for_file( git_repository *repo, - const git_index_entry *item, + const char *path, + uint16_t mode, + git_off_t size, git_oid *oid) { - int result; + int result = 0; git_buf full_path = GIT_BUF_INIT; - if (git_buf_joinpath(&full_path, git_repository_workdir(repo), item->path) < 0) + if (git_buf_joinpath( + &full_path, git_repository_workdir(repo), path) < 0) return -1; - /* calculate OID for file if possible*/ - if (S_ISLNK(item->mode)) + if (!mode) { + struct stat st; + + if (p_stat(path, &st) < 0) { + giterr_set(GITERR_OS, "Could not stat '%s'", path); + result = -1; + goto cleanup; + } + + mode = st.st_mode; + size = st.st_size; + } + + /* calculate OID for file if possible */ + if (S_ISGITLINK(mode)) { + git_submodule *sm; + const git_oid *sm_oid; + + if (!git_submodule_lookup(&sm, repo, path) && + (sm_oid = git_submodule_wd_id(sm)) != NULL) + git_oid_cpy(oid, sm_oid); + else { + /* if submodule lookup failed probably just in an intermediate + * state where some init hasn't happened, so ignore the error + */ + giterr_clear(); + memset(oid, 0, sizeof(*oid)); + } + } else if (S_ISLNK(mode)) { result = git_odb__hashlink(oid, full_path.ptr); - else if (!git__is_sizet(item->file_size)) { - giterr_set(GITERR_OS, "File size overflow for 32-bit systems"); + } else if (!git__is_sizet(size)) { + giterr_set(GITERR_OS, "File size overflow (for 32-bits) on '%s'", path); result = -1; } else { - int fd = git_futils_open_ro(full_path.ptr); - if (fd < 0) - result = fd; - else { - result = git_odb__hashfd( - oid, fd, (size_t)item->file_size, GIT_OBJ_BLOB); - p_close(fd); + git_vector filters = GIT_VECTOR_INIT; + + result = git_filters_load(&filters, repo, path, GIT_FILTER_TO_ODB); + if (result >= 0) { + int fd = git_futils_open_ro(full_path.ptr); + if (fd < 0) + result = fd; + else { + result = git_odb__hashfd_filtered( + oid, fd, (size_t)size, GIT_OBJ_BLOB, &filters); + p_close(fd); + } } + + git_filters_free(&filters); } +cleanup: git_buf_free(&full_path); - return result; } -#define EXEC_BIT_MASK 0000111 +#define MODE_BITS_MASK 0000777 static int maybe_modified( git_iterator *old_iter, @@ -431,24 +458,30 @@ static int maybe_modified( git_delta_t status = GIT_DELTA_MODIFIED; unsigned int omode = oitem->mode; unsigned int nmode = nitem->mode; + bool new_is_workdir = (new_iter->type == GIT_ITERATOR_TYPE_WORKDIR); + const char *matched_pathspec; GIT_UNUSED(old_iter); - if (!diff_path_matches_pathspec(diff, oitem->path)) + if (!git_pathspec_match_path( + &diff->pathspec, oitem->path, + DIFF_FLAG_IS_SET(diff, GIT_DIFF_DISABLE_PATHSPEC_MATCH), + DIFF_FLAG_IS_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE), + &matched_pathspec)) return 0; - /* on platforms with no symlinks, promote plain files to symlinks */ - if (S_ISLNK(omode) && S_ISREG(nmode) && + /* on platforms with no symlinks, preserve mode of existing symlinks */ + if (S_ISLNK(omode) && S_ISREG(nmode) && new_is_workdir && !(diff->diffcaps & GIT_DIFFCAPS_HAS_SYMLINKS)) - nmode = GIT_MODE_TYPE(omode) | (nmode & GIT_MODE_PERMS_MASK); + nmode = omode; - /* on platforms with no execmode, clear exec bit from comparisons */ - if (!(diff->diffcaps & GIT_DIFFCAPS_TRUST_EXEC_BIT)) { - omode = omode & ~EXEC_BIT_MASK; - nmode = nmode & ~EXEC_BIT_MASK; - } + /* on platforms with no execmode, just preserve old mode */ + if (!(diff->diffcaps & GIT_DIFFCAPS_TRUST_MODE_BITS) && + (nmode & MODE_BITS_MASK) != (omode & MODE_BITS_MASK) && + new_is_workdir) + nmode = (nmode & ~MODE_BITS_MASK) | (omode & MODE_BITS_MASK); - /* support "assume unchanged" (badly, b/c we still stat everything) */ + /* support "assume unchanged" (poorly, b/c we still stat everything) */ if ((diff->diffcaps & GIT_DIFFCAPS_ASSUME_UNCHANGED) != 0) status = (oitem->flags_extended & GIT_IDXENTRY_INTENT_TO_ADD) ? GIT_DELTA_MODIFIED : GIT_DELTA_UNMODIFIED; @@ -459,23 +492,29 @@ static int maybe_modified( /* if basic type of file changed, then split into delete and add */ else if (GIT_MODE_TYPE(omode) != GIT_MODE_TYPE(nmode)) { - if (diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem) < 0 || - diff_delta__from_one(diff, GIT_DELTA_ADDED, nitem) < 0) - return -1; - return 0; + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE)) + status = GIT_DELTA_TYPECHANGE; + else { + if (diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem) < 0 || + diff_delta__from_one(diff, GIT_DELTA_ADDED, nitem) < 0) + return -1; + return 0; + } } /* if oids and modes match, then file is unmodified */ - else if (git_oid_cmp(&oitem->oid, &nitem->oid) == 0 && - omode == nmode) + else if (git_oid_equal(&oitem->oid, &nitem->oid) && omode == nmode) status = GIT_DELTA_UNMODIFIED; - /* if we have a workdir item with an unknown oid, check deeper */ - else if (git_oid_iszero(&nitem->oid) && new_iter->type == GIT_ITERATOR_WORKDIR) { + /* if we have an unknown OID and a workdir iterator, then check some + * circumstances that can accelerate things or need special handling + */ + else if (git_oid_iszero(&nitem->oid) && new_is_workdir) { /* TODO: add check against index file st_mtime to avoid racy-git */ - /* if they files look exactly alike, then we'll assume the same */ - if (oitem->file_size == nitem->file_size && + /* if the stat data looks exactly alike, then assume the same */ + if (omode == nmode && + oitem->file_size == nitem->file_size && (!(diff->diffcaps & GIT_DIFFCAPS_TRUST_CTIME) || (oitem->ctime.seconds == nitem->ctime.seconds)) && oitem->mtime.seconds == nitem->mtime.seconds && @@ -487,77 +526,193 @@ static int maybe_modified( status = GIT_DELTA_UNMODIFIED; else if (S_ISGITLINK(nmode)) { + int err; git_submodule *sub; - if ((diff->opts.flags & GIT_DIFF_IGNORE_SUBMODULES) != 0) + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) status = GIT_DELTA_UNMODIFIED; - else if (git_submodule_lookup(&sub, diff->repo, nitem->path) < 0) - return -1; - else if (sub->ignore == GIT_SUBMODULE_IGNORE_ALL) + else if ((err = git_submodule_lookup(&sub, diff->repo, nitem->path)) < 0) { + if (err == GIT_EEXISTS) + status = GIT_DELTA_UNMODIFIED; + else + return err; + } else if (git_submodule_ignore(sub) == GIT_SUBMODULE_IGNORE_ALL) status = GIT_DELTA_UNMODIFIED; else { - /* TODO: support other GIT_SUBMODULE_IGNORE values */ - status = GIT_DELTA_UNMODIFIED; + unsigned int sm_status = 0; + if (git_submodule_status(&sm_status, sub) < 0) + return -1; + + /* check IS_WD_UNMODIFIED because this case is only used + * when the new side of the diff is the working directory + */ + status = GIT_SUBMODULE_STATUS_IS_WD_UNMODIFIED(sm_status) + ? GIT_DELTA_UNMODIFIED : GIT_DELTA_MODIFIED; + + /* grab OID while we are here */ + if (git_oid_iszero(&nitem->oid)) { + const git_oid *sm_oid = git_submodule_wd_id(sub); + if (sm_oid != NULL) { + git_oid_cpy(&noid, sm_oid); + use_noid = &noid; + } + } } } + } - /* TODO: check git attributes so we will not have to read the file - * in if it is marked binary. - */ + /* if mode is GITLINK and submodules are ignored, then skip */ + else if (S_ISGITLINK(nmode) && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES)) + status = GIT_DELTA_UNMODIFIED; - else if (oid_for_workdir_item(diff->repo, nitem, &noid) < 0) - return -1; + /* if we got here and decided that the files are modified, but we + * haven't calculated the OID of the new item, then calculate it now + */ + if (status != GIT_DELTA_UNMODIFIED && git_oid_iszero(&nitem->oid)) { + if (!use_noid) { + if (git_diff__oid_for_file(diff->repo, + nitem->path, nitem->mode, nitem->file_size, &noid) < 0) + return -1; + use_noid = &noid; + } - else if (git_oid_cmp(&oitem->oid, &noid) == 0 && - omode == nmode) + /* if oid matches, then mark unmodified (except submodules, where + * the filesystem content may be modified even if the oid still + * matches between the index and the workdir HEAD) + */ + if (omode == nmode && !S_ISGITLINK(omode) && + git_oid_equal(&oitem->oid, use_noid)) status = GIT_DELTA_UNMODIFIED; + } + + return diff_delta__from_two( + diff, status, oitem, omode, nitem, nmode, use_noid, matched_pathspec); +} - /* store calculated oid so we don't have to recalc later */ - use_noid = &noid; +static bool entry_is_prefixed( + git_diff_list *diff, + const git_index_entry *item, + const git_index_entry *prefix_item) +{ + size_t pathlen; + + if (!item || diff->pfxcomp(item->path, prefix_item->path) != 0) + return false; + + pathlen = strlen(prefix_item->path); + + return (prefix_item->path[pathlen - 1] == '/' || + item->path[pathlen] == '\0' || + item->path[pathlen] == '/'); +} + +static int diff_list_init_from_iterators( + git_diff_list *diff, + git_iterator *old_iter, + git_iterator *new_iter) +{ + diff->old_src = old_iter->type; + diff->new_src = new_iter->type; + + /* Use case-insensitive compare if either iterator has + * the ignore_case bit set */ + if (!git_iterator_ignore_case(old_iter) && + !git_iterator_ignore_case(new_iter)) + { + diff->opts.flags &= ~GIT_DIFF_DELTAS_ARE_ICASE; + + diff->strcomp = git__strcmp; + diff->strncomp = git__strncmp; + diff->pfxcomp = git__prefixcmp; + diff->entrycomp = git_index_entry__cmp; + } else { + diff->opts.flags |= GIT_DIFF_DELTAS_ARE_ICASE; + + diff->strcomp = git__strcasecmp; + diff->strncomp = git__strncasecmp; + diff->pfxcomp = git__prefixcmp_icase; + diff->entrycomp = git_index_entry__cmp_icase; } - return diff_delta__from_two(diff, status, oitem, nitem, use_noid); + return 0; } -static int diff_from_iterators( +int git_diff__from_iterators( + git_diff_list **diff_ptr, git_repository *repo, - const git_diff_options *opts, /**< can be NULL for defaults */ git_iterator *old_iter, git_iterator *new_iter, - git_diff_list **diff_ptr) + const git_diff_options *opts) { + int error = 0; const git_index_entry *oitem, *nitem; git_buf ignore_prefix = GIT_BUF_INIT; git_diff_list *diff = git_diff_list_alloc(repo, opts); - if (!diff) + + *diff_ptr = NULL; + + if (!diff || diff_list_init_from_iterators(diff, old_iter, new_iter) < 0) goto fail; - diff->old_src = old_iter->type; - diff->new_src = new_iter->type; + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_DELTAS_ARE_ICASE)) { + if (git_iterator_set_ignore_case(old_iter, true) < 0 || + git_iterator_set_ignore_case(new_iter, true) < 0) + goto fail; + } - if (git_iterator_current(old_iter, &oitem) < 0 || - git_iterator_current(new_iter, &nitem) < 0) + if (git_iterator_current(&oitem, old_iter) < 0 || + git_iterator_current(&nitem, new_iter) < 0) goto fail; /* run iterators building diffs */ while (oitem || nitem) { + int cmp = oitem ? (nitem ? diff->entrycomp(oitem, nitem) : -1) : 1; /* create DELETED records for old items not matched in new */ - if (oitem && (!nitem || strcmp(oitem->path, nitem->path) < 0)) { - if (diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem) < 0 || - git_iterator_advance(old_iter, &oitem) < 0) + if (cmp < 0) { + if (diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem) < 0) + goto fail; + + /* if we are generating TYPECHANGE records then check for that + * instead of just generating a DELETE record + */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) && + entry_is_prefixed(diff, nitem, oitem)) + { + /* this entry has become a tree! convert to TYPECHANGE */ + git_diff_delta *last = diff_delta__last_for_item(diff, oitem); + if (last) { + last->status = GIT_DELTA_TYPECHANGE; + last->new_file.mode = GIT_FILEMODE_TREE; + } + + /* If new_iter is a workdir iterator, then this situation + * will certainly be followed by a series of untracked items. + * Unless RECURSE_UNTRACKED_DIRS is set, skip over them... + */ + if (S_ISDIR(nitem->mode) && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) + { + if (git_iterator_advance(&nitem, new_iter) < 0) + goto fail; + } + } + + if (git_iterator_advance(&oitem, old_iter) < 0) goto fail; } /* create ADDED, TRACKED, or IGNORED records for new items not * matched in old (and/or descend into directories as needed) */ - else if (nitem && (!oitem || strcmp(oitem->path, nitem->path) > 0)) { + else if (cmp > 0) { git_delta_t delta_type = GIT_DELTA_UNTRACKED; + bool contains_oitem = entry_is_prefixed(diff, oitem, nitem); /* check if contained in ignored parent directory */ if (git_buf_len(&ignore_prefix) && - git__prefixcmp(nitem->path, git_buf_cstr(&ignore_prefix)) == 0) + diff->pfxcomp(nitem->path, git_buf_cstr(&ignore_prefix)) == 0) delta_type = GIT_DELTA_IGNORED; if (S_ISDIR(nitem->mode)) { @@ -565,20 +720,48 @@ static int diff_from_iterators( * it or if the user requested the contents of untracked * directories and it is not under an ignored directory. */ - if ((oitem && git__prefixcmp(oitem->path, nitem->path) == 0) || + bool recurse_into_dir = (delta_type == GIT_DELTA_UNTRACKED && - (diff->opts.flags & GIT_DIFF_RECURSE_UNTRACKED_DIRS) != 0)) - { - /* if this directory is ignored, remember it as the - * "ignore_prefix" for processing contained items - */ - if (delta_type == GIT_DELTA_UNTRACKED && - git_iterator_current_is_ignored(new_iter)) - git_buf_sets(&ignore_prefix, nitem->path); - - if (git_iterator_advance_into_directory(new_iter, &nitem) < 0) + DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) || + (delta_type == GIT_DELTA_IGNORED && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)); + + /* do not advance into directories that contain a .git file */ + if (!contains_oitem && recurse_into_dir) { + git_buf *full = NULL; + if (git_iterator_current_workdir_path(&full, new_iter) < 0) goto fail; + if (git_path_contains_dir(full, DOT_GIT)) + recurse_into_dir = false; + } + + /* if directory is ignored, remember ignore_prefix */ + if ((contains_oitem || recurse_into_dir) && + delta_type == GIT_DELTA_UNTRACKED && + git_iterator_current_is_ignored(new_iter)) + { + git_buf_sets(&ignore_prefix, nitem->path); + delta_type = GIT_DELTA_IGNORED; + + /* skip recursion if we've just learned this is ignored */ + if (DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)) + recurse_into_dir = false; + } + + if (contains_oitem || recurse_into_dir) { + /* advance into directory */ + error = git_iterator_advance_into(&nitem, new_iter); + + /* if directory is empty, can't advance into it, so skip */ + if (error == GIT_ENOTFOUND) { + giterr_clear(); + error = git_iterator_advance(&nitem, new_iter); + + git_buf_clear(&ignore_prefix); + } + if (error < 0) + goto fail; continue; } } @@ -598,8 +781,9 @@ static int diff_from_iterators( * checked before container directory exclusions are used to * skip the file. */ - else if (delta_type == GIT_DELTA_IGNORED) { - if (git_iterator_advance(new_iter, &nitem) < 0) + else if (delta_type == GIT_DELTA_IGNORED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)) { + if (git_iterator_advance(&nitem, new_iter) < 0) goto fail; continue; /* ignored parent directory, so skip completely */ } @@ -607,11 +791,28 @@ static int diff_from_iterators( else if (git_iterator_current_is_ignored(new_iter)) delta_type = GIT_DELTA_IGNORED; - else if (new_iter->type != GIT_ITERATOR_WORKDIR) + else if (new_iter->type != GIT_ITERATOR_TYPE_WORKDIR) delta_type = GIT_DELTA_ADDED; - if (diff_delta__from_one(diff, delta_type, nitem) < 0 || - git_iterator_advance(new_iter, &nitem) < 0) + if (diff_delta__from_one(diff, delta_type, nitem) < 0) + goto fail; + + /* if we are generating TYPECHANGE records then check for that + * instead of just generating an ADDED/UNTRACKED record + */ + if (delta_type != GIT_DELTA_IGNORED && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) && + contains_oitem) + { + /* this entry was prefixed with a tree - make TYPECHANGE */ + git_diff_delta *last = diff_delta__last_for_item(diff, nitem); + if (last) { + last->status = GIT_DELTA_TYPECHANGE; + last->old_file.mode = GIT_FILEMODE_TREE; + } + } + + if (git_iterator_advance(&nitem, new_iter) < 0) goto fail; } @@ -619,165 +820,116 @@ static int diff_from_iterators( * (or ADDED and DELETED pair if type changed) */ else { - assert(oitem && nitem && strcmp(oitem->path, nitem->path) == 0); + assert(oitem && nitem && cmp == 0); if (maybe_modified(old_iter, oitem, new_iter, nitem, diff) < 0 || - git_iterator_advance(old_iter, &oitem) < 0 || - git_iterator_advance(new_iter, &nitem) < 0) + git_iterator_advance(&oitem, old_iter) < 0 || + git_iterator_advance(&nitem, new_iter) < 0) goto fail; } } - git_iterator_free(old_iter); - git_iterator_free(new_iter); - git_buf_free(&ignore_prefix); - *diff_ptr = diff; - return 0; fail: - git_iterator_free(old_iter); - git_iterator_free(new_iter); + if (!*diff_ptr) { + git_diff_list_free(diff); + error = -1; + } + git_buf_free(&ignore_prefix); - git_diff_list_free(diff); - *diff_ptr = NULL; - return -1; + return error; } +#define DIFF_FROM_ITERATORS(MAKE_FIRST, MAKE_SECOND) do { \ + git_iterator *a = NULL, *b = NULL; \ + char *pfx = opts ? git_pathspec_prefix(&opts->pathspec) : NULL; \ + GITERR_CHECK_VERSION(opts, GIT_DIFF_OPTIONS_VERSION, "git_diff_options"); \ + if (!(error = MAKE_FIRST) && !(error = MAKE_SECOND)) \ + error = git_diff__from_iterators(diff, repo, a, b, opts); \ + git__free(pfx); git_iterator_free(a); git_iterator_free(b); \ +} while (0) int git_diff_tree_to_tree( + git_diff_list **diff, git_repository *repo, - const git_diff_options *opts, /**< can be NULL for defaults */ git_tree *old_tree, git_tree *new_tree, - git_diff_list **diff) + const git_diff_options *opts) { - git_iterator *a = NULL, *b = NULL; - char *prefix = opts ? diff_prefix_from_pathspec(&opts->pathspec) : NULL; - - assert(repo && old_tree && new_tree && diff); + int error = 0; - if (git_iterator_for_tree_range(&a, repo, old_tree, prefix, prefix) < 0 || - git_iterator_for_tree_range(&b, repo, new_tree, prefix, prefix) < 0) - return -1; + assert(diff && repo); - git__free(prefix); + DIFF_FROM_ITERATORS( + git_iterator_for_tree(&a, old_tree, 0, pfx, pfx), + git_iterator_for_tree(&b, new_tree, 0, pfx, pfx) + ); - return diff_from_iterators(repo, opts, a, b, diff); + return error; } -int git_diff_index_to_tree( +int git_diff_tree_to_index( + git_diff_list **diff, git_repository *repo, - const git_diff_options *opts, git_tree *old_tree, - git_diff_list **diff) + git_index *index, + const git_diff_options *opts) { - git_iterator *a = NULL, *b = NULL; - char *prefix = opts ? diff_prefix_from_pathspec(&opts->pathspec) : NULL; + int error = 0; - assert(repo && diff); + assert(diff && repo); - if (git_iterator_for_tree_range(&a, repo, old_tree, prefix, prefix) < 0 || - git_iterator_for_index_range(&b, repo, prefix, prefix) < 0) - return -1; + if (!index && (error = git_repository_index__weakptr(&index, repo)) < 0) + return error; - git__free(prefix); + DIFF_FROM_ITERATORS( + git_iterator_for_tree(&a, old_tree, 0, pfx, pfx), + git_iterator_for_index(&b, index, 0, pfx, pfx) + ); - return diff_from_iterators(repo, opts, a, b, diff); + return error; } -int git_diff_workdir_to_index( +int git_diff_index_to_workdir( + git_diff_list **diff, git_repository *repo, - const git_diff_options *opts, - git_diff_list **diff) + git_index *index, + const git_diff_options *opts) { - git_iterator *a = NULL, *b = NULL; - char *prefix = opts ? diff_prefix_from_pathspec(&opts->pathspec) : NULL; + int error = 0; - assert(repo && diff); + assert(diff && repo); - if (git_iterator_for_index_range(&a, repo, prefix, prefix) < 0 || - git_iterator_for_workdir_range(&b, repo, prefix, prefix) < 0) - return -1; + if (!index && (error = git_repository_index__weakptr(&index, repo)) < 0) + return error; - git__free(prefix); + DIFF_FROM_ITERATORS( + git_iterator_for_index(&a, index, 0, pfx, pfx), + git_iterator_for_workdir( + &b, repo, GIT_ITERATOR_DONT_AUTOEXPAND, pfx, pfx) + ); - return diff_from_iterators(repo, opts, a, b, diff); + return error; } -int git_diff_workdir_to_tree( +int git_diff_tree_to_workdir( + git_diff_list **diff, git_repository *repo, - const git_diff_options *opts, git_tree *old_tree, - git_diff_list **diff) -{ - git_iterator *a = NULL, *b = NULL; - char *prefix = opts ? diff_prefix_from_pathspec(&opts->pathspec) : NULL; - - assert(repo && old_tree && diff); - - if (git_iterator_for_tree_range(&a, repo, old_tree, prefix, prefix) < 0 || - git_iterator_for_workdir_range(&b, repo, prefix, prefix) < 0) - return -1; - - git__free(prefix); - - return diff_from_iterators(repo, opts, a, b, diff); -} - -int git_diff_merge( - git_diff_list *onto, - const git_diff_list *from) + const git_diff_options *opts) { int error = 0; - git_pool onto_pool; - git_vector onto_new; - git_diff_delta *delta; - unsigned int i, j; - - assert(onto && from); - - if (!from->deltas.length) - return 0; - - if (git_vector_init(&onto_new, onto->deltas.length, diff_delta__cmp) < 0 || - git_pool_init(&onto_pool, 1, 0) < 0) - return -1; - - for (i = 0, j = 0; i < onto->deltas.length || j < from->deltas.length; ) { - git_diff_delta *o = GIT_VECTOR_GET(&onto->deltas, i); - const git_diff_delta *f = GIT_VECTOR_GET(&from->deltas, j); - int cmp = !f ? -1 : !o ? 1 : strcmp(o->old_file.path, f->old_file.path); - if (cmp < 0) { - delta = diff_delta__dup(o, &onto_pool); - i++; - } else if (cmp > 0) { - delta = diff_delta__dup(f, &onto_pool); - j++; - } else { - delta = diff_delta__merge_like_cgit(o, f, &onto_pool); - i++; - j++; - } - - if ((error = !delta ? -1 : git_vector_insert(&onto_new, delta)) < 0) - break; - } + assert(diff && repo); - if (!error) { - git_vector_swap(&onto->deltas, &onto_new); - git_pool_swap(&onto->pool, &onto_pool); - onto->new_src = from->new_src; - } - - git_vector_foreach(&onto_new, i, delta) - git__free(delta); - git_vector_free(&onto_new); - git_pool_clear(&onto_pool); + DIFF_FROM_ITERATORS( + git_iterator_for_tree(&a, old_tree, 0, pfx, pfx), + git_iterator_for_workdir( + &b, repo, GIT_ITERATOR_DONT_AUTOEXPAND, pfx, pfx) + ); return error; } - diff --git a/src/diff.h b/src/diff.h index ac2457956..8e3cbcd46 100644 --- a/src/diff.h +++ b/src/diff.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -7,6 +7,9 @@ #ifndef INCLUDE_diff_h__ #define INCLUDE_diff_h__ +#include "git2/diff.h" +#include "git2/oid.h" + #include <stdio.h> #include "vector.h" #include "buffer.h" @@ -20,21 +23,56 @@ enum { GIT_DIFFCAPS_HAS_SYMLINKS = (1 << 0), /* symlinks on platform? */ GIT_DIFFCAPS_ASSUME_UNCHANGED = (1 << 1), /* use stat? */ - GIT_DIFFCAPS_TRUST_EXEC_BIT = (1 << 2), /* use st_mode exec bit? */ + 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? */ }; +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. */ +}; + struct git_diff_list { + git_refcount rc; git_repository *repo; git_diff_options opts; git_vector pathspec; - git_vector deltas; /* vector of git_diff_file_delta */ + git_vector deltas; /* vector of git_diff_delta */ git_pool pool; git_iterator_type_t old_src; git_iterator_type_t new_src; uint32_t diffcaps; + + int (*strcomp)(const char *, const char *); + int (*strncomp)(const char *, const char *, size_t); + int (*pfxcomp)(const char *str, const char *pfx); + int (*entrycomp)(const void *a, const void *b); }; +extern void git_diff__cleanup_modes( + uint32_t diffcaps, uint32_t *omode, uint32_t *nmode); + +extern void git_diff_list_addref(git_diff_list *diff); + +extern int git_diff_delta__cmp(const void *a, const void *b); + +extern bool git_diff_delta__should_skip( + const git_diff_options *opts, const git_diff_delta *delta); + +extern int git_diff__oid_for_file( + git_repository *, const char *, uint16_t, git_off_t, git_oid *); + +extern 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); + #endif diff --git a/src/diff_output.c b/src/diff_output.c index ba7ef8245..34a3e506c 100644 --- a/src/diff_output.c +++ b/src/diff_output.c @@ -1,29 +1,18 @@ /* - * Copyright (C) 2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ #include "common.h" -#include "git2/diff.h" #include "git2/attr.h" -#include "git2/blob.h" -#include "xdiff/xdiff.h" +#include "git2/oid.h" +#include "git2/submodule.h" +#include "diff_output.h" #include <ctype.h> -#include "diff.h" -#include "map.h" #include "fileops.h" #include "filter.h" - -typedef struct { - git_diff_list *diff; - void *cb_data; - git_diff_hunk_fn hunk_cb; - git_diff_data_fn line_cb; - unsigned int index; - git_diff_delta *delta; - git_diff_range range; -} diff_output_info; +#include "buf_text.h" static int read_next_int(const char **str, int *value) { @@ -39,77 +28,50 @@ static int read_next_int(const char **str, int *value) return (digits > 0) ? 0 : -1; } -static int diff_output_cb(void *priv, mmbuffer_t *bufs, int len) +static int parse_hunk_header(git_diff_range *range, const char *header) { - diff_output_info *info = priv; - - if (len == 1 && info->hunk_cb) { - git_diff_range range = { -1, 0, -1, 0 }; - const char *scan = bufs[0].ptr; - - /* expect something of the form "@@ -%d[,%d] +%d[,%d] @@" */ - if (*scan != '@') - return -1; - - if (read_next_int(&scan, &range.old_start) < 0) - return -1; - if (*scan == ',' && read_next_int(&scan, &range.old_lines) < 0) - return -1; - - if (read_next_int(&scan, &range.new_start) < 0) - return -1; - if (*scan == ',' && read_next_int(&scan, &range.new_lines) < 0) - return -1; - - if (range.old_start < 0 || range.new_start < 0) + /* 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; - - memcpy(&info->range, &range, sizeof(git_diff_range)); - - return info->hunk_cb( - info->cb_data, info->delta, &range, bufs[0].ptr, bufs[0].size); - } - - if ((len == 2 || len == 3) && info->line_cb) { - int origin; - - /* expect " "/"-"/"+", then data, then maybe newline */ - origin = - (*bufs[0].ptr == '+') ? GIT_DIFF_LINE_ADDITION : - (*bufs[0].ptr == '-') ? GIT_DIFF_LINE_DELETION : - GIT_DIFF_LINE_CONTEXT; - - if (info->line_cb( - info->cb_data, info->delta, &info->range, origin, bufs[1].ptr, bufs[1].size) < 0) + } 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; - - /* deal with adding and removing newline at EOF */ - if (len == 3) { - if (origin == GIT_DIFF_LINE_ADDITION) - origin = GIT_DIFF_LINE_ADD_EOFNL; - else - origin = GIT_DIFF_LINE_DEL_EOFNL; - - return info->line_cb( - info->cb_data, info->delta, &info->range, origin, bufs[2].ptr, bufs[2].size); - } - } + } else + range->new_lines = 1; + if (range->old_start < 0 || range->new_start < 0) + return -1; return 0; } -#define BINARY_DIFF_FLAGS (GIT_DIFF_FILE_BINARY|GIT_DIFF_FILE_NOT_BINARY) +#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) +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_FILE_BINARY; + file->flags |= GIT_DIFF_FLAG_BINARY; else if (GIT_ATTR_TRUE(value)) - file->flags |= GIT_DIFF_FILE_NOT_BINARY; + file->flags |= GIT_DIFF_FLAG_NOT_BINARY; /* otherwise leave file->flags alone */ return 0; @@ -117,101 +79,133 @@ static int update_file_is_binary_by_attr(git_repository *repo, git_diff_file *fi static void update_delta_is_binary(git_diff_delta *delta) { - if ((delta->old_file.flags & GIT_DIFF_FILE_BINARY) != 0 || - (delta->new_file.flags & GIT_DIFF_FILE_BINARY) != 0) - delta->binary = 1; - else if ((delta->old_file.flags & GIT_DIFF_FILE_NOT_BINARY) != 0 || - (delta->new_file.flags & GIT_DIFF_FILE_NOT_BINARY) != 0) - delta->binary = 0; - /* otherwise leave delta->binary value untouched */ + 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 */ } -static int file_is_binary_by_attr( - git_diff_list *diff, +/* 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) { - int error = 0, mirror_new; - - delta->binary = -1; + /* 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_FILE_BINARY; - delta->new_file.flags |= GIT_DIFF_FILE_BINARY; - delta->binary = 1; - return 0; + 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 (diff->opts.flags & GIT_DIFF_FORCE_TEXT) { - delta->old_file.flags |= GIT_DIFF_FILE_NOT_BINARY; - delta->new_file.flags |= GIT_DIFF_FILE_NOT_BINARY; - delta->binary = 0; - return 0; + 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(diff->repo, &delta->old_file) < 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 || - strcmp(delta->new_file.path, delta->old_file.path) == 0); + ctxt->diff->strcomp(delta->new_file.path, delta->old_file.path) == 0); if (mirror_new) - delta->new_file.flags &= (delta->old_file.flags & BINARY_DIFF_FLAGS); + delta->new_file.flags |= (delta->old_file.flags & KNOWN_BINARY_FLAGS); else - error = update_file_is_binary_by_attr(diff->repo, &delta->new_file); + error = update_file_is_binary_by_attr(ctxt->repo, &delta->new_file); update_delta_is_binary(delta); return error; } -static int file_is_binary_by_content( +static int diff_delta_is_binary_by_content( + diff_context *ctxt, git_diff_delta *delta, - git_map *old_data, - git_map *new_data) + git_diff_file *file, + const git_map *map) { - git_buf search; + const git_buf search = { map->data, 0, min(map->len, 4000) }; - if ((delta->old_file.flags & BINARY_DIFF_FLAGS) == 0) { - search.ptr = old_data->data; - search.size = min(old_data->len, 4000); + if (diff_delta_is_binary_forced(ctxt, delta)) + return 0; - if (git_buf_is_binary(&search)) - delta->old_file.flags |= GIT_DIFF_FILE_BINARY; - else - delta->old_file.flags |= GIT_DIFF_FILE_NOT_BINARY; - } + /* 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. + */ - if ((delta->new_file.flags & BINARY_DIFF_FLAGS) == 0) { - search.ptr = new_data->data; - search.size = min(new_data->len, 4000); + /* 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; - if (git_buf_is_binary(&search)) - delta->new_file.flags |= GIT_DIFF_FILE_BINARY; - else - delta->new_file.flags |= GIT_DIFF_FILE_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; } - update_delta_is_binary(delta); + if (file->size > threshold) + file->flags |= GIT_DIFF_FLAG_BINARY; - /* TODO: if value != NULL, implement diff drivers */ + update_delta_is_binary(delta); return 0; } static void setup_xdiff_options( - git_diff_options *opts, xdemitconf_t *cfg, xpparam_t *param) + 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 || !opts->context_lines) ? 3 : opts->context_lines; + (!opts) ? 3 : opts->context_lines; cfg->interhunkctxlen = - (!opts || !opts->interhunk_lines) ? 3 : opts->interhunk_lines; + (!opts) ? 0 : opts->interhunk_lines; if (!opts) return; @@ -224,54 +218,239 @@ static void setup_xdiff_options( param->flags |= XDF_IGNORE_WHITESPACE_AT_EOL; } + static int get_blob_content( - git_repository *repo, - const git_oid *oid, + diff_context *ctxt, + git_diff_delta *delta, + git_diff_file *file, git_map *map, git_blob **blob) { - if (git_oid_iszero(oid)) + int error; + git_odb_object *odb_obj = NULL; + + if (git_oid_iszero(&file->oid)) return 0; - if (git_blob_lookup(blob, repo, oid) < 0) - return -1; + 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 = git_blob_rawsize(*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( - git_repository *repo, + 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 (git_buf_joinpath(&path, git_repository_workdir(repo), file->path) < 0) + 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 read_len; + ssize_t alloc_len, read_len; + + file->flags |= GIT_DIFF_FLAG__FREE_DATA; + file->flags |= GIT_DIFF_FLAG_BINARY; - file->flags |= GIT_DIFF_FILE_FREE_DATA; - file->flags |= GIT_DIFF_FILE_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((size_t)file->size + 1); + map->data = git__malloc(alloc_len); GITERR_CHECK_ALLOC(map->data); - read_len = p_readlink(path.ptr, map->data, (size_t)file->size + 1); - if (read_len != (ssize_t)file->size) { + 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; - } else - map->len = read_len; + goto cleanup; + } + + map->len = read_len; } else { - error = git_futils_mmap_ro_file(map, path.ptr); - file->flags |= GIT_DIFF_FILE_UNMAP_DATA; + 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; } @@ -281,185 +460,563 @@ 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_FILE_FREE_DATA) { + if (file->flags & GIT_DIFF_FLAG__FREE_DATA) { git__free(map->data); - map->data = NULL; - file->flags &= ~GIT_DIFF_FILE_FREE_DATA; + map->data = ""; + map->len = 0; + file->flags &= ~GIT_DIFF_FLAG__FREE_DATA; } - else if (file->flags & GIT_DIFF_FILE_UNMAP_DATA) { + else if (file->flags & GIT_DIFF_FLAG__UNMAP_DATA) { git_futils_mmap_free(map); - map->data = NULL; - file->flags &= ~GIT_DIFF_FILE_UNMAP_DATA; + map->data = ""; + map->len = 0; + file->flags &= ~GIT_DIFF_FLAG__UNMAP_DATA; } } -static void fill_map_from_mmfile(git_map *dst, mmfile_t *src) { - assert(dst && src); - dst->data = src->ptr; - dst->len = src->size; -#ifdef GIT_WIN32 - dst->fmh = NULL; -#endif +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; } -int git_diff_foreach( - git_diff_list *diff, - void *data, - git_diff_file_fn file_cb, - git_diff_hunk_fn hunk_cb, - git_diff_data_fn line_cb) +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; - diff_output_info info; - git_diff_delta *delta; - xpparam_t xdiff_params; - xdemitconf_t xdiff_config; - xdemitcb_t xdiff_callback; + git_diff_delta *delta = patch->delta; + bool check_if_unmodified = false; - info.diff = diff; - info.cb_data = data; - info.hunk_cb = hunk_cb; - info.line_cb = line_cb; + if ((patch->flags & GIT_DIFF_PATCH_LOADED) != 0) + return 0; - setup_xdiff_options(&diff->opts, &xdiff_config, &xdiff_params); - memset(&xdiff_callback, 0, sizeof(xdiff_callback)); - xdiff_callback.outf = diff_output_cb; - xdiff_callback.priv = &info; + error = diff_delta_is_binary_by_attr(ctxt, patch); - git_vector_foreach(&diff->deltas, info.index, delta) { - git_blob *old_blob = NULL, *new_blob = NULL; - git_map old_data, new_data; - mmfile_t old_xdiff_data, new_xdiff_data; + patch->old_data.data = ""; + patch->old_data.len = 0; + patch->old_blob = NULL; - if (delta->status == GIT_DELTA_UNMODIFIED && - (diff->opts.flags & GIT_DIFF_INCLUDE_UNMODIFIED) == 0) - continue; + patch->new_data.data = ""; + patch->new_data.len = 0; + patch->new_blob = NULL; - if (delta->status == GIT_DELTA_IGNORED && - (diff->opts.flags & GIT_DIFF_INCLUDE_IGNORED) == 0) - continue; + if ((delta->flags & GIT_DIFF_FLAG_BINARY) != 0) + goto cleanup; - if (delta->status == GIT_DELTA_UNTRACKED && - (diff->opts.flags & GIT_DIFF_INCLUDE_UNTRACKED) == 0) - continue; + 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 ((error = file_is_binary_by_attr(diff, delta)) < 0) + 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; + } - old_data.data = ""; - old_data.len = 0; - new_data.data = ""; - new_data.len = 0; + 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; + } - /* TODO: Partial blob reading to defer loading whole blob. - * I.e. I want a blob with just the first 4kb loaded, then - * later on I will read the rest of the blob if needed. - */ + 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; + } - /* map files */ - if (delta->binary != 1 && - (hunk_cb || line_cb) && - (delta->status == GIT_DELTA_DELETED || - delta->status == GIT_DELTA_MODIFIED)) - { - if (diff->old_src == GIT_ITERATOR_WORKDIR) - error = get_workdir_content(diff->repo, &delta->old_file, &old_data); - else - error = get_blob_content( - diff->repo, &delta->old_file.oid, &old_data, &old_blob); - - if (error < 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 (delta->binary != 1 && - (hunk_cb || line_cb || git_oid_iszero(&delta->new_file.oid)) && - (delta->status == GIT_DELTA_ADDED || - delta->status == GIT_DELTA_MODIFIED)) - { - if (diff->new_src == GIT_ITERATOR_WORKDIR) - error = get_workdir_content(diff->repo, &delta->new_file, &new_data); - else - error = get_blob_content( - diff->repo, &delta->new_file.oid, &new_data, &new_blob); - - if (error < 0) - goto cleanup; - - if ((delta->new_file.flags | GIT_DIFF_FILE_VALID_OID) == 0) { - error = git_odb_hash( - &delta->new_file.oid, new_data.data, new_data.len, GIT_OBJ_BLOB); - - if (error < 0) - goto cleanup; - - /* since we did not have the definitive oid, we may have - * incorrect status and need to skip this item. - */ - if (git_oid_cmp(&delta->old_file.oid, &delta->new_file.oid) == 0) { - delta->status = GIT_DELTA_UNMODIFIED; - if ((diff->opts.flags & GIT_DIFF_INCLUDE_UNMODIFIED) == 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 we have not already decided whether file is binary, - * check the first 4K for nul bytes to decide... + 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->binary == -1) { - error = file_is_binary_by_content( - delta, &old_data, &new_data); - if (error < 0) - goto cleanup; - } + 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; + } - /* TODO: if ignore_whitespace is set, then we *must* do text - * diffs to tell if a file has really been changed. + 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 (file_cb != NULL) { - error = file_cb(data, delta, (float)info.index / diff->deltas.length); - if (error < 0) - goto cleanup; - } + 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; + } - /* don't do hunk and line diffs if file is binary */ - if (delta->binary == 1) - goto cleanup; + return ctxt->error; +} - /* nothing to do if we did not get data */ - if (!old_data.len && !new_data.len) - goto cleanup; +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; - assert(hunk_cb || line_cb); + if ((patch->flags & GIT_DIFF_PATCH_DIFFED) != 0) + return 0; - info.delta = delta; - old_xdiff_data.ptr = old_data.data; - old_xdiff_data.size = old_data.len; - new_xdiff_data.ptr = new_data.data; - new_xdiff_data.size = new_data.len; + if ((patch->flags & GIT_DIFF_PATCH_LOADED) == 0) + if ((error = diff_patch_load(ctxt, patch)) < 0) + return error; - xdl_diff(&old_xdiff_data, &new_xdiff_data, - &xdiff_params, &xdiff_config, &xdiff_callback); + if ((patch->flags & GIT_DIFF_PATCH_DIFFABLE) == 0) + return 0; -cleanup: - release_content(&delta->old_file, &old_data, old_blob); - release_content(&delta->new_file, &new_data, new_blob); + 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_fn print_cb; - void *cb_data; + git_diff_data_cb print_cb; + void *payload; git_buf *buf; } diff_print_info; @@ -467,32 +1024,40 @@ static char pick_suffix(int mode) { if (S_ISDIR(mode)) return '/'; - else if (mode & 0100) + else if (mode & 0100) //-V536 /* in git, modes are very regular, so we must have 0100755 mode */ return '*'; else return ' '; } -static int print_compact(void *data, git_diff_delta *delta, float progress) +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 code, old_suffix, new_suffix; + char old_suffix, new_suffix, code = git_diff_status_char(delta->status); GIT_UNUSED(progress); - switch (delta->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 = 0; - } - - if (!code) + if (code == ' ') return 0; old_suffix = pick_suffix(delta->old_file.mode); @@ -501,7 +1066,7 @@ static int print_compact(void *data, git_diff_delta *delta, float progress) git_buf_clear(pi->buf); if (delta->old_file.path != delta->new_file.path && - strcmp(delta->old_file.path,delta->new_file.path) != 0) + 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 && @@ -516,13 +1081,20 @@ static int print_compact(void *data, git_diff_delta *delta, float progress) if (git_buf_oom(pi->buf)) return -1; - return pi->print_cb(pi->cb_data, delta, NULL, GIT_DIFF_LINE_FILE_HDR, git_buf_cstr(pi->buf), git_buf_len(pi->buf)); + 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, - void *cb_data, - git_diff_data_fn print_cb) + git_diff_data_cb print_cb, + void *payload) { int error; git_buf buf = GIT_BUF_INIT; @@ -530,18 +1102,17 @@ int git_diff_print_compact( pi.diff = diff; pi.print_cb = print_cb; - pi.cb_data = cb_data; + pi.payload = payload; pi.buf = &buf; - error = git_diff_foreach(diff, &pi, print_compact, NULL, NULL); + 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, git_diff_delta *delta) +static int print_oid_range(diff_print_info *pi, const git_diff_delta *delta) { char start_oid[8], end_oid[8]; @@ -571,17 +1142,24 @@ static int print_oid_range(diff_print_info *pi, git_diff_delta *delta) return 0; } -static int print_patch_file(void *data, git_diff_delta *delta, float progress) +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; - int result; 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; @@ -603,7 +1181,7 @@ static int print_patch_file(void *data, git_diff_delta *delta, float progress) newpath = "/dev/null"; } - if (delta->binary != 1) { + 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); } @@ -611,12 +1189,15 @@ static int print_patch_file(void *data, git_diff_delta *delta, float progress) if (git_buf_oom(pi->buf)) return -1; - result = pi->print_cb(pi->cb_data, delta, NULL, GIT_DIFF_LINE_FILE_HDR, git_buf_cstr(pi->buf), git_buf_len(pi->buf)); - if (result < 0) - return result; + 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->binary != 1) - return 0; + if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0) + return 0; git_buf_clear(pi->buf); git_buf_printf( @@ -625,35 +1206,55 @@ static int print_patch_file(void *data, git_diff_delta *delta, float progress) if (git_buf_oom(pi->buf)) return -1; - return pi->print_cb(pi->cb_data, delta, NULL, GIT_DIFF_LINE_BINARY, git_buf_cstr(pi->buf), git_buf_len(pi->buf)); + 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( - void *data, - git_diff_delta *d, - git_diff_range *r, + const git_diff_delta *d, + const git_diff_range *r, const char *header, - size_t header_len) + 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; - return pi->print_cb(pi->cb_data, d, r, GIT_DIFF_LINE_HUNK_HDR, git_buf_cstr(pi->buf), git_buf_len(pi->buf)); + 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( - void *data, - git_diff_delta *delta, - git_diff_range *range, + 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) + 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 || @@ -666,13 +1267,20 @@ static int print_patch_line( if (git_buf_oom(pi->buf)) return -1; - return pi->print_cb(pi->cb_data, delta, range, line_origin, git_buf_cstr(pi->buf), git_buf_len(pi->buf)); + 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, - void *cb_data, - git_diff_data_fn print_cb) + git_diff_data_cb print_cb, + void *payload) { int error; git_buf buf = GIT_BUF_INIT; @@ -680,107 +1288,532 @@ int git_diff_print_patch( pi.diff = diff; pi.print_cb = print_cb; - pi.cb_data = cb_data; + pi.payload = payload; pi.buf = &buf; error = git_diff_foreach( - diff, &pi, print_patch_file, print_patch_hunk, print_patch_line); + diff, print_patch_file, print_patch_hunk, print_patch_line, &pi); git_buf_free(&buf); return error; } -int git_diff_blobs( - git_blob *old_blob, - git_blob *new_blob, - git_diff_options *options, - void *cb_data, - git_diff_file_fn file_cb, - git_diff_hunk_fn hunk_cb, - git_diff_data_fn line_cb) -{ - diff_output_info info; +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; - mmfile_t old_data, new_data; - git_map old_map, new_map; - xpparam_t xdiff_params; - xdemitconf_t xdiff_config; - xdemitcb_t xdiff_callback; - git_blob *new, *old; + 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 */ - memset(&delta, 0, sizeof(delta)); + data->patch.delta = delta; + delta->flags = delta->flags & ~KNOWN_BINARY_FLAGS; - new = new_blob; - old = old_blob; + delta->status = has_new ? + (has_old ? GIT_DELTA_MODIFIED : GIT_DELTA_ADDED) : + (has_old ? GIT_DELTA_DELETED : GIT_DELTA_UNTRACKED); - if (options && (options->flags & GIT_DIFF_REVERSE)) { - git_blob *swap = old; - old = new; - new = swap; + 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; } - if (old) { - old_data.ptr = (char *)git_blob_rawcontent(old); - old_data.size = git_blob_rawsize(old); - git_oid_cpy(&delta.old_file.oid, git_object_id((const git_object *)old)); + 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 { - old_data.ptr = ""; - old_data.size = 0; + 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); } - if (new) { - new_data.ptr = (char *)git_blob_rawcontent(new); - new_data.size = git_blob_rawsize(new); - git_oid_cpy(&delta.new_file.oid, git_object_id((const git_object *)new)); - } else { - new_data.ptr = ""; - new_data.size = 0; + 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); } - /* populate a "fake" delta record */ - delta.status = new ? - (old ? GIT_DELTA_MODIFIED : GIT_DELTA_ADDED) : - (old ? GIT_DELTA_DELETED : GIT_DELTA_UNTRACKED); + return count; +} - if (git_oid_cmp(&delta.new_file.oid, &delta.old_file.oid) == 0) - delta.status = GIT_DELTA_UNMODIFIED; +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; - delta.old_file.size = old_data.size; - delta.new_file.size = new_data.size; + if (patch_ptr) + *patch_ptr = NULL; + if (delta_ptr) + *delta_ptr = NULL; - fill_map_from_mmfile(&old_map, &old_data); - fill_map_from_mmfile(&new_map, &new_data); + if (diff_required(diff, "git_diff_get_patch") < 0) + return -1; - if (file_is_binary_by_content(&delta, &old_map, &new_map) < 0) + if (diff_context_init( + &ctxt, diff, NULL, NULL, + NULL, diff_patch_hunk_cb, diff_patch_line_cb, NULL) < 0) return -1; - if (file_cb != NULL) { - int error = file_cb(cb_data, &delta, 1); - if (error < 0) - return error; + delta = git_vector_get(&diff->deltas, idx); + if (!delta) { + giterr_set(GITERR_INVALID, "Index out of range for delta in diff"); + return GIT_ENOTFOUND; } - /* don't do hunk and line diffs if the two blobs are identical */ - if (delta.status == GIT_DELTA_UNMODIFIED) + 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 do hunk and line diffs if file is binary */ - if (delta.binary == 1) + /* Don't load the patch if the user doesn't want it */ + if (!patch_ptr) return 0; - info.diff = NULL; - info.delta = δ - info.cb_data = cb_data; - info.hunk_cb = hunk_cb; - info.line_cb = line_cb; + patch = diff_patch_alloc(&ctxt, delta); + if (!patch) + return -1; + + if (!(error = diff_patch_load(&ctxt, patch))) { + ctxt.payload = patch; - setup_xdiff_options(options, &xdiff_config, &xdiff_params); - memset(&xdiff_callback, 0, sizeof(xdiff_callback)); - xdiff_callback.outf = diff_output_cb; - xdiff_callback.priv = &info; + error = diff_patch_generate(&ctxt, patch); - xdl_diff(&old_data, &new_data, &xdiff_params, &xdiff_config, &xdiff_callback); + 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 new file mode 100644 index 000000000..083355676 --- /dev/null +++ b/src/diff_output.h @@ -0,0 +1,93 @@ +/* + * 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_tform.c b/src/diff_tform.c new file mode 100644 index 000000000..efcb19d95 --- /dev/null +++ b/src/diff_tform.c @@ -0,0 +1,687 @@ +/* + * 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 "git2/config.h" +#include "git2/blob.h" +#include "hashsig.h" + +static git_diff_delta *diff_delta__dup( + const git_diff_delta *d, git_pool *pool) +{ + git_diff_delta *delta = git__malloc(sizeof(git_diff_delta)); + if (!delta) + return NULL; + + memcpy(delta, d, sizeof(git_diff_delta)); + + delta->old_file.path = git_pool_strdup(pool, d->old_file.path); + if (delta->old_file.path == NULL) + goto fail; + + if (d->new_file.path != d->old_file.path) { + delta->new_file.path = git_pool_strdup(pool, d->new_file.path); + if (delta->new_file.path == NULL) + goto fail; + } else { + delta->new_file.path = delta->old_file.path; + } + + return delta; + +fail: + git__free(delta); + return NULL; +} + +static git_diff_delta *diff_delta__merge_like_cgit( + const git_diff_delta *a, const git_diff_delta *b, git_pool *pool) +{ + git_diff_delta *dup; + + /* Emulate C git for merging two diffs (a la 'git diff <sha>'). + * + * When C git does a diff between the work dir and a tree, it actually + * diffs with the index but uses the workdir contents. This emulates + * those choices so we can emulate the type of diff. + * + * We have three file descriptions here, let's call them: + * f1 = a->old_file + * f2 = a->new_file AND b->old_file + * f3 = b->new_file + */ + + /* if f2 == f3 or f2 is deleted, then just dup the 'a' diff */ + if (b->status == GIT_DELTA_UNMODIFIED || a->status == GIT_DELTA_DELETED) + return diff_delta__dup(a, pool); + + /* otherwise, base this diff on the 'b' diff */ + if ((dup = diff_delta__dup(b, pool)) == NULL) + return NULL; + + /* If 'a' status is uninteresting, then we're done */ + if (a->status == GIT_DELTA_UNMODIFIED) + return dup; + + assert(a->status != GIT_DELTA_UNMODIFIED); + assert(b->status != GIT_DELTA_UNMODIFIED); + + /* A cgit exception is that the diff of a file that is only in the + * index (i.e. not in HEAD nor workdir) is given as empty. + */ + if (dup->status == GIT_DELTA_DELETED) { + if (a->status == GIT_DELTA_ADDED) + dup->status = GIT_DELTA_UNMODIFIED; + /* else don't overwrite DELETE status */ + } else { + dup->status = a->status; + } + + git_oid_cpy(&dup->old_file.oid, &a->old_file.oid); + dup->old_file.mode = a->old_file.mode; + dup->old_file.size = a->old_file.size; + dup->old_file.flags = a->old_file.flags; + + return dup; +} + +int git_diff_merge( + git_diff_list *onto, + const git_diff_list *from) +{ + int error = 0; + git_pool onto_pool; + git_vector onto_new; + git_diff_delta *delta; + bool ignore_case = false; + unsigned int i, j; + + assert(onto && from); + + if (!from->deltas.length) + return 0; + + if (git_vector_init( + &onto_new, onto->deltas.length, git_diff_delta__cmp) < 0 || + git_pool_init(&onto_pool, 1, 0) < 0) + return -1; + + if ((onto->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0 || + (from->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0) + { + ignore_case = true; + + /* This function currently only supports merging diff lists that + * are sorted identically. */ + assert((onto->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0 && + (from->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0); + } + + for (i = 0, j = 0; i < onto->deltas.length || j < from->deltas.length; ) { + git_diff_delta *o = GIT_VECTOR_GET(&onto->deltas, i); + const git_diff_delta *f = GIT_VECTOR_GET(&from->deltas, j); + int cmp = !f ? -1 : !o ? 1 : STRCMP_CASESELECT(ignore_case, o->old_file.path, f->old_file.path); + + if (cmp < 0) { + delta = diff_delta__dup(o, &onto_pool); + i++; + } else if (cmp > 0) { + delta = diff_delta__dup(f, &onto_pool); + j++; + } else { + delta = diff_delta__merge_like_cgit(o, f, &onto_pool); + i++; + j++; + } + + /* the ignore rules for the target may not match the source + * or the result of a merged delta could be skippable... + */ + if (git_diff_delta__should_skip(&onto->opts, delta)) { + git__free(delta); + continue; + } + + if ((error = !delta ? -1 : git_vector_insert(&onto_new, delta)) < 0) + break; + } + + if (!error) { + git_vector_swap(&onto->deltas, &onto_new); + git_pool_swap(&onto->pool, &onto_pool); + onto->new_src = from->new_src; + + /* prefix strings also come from old pool, so recreate those.*/ + onto->opts.old_prefix = + git_pool_strdup_safe(&onto->pool, onto->opts.old_prefix); + onto->opts.new_prefix = + git_pool_strdup_safe(&onto->pool, onto->opts.new_prefix); + } + + git_vector_foreach(&onto_new, i, delta) + git__free(delta); + git_vector_free(&onto_new); + git_pool_clear(&onto_pool); + + return error; +} + +static int 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; + int error = 0; + + GIT_UNUSED(f); + error = git_hashsig_create_fromfile((git_hashsig **)out, path, opt); + + if (error == GIT_EBUFS) { + error = 0; + giterr_clear(); + } + + return error; +} + +static int 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(); + } + + return error; +} + +static void find_similar__hashsig_free(void *sig, void *payload) +{ + GIT_UNUSED(payload); + git_hashsig_free(sig); +} + +static int find_similar__calc_similarity( + int *score, void *siga, void *sigb, void *payload) +{ + GIT_UNUSED(payload); + *score = git_hashsig_compare(siga, sigb); + return 0; +} + +#define DEFAULT_THRESHOLD 50 +#define DEFAULT_BREAK_REWRITE_THRESHOLD 60 +#define DEFAULT_TARGET_LIMIT 200 + +static int normalize_find_opts( + git_diff_list *diff, + git_diff_find_options *opts, + git_diff_find_options *given) +{ + git_config *cfg = NULL; + + if (diff->repo != NULL && + git_repository_config__weakptr(&cfg, diff->repo) < 0) + return -1; + + if (given != NULL) + memcpy(opts, given, sizeof(*opts)); + else { + const char *val = NULL; + + GIT_INIT_STRUCTURE(opts, GIT_DIFF_FIND_OPTIONS_VERSION); + + opts->flags = GIT_DIFF_FIND_RENAMES; + + if (git_config_get_string(&val, cfg, "diff.renames") < 0) + giterr_clear(); + else if (val && + (!strcasecmp(val, "copies") || !strcasecmp(val, "copy"))) + opts->flags = GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES; + } + + GITERR_CHECK_VERSION(opts, GIT_DIFF_FIND_OPTIONS_VERSION, "git_diff_find_options"); + + /* some flags imply others */ + + 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; + +#define USE_DEFAULT(X) ((X) == 0 || (X) > 100) + + if (USE_DEFAULT(opts->rename_threshold)) + opts->rename_threshold = DEFAULT_THRESHOLD; + + if (USE_DEFAULT(opts->rename_from_rewrite_threshold)) + opts->rename_from_rewrite_threshold = DEFAULT_THRESHOLD; + + if (USE_DEFAULT(opts->copy_threshold)) + opts->copy_threshold = DEFAULT_THRESHOLD; + + if (USE_DEFAULT(opts->break_rewrite_threshold)) + opts->break_rewrite_threshold = DEFAULT_BREAK_REWRITE_THRESHOLD; + +#undef USE_DEFAULT + + if (!opts->target_limit) { + int32_t limit = 0; + + opts->target_limit = DEFAULT_TARGET_LIMIT; + + if (git_config_get_int32(&limit, cfg, "diff.renameLimit") < 0) + giterr_clear(); + else 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 = 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; + + 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 apply_splits_and_deletes(git_diff_list *diff, size_t expected_size) +{ + git_vector onto = GIT_VECTOR_INIT; + size_t i; + git_diff_delta *delta; + + if (git_vector_init(&onto, expected_size, git_diff_delta__cmp) < 0) + return -1; + + /* build new delta list without TO_DELETE and splitting TO_SPLIT */ + git_vector_foreach(&diff->deltas, i, delta) { + 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) + goto on_error; + + deleted->status = GIT_DELTA_DELETED; + memset(&deleted->new_file, 0, sizeof(deleted->new_file)); + deleted->new_file.path = deleted->old_file.path; + deleted->new_file.flags |= GIT_DIFF_FLAG_VALID_OID; + + if (git_vector_insert(&onto, deleted) < 0) + goto on_error; + + 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; + } + + if (git_vector_insert(&onto, delta) < 0) + goto on_error; + } + + /* cannot return an error past this point */ + 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); + + return 0; + +on_error: + git_vector_foreach(&onto, i, delta) + git__free(delta); + + git_vector_free(&onto); + + return -1; +} + +GIT_INLINE(git_diff_file *) similarity_get_file(git_diff_list *diff, size_t idx) +{ + git_diff_delta *delta = git_vector_get(&diff->deltas, idx / 2); + 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, + 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; + + /* TODO: apply wd-to-odb filters to file data if necessary */ + + 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); + + git_buf_free(&path); + } else { /* compute hashsig from blob buffer */ + git_blob *blob = NULL; + git_off_t blobsize; + + /* TODO: add max size threshold a la diff? */ + + if ((error = git_blob_lookup(&blob, diff->repo, &file->oid)) < 0) + return error; + + blobsize = git_blob_rawsize(blob); + if (!git__is_sizet(blobsize)) /* ? what to do ? */ + blobsize = (size_t)-1; + + error = opts->metric->buffer_signature( + &cache[file_idx], file, git_blob_rawcontent(blob), + (size_t)blobsize, opts->metric->payload); + + git_blob_free(blob); + } + + return error; +} + +static int similarity_measure( + git_diff_list *diff, + 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); + + 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; + + /* 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]) + 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; +} + +#define FLAG_SET(opts,flag_name) ((opts.flags & flag_name) != 0) + +int git_diff_find_similar( + git_diff_list *diff, + git_diff_find_options *given_opts) +{ + size_t i, j, cache_size, *matches; + int error = 0, similarity; + git_diff_delta *from, *to; + git_diff_find_options opts; + size_t tried_targets, num_rewrites = 0; + void **cache; + + if ((error = normalize_find_opts(diff, &opts, given_opts)) < 0) + return error; + + /* TODO: maybe abort if deltas.length > target_limit ??? */ + + cache_size = diff->deltas.length * 2; /* must store b/c length may change */ + cache = git__calloc(cache_size, sizeof(void *)); + GITERR_CHECK_ALLOC(cache); + + matches = git__calloc(diff->deltas.length, sizeof(size_t)); + GITERR_CHECK_ALLOC(matches); + + /* first break MODIFIED records that are too different (if requested) */ + + if (FLAG_SET(opts, GIT_DIFF_FIND_AND_BREAK_REWRITES)) { + git_vector_foreach(&diff->deltas, i, from) { + if (from->status != GIT_DELTA_MODIFIED) + continue; + + similarity = similarity_measure( + diff, &opts, cache, 2 * i, 2 * i + 1); + + if (similarity < 0) { + error = similarity; + goto cleanup; + } + + if ((unsigned int)similarity < opts.break_rewrite_threshold) { + from->flags |= GIT_DIFF_FLAG__TO_SPLIT; + num_rewrites++; + } + } + } + + /* next find the most similar delta for each rename / copy candidate */ + + git_vector_foreach(&diff->deltas, i, from) { + tried_targets = 0; + + /* skip things that aren't blobs */ + if (GIT_MODE_TYPE(from->old_file.mode) != + GIT_MODE_TYPE(GIT_FILEMODE_BLOB)) + continue; + + /* 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)) + 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; + + git_vector_foreach(&diff->deltas, j, to) { + if (i == j) + continue; + + /* skip things that aren't blobs */ + if (GIT_MODE_TYPE(to->new_file.mode) != + GIT_MODE_TYPE(GIT_FILEMODE_BLOB)) + continue; + + 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 */ + 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; + } + + if (to->similarity < (unsigned int)similarity) { + to->similarity = (unsigned int)similarity; + matches[j] = i + 1; + } + } + } + + /* next rewrite the diffs with renames / copies */ + + git_vector_foreach(&diff->deltas, j, to) { + if (!matches[j]) { + assert(to->similarity == 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 + */ + + if (from->status == GIT_DELTA_DELETED) { + if (to->similarity < opts.rename_threshold) { + to->similarity = 0; + continue; + } + + to->status = GIT_DELTA_RENAMED; + memcpy(&to->old_file, &from->old_file, sizeof(to->old_file)); + + from->flags |= GIT_DIFF_FLAG__TO_DELETE; + num_rewrites++; + + continue; + } + + if (from->status == GIT_DELTA_MODIFIED && + (from->flags & GIT_DIFF_FLAG__TO_SPLIT) != 0) + { + if (to->similarity < opts.rename_threshold) { + to->similarity = 0; + continue; + } + + to->status = GIT_DELTA_RENAMED; + memcpy(&to->old_file, &from->old_file, sizeof(to->old_file)); + + from->status = GIT_DELTA_ADDED; + from->flags &= ~GIT_DIFF_FLAG__TO_SPLIT; + memset(&from->old_file, 0, sizeof(from->old_file)); + num_rewrites--; + + continue; + } + + 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); + + if (similarity < 0) { + error = similarity; + goto cleanup; + } + + 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)); + + 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; + + continue; + } + } + + if (to->similarity < opts.copy_threshold) { + to->similarity = 0; + continue; + } + + /* convert "to" to a COPIED record */ + to->status = GIT_DELTA_COPIED; + memcpy(&to->old_file, &from->old_file, sizeof(to->old_file)); + } + + if (num_rewrites > 0) { + assert(num_rewrites < diff->deltas.length); + + error = apply_splits_and_deletes( + diff, diff->deltas.length - num_rewrites); + } + +cleanup: + git__free(matches); + + for (i = 0; i < cache_size; ++i) { + if (cache[i] != NULL) + opts.metric->free_signature(cache[i], opts.metric->payload); + } + git__free(cache); + + if (!given_opts || !given_opts->metric) + git__free(opts.metric); + + return error; +} + +#undef FLAG_SET diff --git a/src/errors.c b/src/errors.c index d43d7d9b5..e2629f69e 100644 --- a/src/errors.c +++ b/src/errors.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -40,52 +40,34 @@ void giterr_set(int error_class, const char *string, ...) { git_buf buf = GIT_BUF_INIT; va_list arglist; - - int unix_error_code = 0; - -#ifdef GIT_WIN32 - DWORD win32_error_code = 0; -#endif - - if (error_class == GITERR_OS) { - unix_error_code = errno; - errno = 0; - #ifdef GIT_WIN32 - win32_error_code = GetLastError(); - SetLastError(0); + DWORD win32_error_code = (error_class == GITERR_OS) ? GetLastError() : 0; #endif - } + int error_code = (error_class == GITERR_OS) ? errno : 0; va_start(arglist, string); git_buf_vprintf(&buf, string, arglist); va_end(arglist); - /* automatically suffix strerror(errno) for GITERR_OS errors */ if (error_class == GITERR_OS) { - - if (unix_error_code != 0) { +#ifdef GIT_WIN32 + char * win32_error = git_win32_get_error_message(win32_error_code); + if (win32_error) { git_buf_PUTS(&buf, ": "); - git_buf_puts(&buf, strerror(unix_error_code)); - } + git_buf_puts(&buf, win32_error); + git__free(win32_error); -#ifdef GIT_WIN32 - else if (win32_error_code != 0) { - LPVOID lpMsgBuf = NULL; - - FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, win32_error_code, 0, (LPTSTR) &lpMsgBuf, 0, NULL); - - if (lpMsgBuf) { - git_buf_PUTS(&buf, ": "); - git_buf_puts(&buf, lpMsgBuf); - LocalFree(lpMsgBuf); - } + SetLastError(0); } + else #endif + if (error_code) { + git_buf_PUTS(&buf, ": "); + git_buf_puts(&buf, strerror(error_code)); + } + + if (error_code) + errno = 0; } if (!git_buf_oom(&buf)) @@ -94,22 +76,40 @@ void giterr_set(int error_class, const char *string, ...) void giterr_set_str(int error_class, const char *string) { - char *message = git__strdup(string); + char *message; + + assert(string); + + message = git__strdup(string); if (message) set_error(error_class, message); } -void giterr_set_regex(const regex_t *regex, int error_code) +int giterr_set_regex(const regex_t *regex, int error_code) { char error_buf[1024]; + + assert(error_code); + regerror(error_code, regex, error_buf, sizeof(error_buf)); giterr_set_str(GITERR_REGEX, error_buf); + + if (error_code == REG_NOMATCH) + return GIT_ENOTFOUND; + + return GIT_EINVALIDSPEC; } void giterr_clear(void) { + set_error(0, NULL); GIT_GLOBAL->last_error = NULL; + + errno = 0; +#ifdef GIT_WIN32 + SetLastError(0); +#endif } const git_error *giterr_last(void) diff --git a/src/fetch.c b/src/fetch.c index c92cf4ef5..b60a95232 100644 --- a/src/fetch.c +++ b/src/fetch.c @@ -1,18 +1,16 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ -#include "git2/remote.h" #include "git2/oid.h" #include "git2/refs.h" #include "git2/revwalk.h" -#include "git2/indexer.h" +#include "git2/transport.h" #include "common.h" -#include "transport.h" #include "remote.h" #include "refspec.h" #include "pack.h" @@ -21,7 +19,7 @@ struct filter_payload { git_remote *remote; - const git_refspec *spec; + const git_refspec *spec, *tagspec; git_odb *odb; int found_head; }; @@ -29,18 +27,21 @@ struct filter_payload { static int filter_ref__cb(git_remote_head *head, void *payload) { struct filter_payload *p = payload; + int match = 0; - if (!p->found_head && strcmp(head->name, GIT_HEAD_FILE) == 0) { + if (!git_reference_is_valid_name(head->name)) + return 0; + + if (!p->found_head && strcmp(head->name, GIT_HEAD_FILE) == 0) p->found_head = 1; - } else { - /* If it doesn't match the refpec, we don't want it */ - if (!git_refspec_src_matches(p->spec, head->name)) - return 0; - - /* Don't even try to ask for the annotation target */ - if (!git__suffixcmp(head->name, "^{}")) - return 0; - } + else if (git_refspec_src_matches(p->spec, head->name)) + match = 1; + else if (p->remote->download_tags == GIT_REMOTE_DOWNLOAD_TAGS_ALL && + git_refspec_src_matches(p->tagspec, head->name)) + match = 1; + + if (!match) + return 0; /* If we have the object, mark it so we don't ask for it */ if (git_odb_exists(p->odb, &head->oid)) @@ -54,8 +55,12 @@ static int filter_ref__cb(git_remote_head *head, void *payload) static int filter_wants(git_remote *remote) { struct filter_payload p; + git_refspec tagspec; + int error = -1; git_vector_clear(&remote->refs); + if (git_refspec__parse(&tagspec, GIT_REFSPEC_TAGS, true) < 0) + return error; /* * The fetch refspec can be NULL, and what this means is that the @@ -64,13 +69,19 @@ static int filter_wants(git_remote *remote) * 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; if (git_repository_odb__weakptr(&p.odb, remote->repo) < 0) - return -1; + goto cleanup; - return remote->transport->ls(remote->transport, &filter_ref__cb, &p); + error = git_remote_ls(remote, filter_ref__cb, &p); + +cleanup: + git_refspec__free(&tagspec); + + return error; } /* @@ -81,7 +92,7 @@ static int filter_wants(git_remote *remote) int git_fetch_negotiate(git_remote *remote) { git_transport *t = remote->transport; - + if (filter_wants(remote) < 0) { giterr_set(GITERR_NET, "Failed to filter the reference list for wants"); return -1; @@ -92,109 +103,24 @@ int git_fetch_negotiate(git_remote *remote) return 0; /* - * Now we have everything set up so we can start tell the server - * what we want and what we have. + * Now we have everything set up so we can start tell the + * server what we want and what we have. */ - return t->negotiate_fetch(t, remote->repo, &remote->refs); + return t->negotiate_fetch(t, + remote->repo, + (const git_remote_head * const *)remote->refs.contents, + remote->refs.length); } -int git_fetch_download_pack(git_remote *remote, git_off_t *bytes, git_indexer_stats *stats) +int git_fetch_download_pack( + git_remote *remote, + git_transfer_progress_callback progress_cb, + void *progress_payload) { + git_transport *t = remote->transport; + if(!remote->need_pack) return 0; - return remote->transport->download_pack(remote->transport, remote->repo, bytes, stats); -} - -/* Receiving data from a socket and storing it is pretty much the same for git and HTTP */ -int git_fetch__download_pack( - const char *buffered, - size_t buffered_size, - GIT_SOCKET fd, - git_repository *repo, - git_off_t *bytes, - git_indexer_stats *stats) -{ - int recvd; - char buff[1024]; - gitno_buffer buf; - git_indexer_stream *idx; - - gitno_buffer_setup(&buf, buff, sizeof(buff), fd); - - if (memcmp(buffered, "PACK", strlen("PACK"))) { - giterr_set(GITERR_NET, "The pack doesn't start with the signature"); - return -1; - } - - if (git_indexer_stream_new(&idx, git_repository_path(repo)) < 0) - return -1; - - memset(stats, 0, sizeof(git_indexer_stats)); - if (git_indexer_stream_add(idx, buffered, buffered_size, stats) < 0) - goto on_error; - - *bytes = buffered_size; - - do { - if (git_indexer_stream_add(idx, buf.data, buf.offset, stats) < 0) - goto on_error; - - gitno_consume_n(&buf, buf.offset); - if ((recvd = gitno_recv(&buf)) < 0) - goto on_error; - - *bytes += recvd; - } while(recvd > 0); - - if (git_indexer_stream_finalize(idx, stats)) - goto on_error; - - git_indexer_stream_free(idx); - return 0; - -on_error: - git_indexer_stream_free(idx); - return -1; -} - -int git_fetch_setup_walk(git_revwalk **out, git_repository *repo) -{ - git_revwalk *walk; - git_strarray refs; - unsigned int i; - git_reference *ref; - - if (git_reference_list(&refs, repo, GIT_REF_LISTALL) < 0) - return -1; - - if (git_revwalk_new(&walk, repo) < 0) - return -1; - - git_revwalk_sorting(walk, GIT_SORT_TIME); - - for (i = 0; i < refs.count; ++i) { - /* No tags */ - if (!git__prefixcmp(refs.strings[i], GIT_REFS_TAGS_DIR)) - continue; - - if (git_reference_lookup(&ref, repo, refs.strings[i]) < 0) - goto on_error; - - if (git_reference_type(ref) == GIT_REF_SYMBOLIC) - continue; - if (git_revwalk_push(walk, git_reference_oid(ref)) < 0) - goto on_error; - - git_reference_free(ref); - } - - git_strarray_free(&refs); - *out = walk; - return 0; - -on_error: - git_reference_free(ref); - git_strarray_free(&refs); - return -1; + return t->download_pack(t, remote->repo, &remote->stats, progress_cb, progress_payload); } diff --git a/src/fetch.h b/src/fetch.h index b3192a563..059251d04 100644 --- a/src/fetch.h +++ b/src/fetch.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -10,10 +10,19 @@ #include "netops.h" int git_fetch_negotiate(git_remote *remote); -int git_fetch_download_pack(git_remote *remote, git_off_t *bytes, git_indexer_stats *stats); -int git_fetch__download_pack(const char *buffered, size_t buffered_size, GIT_SOCKET fd, - git_repository *repo, git_off_t *bytes, git_indexer_stats *stats); +int git_fetch_download_pack( + git_remote *remote, + git_transfer_progress_callback progress_cb, + void *progress_payload); + +int git_fetch__download_pack( + git_transport *t, + git_repository *repo, + git_transfer_progress *stats, + git_transfer_progress_callback progress_cb, + void *progress_payload); + int git_fetch_setup_walk(git_revwalk **out, git_repository *repo); #endif diff --git a/src/fetchhead.c b/src/fetchhead.c new file mode 100644 index 000000000..4dcebb857 --- /dev/null +++ b/src/fetchhead.c @@ -0,0 +1,295 @@ +/* + * 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/types.h" +#include "git2/oid.h" + +#include "fetchhead.h" +#include "common.h" +#include "buffer.h" +#include "fileops.h" +#include "filebuf.h" +#include "refs.h" +#include "repository.h" + +int git_fetchhead_ref_cmp(const void *a, const void *b) +{ + const git_fetchhead_ref *one = (const git_fetchhead_ref *)a; + const git_fetchhead_ref *two = (const git_fetchhead_ref *)b; + + if (one->is_merge && !two->is_merge) + return -1; + if (two->is_merge && !one->is_merge) + return 1; + + if (one->ref_name && two->ref_name) + return strcmp(one->ref_name, two->ref_name); + else if (one->ref_name) + return -1; + else if (two->ref_name) + return 1; + + return 0; +} + +int git_fetchhead_ref_create( + git_fetchhead_ref **out, + git_oid *oid, + unsigned int is_merge, + const char *ref_name, + const char *remote_url) +{ + git_fetchhead_ref *fetchhead_ref; + + assert(out && oid); + + *out = NULL; + + fetchhead_ref = git__malloc(sizeof(git_fetchhead_ref)); + GITERR_CHECK_ALLOC(fetchhead_ref); + + memset(fetchhead_ref, 0x0, sizeof(git_fetchhead_ref)); + + git_oid_cpy(&fetchhead_ref->oid, oid); + fetchhead_ref->is_merge = is_merge; + + if (ref_name) + fetchhead_ref->ref_name = git__strdup(ref_name); + + if (remote_url) + fetchhead_ref->remote_url = git__strdup(remote_url); + + *out = fetchhead_ref; + + return 0; +} + +static int fetchhead_ref_write( + git_filebuf *file, + git_fetchhead_ref *fetchhead_ref) +{ + char oid[GIT_OID_HEXSZ + 1]; + const char *type, *name; + + assert(file && fetchhead_ref); + + git_oid_fmt(oid, &fetchhead_ref->oid); + oid[GIT_OID_HEXSZ] = '\0'; + + if (git__prefixcmp(fetchhead_ref->ref_name, GIT_REFS_HEADS_DIR) == 0) { + type = "branch "; + name = fetchhead_ref->ref_name + strlen(GIT_REFS_HEADS_DIR); + } else if(git__prefixcmp(fetchhead_ref->ref_name, + GIT_REFS_TAGS_DIR) == 0) { + type = "tag "; + name = fetchhead_ref->ref_name + strlen(GIT_REFS_TAGS_DIR); + } else { + type = ""; + name = fetchhead_ref->ref_name; + } + + return git_filebuf_printf(file, "%s\t%s\t%s'%s' of %s\n", + oid, + (fetchhead_ref->is_merge) ? "" : "not-for-merge", + type, + name, + fetchhead_ref->remote_url); +} + +int git_fetchhead_write(git_repository *repo, git_vector *fetchhead_refs) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_buf path = GIT_BUF_INIT; + unsigned int i; + git_fetchhead_ref *fetchhead_ref; + + assert(repo && fetchhead_refs); + + if (git_buf_joinpath(&path, repo->path_repository, GIT_FETCH_HEAD_FILE) < 0) + return -1; + + if (git_filebuf_open(&file, path.ptr, GIT_FILEBUF_FORCE) < 0) { + git_buf_free(&path); + return -1; + } + + git_buf_free(&path); + + git_vector_sort(fetchhead_refs); + + git_vector_foreach(fetchhead_refs, i, fetchhead_ref) + fetchhead_ref_write(&file, fetchhead_ref); + + return git_filebuf_commit(&file, GIT_REFS_FILE_MODE); +} + +static int fetchhead_ref_parse( + git_oid *oid, + unsigned int *is_merge, + git_buf *ref_name, + const char **remote_url, + char *line, + size_t line_num) +{ + char *oid_str, *is_merge_str, *desc, *name = NULL; + const char *type = NULL; + int error = 0; + + *remote_url = NULL; + + if (!*line) { + giterr_set(GITERR_FETCHHEAD, + "Empty line in FETCH_HEAD line %d", line_num); + return -1; + } + + /* Compat with old git clients that wrote FETCH_HEAD like a loose ref. */ + if ((oid_str = git__strsep(&line, "\t")) == NULL) { + oid_str = line; + line += strlen(line); + + *is_merge = 1; + } + + if (strlen(oid_str) != GIT_OID_HEXSZ) { + giterr_set(GITERR_FETCHHEAD, + "Invalid object ID in FETCH_HEAD line %d", line_num); + return -1; + } + + if (git_oid_fromstr(oid, oid_str) < 0) { + const git_error *oid_err = giterr_last(); + const char *err_msg = oid_err ? oid_err->message : "Invalid object ID"; + + giterr_set(GITERR_FETCHHEAD, "%s in FETCH_HEAD line %d", + err_msg, line_num); + return -1; + } + + /* Parse new data from newer git clients */ + if (*line) { + if ((is_merge_str = git__strsep(&line, "\t")) == NULL) { + giterr_set(GITERR_FETCHHEAD, + "Invalid description data in FETCH_HEAD line %d", line_num); + return -1; + } + + if (*is_merge_str == '\0') + *is_merge = 1; + else if (strcmp(is_merge_str, "not-for-merge") == 0) + *is_merge = 0; + else { + giterr_set(GITERR_FETCHHEAD, + "Invalid for-merge entry in FETCH_HEAD line %d", line_num); + return -1; + } + + if ((desc = line) == NULL) { + giterr_set(GITERR_FETCHHEAD, + "Invalid description in FETCH_HEAD line %d", line_num); + return -1; + } + + if (git__prefixcmp(desc, "branch '") == 0) { + type = GIT_REFS_HEADS_DIR; + name = desc + 8; + } else if (git__prefixcmp(desc, "tag '") == 0) { + type = GIT_REFS_TAGS_DIR; + name = desc + 5; + } else if (git__prefixcmp(desc, "'") == 0) + name = desc + 1; + + if (name) { + if ((desc = strchr(name, '\'')) == NULL || + git__prefixcmp(desc, "' of ") != 0) { + giterr_set(GITERR_FETCHHEAD, + "Invalid description in FETCH_HEAD line %d", line_num); + return -1; + } + + *desc = '\0'; + desc += 5; + } + + *remote_url = desc; + } + + git_buf_clear(ref_name); + + if (type) + git_buf_join(ref_name, '/', type, name); + else if(name) + git_buf_puts(ref_name, name); + + return error; +} + +int git_repository_fetchhead_foreach(git_repository *repo, + git_repository_fetchhead_foreach_cb cb, + void *payload) +{ + git_buf path = GIT_BUF_INIT, file = GIT_BUF_INIT, name = GIT_BUF_INIT; + const char *ref_name; + git_oid oid; + const char *remote_url; + unsigned int is_merge = 0; + char *buffer, *line; + size_t line_num = 0; + int error = 0; + + assert(repo && cb); + + if (git_buf_joinpath(&path, repo->path_repository, GIT_FETCH_HEAD_FILE) < 0) + return -1; + + if ((error = git_futils_readbuffer(&file, git_buf_cstr(&path))) < 0) + goto done; + + buffer = file.ptr; + + while ((line = git__strsep(&buffer, "\n")) != NULL) { + ++line_num; + + if ((error = fetchhead_ref_parse(&oid, &is_merge, &name, &remote_url, + line, line_num)) < 0) + goto done; + + if (git_buf_len(&name) > 0) + ref_name = git_buf_cstr(&name); + else + ref_name = NULL; + + if ((cb(ref_name, remote_url, &oid, is_merge, payload)) != 0) { + error = GIT_EUSER; + goto done; + } + } + + if (*buffer) { + giterr_set(GITERR_FETCHHEAD, "No EOL at line %d", line_num+1); + error = -1; + goto done; + } + +done: + git_buf_free(&file); + git_buf_free(&path); + git_buf_free(&name); + + return error; +} + +void git_fetchhead_ref_free(git_fetchhead_ref *fetchhead_ref) +{ + if (fetchhead_ref == NULL) + return; + + git__free(fetchhead_ref->remote_url); + git__free(fetchhead_ref->ref_name); + git__free(fetchhead_ref); +} + diff --git a/src/fetchhead.h b/src/fetchhead.h new file mode 100644 index 000000000..74fce049b --- /dev/null +++ b/src/fetchhead.h @@ -0,0 +1,34 @@ +/* + * 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_fetchhead_h__ +#define INCLUDE_fetchhead_h__ + +#include "vector.h" + +struct git_fetchhead_ref { + git_oid oid; + unsigned int is_merge; + char *ref_name; + char *remote_url; +}; + +typedef struct git_fetchhead_ref git_fetchhead_ref; + +int git_fetchhead_ref_create( + git_fetchhead_ref **fetchhead_ref_out, + git_oid *oid, + unsigned int is_merge, + const char *ref_name, + const char *remote_url); + +int git_fetchhead_ref_cmp(const void *a, const void *b); + +int git_fetchhead_write(git_repository *repo, git_vector *fetchhead_refs); + +void git_fetchhead_ref_free(git_fetchhead_ref *fetchhead_ref); + +#endif diff --git a/src/filebuf.c b/src/filebuf.c index 6538aea66..246ae34e7 100644 --- a/src/filebuf.c +++ b/src/filebuf.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -50,6 +50,7 @@ static int lock_file(git_filebuf *file, int flags) if (flags & GIT_FILEBUF_FORCE) p_unlink(file->path_lock); else { + giterr_clear(); /* actual OS error code just confuses */ giterr_set(GITERR_OS, "Failed to lock file '%s' for writing", file->path_lock); return -1; @@ -72,7 +73,7 @@ static int lock_file(git_filebuf *file, int flags) if ((flags & GIT_FILEBUF_APPEND) && git_path_exists(file->path_original) == true) { git_file source; char buffer[2048]; - size_t read_bytes; + ssize_t read_bytes; source = p_open(file->path_original, O_RDONLY); if (source < 0) { @@ -82,13 +83,18 @@ static int lock_file(git_filebuf *file, int flags) return -1; } - while ((read_bytes = p_read(source, buffer, 2048)) > 0) { + while ((read_bytes = p_read(source, buffer, sizeof(buffer))) > 0) { p_write(file->fd, buffer, read_bytes); - if (file->digest) - git_hash_update(file->digest, buffer, read_bytes); + if (file->compute_digest) + git_hash_update(&file->digest, buffer, read_bytes); } p_close(source); + + if (read_bytes < 0) { + giterr_set(GITERR_OS, "Failed to read file '%s'", file->path_original); + return -1; + } } return 0; @@ -102,8 +108,10 @@ void git_filebuf_cleanup(git_filebuf *file) if (file->fd_is_open && file->path_lock && git_path_exists(file->path_lock)) p_unlink(file->path_lock); - if (file->digest) - git_hash_free_ctx(file->digest); + if (file->compute_digest) { + git_hash_ctx_cleanup(&file->digest); + file->compute_digest = 0; + } if (file->buffer) git__free(file->buffer); @@ -130,6 +138,11 @@ GIT_INLINE(int) flush_buffer(git_filebuf *file) return result; } +int git_filebuf_flush(git_filebuf *file) +{ + return flush_buffer(file); +} + static int write_normal(git_filebuf *file, void *source, size_t len) { if (len > 0) { @@ -138,8 +151,8 @@ static int write_normal(git_filebuf *file, void *source, size_t len) return -1; } - if (file->digest) - git_hash_update(file->digest, source, len); + if (file->compute_digest) + git_hash_update(&file->digest, source, len); } return 0; @@ -175,8 +188,8 @@ static int write_deflate(git_filebuf *file, void *source, size_t len) assert(zs->avail_in == 0); - if (file->digest) - git_hash_update(file->digest, source, len); + if (file->compute_digest) + git_hash_update(&file->digest, source, len); } return 0; @@ -210,8 +223,10 @@ int git_filebuf_open(git_filebuf *file, const char *path, int flags) /* If we are hashing on-write, allocate a new hash context */ if (flags & GIT_FILEBUF_HASH_CONTENTS) { - file->digest = git_hash_new_ctx(); - GITERR_CHECK_ALLOC(file->digest); + file->compute_digest = 1; + + if (git_hash_ctx_init(&file->digest) < 0) + goto cleanup; } compression = flags >> GIT_FILEBUF_DEFLATE_SHIFT; @@ -280,16 +295,16 @@ cleanup: int git_filebuf_hash(git_oid *oid, git_filebuf *file) { - assert(oid && file && file->digest); + assert(oid && file && file->compute_digest); flush_buffer(file); if (verify_last_error(file) < 0) return -1; - git_hash_final(oid, file->digest); - git_hash_free_ctx(file->digest); - file->digest = NULL; + git_hash_final(oid, &file->digest); + git_hash_ctx_cleanup(&file->digest); + file->compute_digest = 0; return 0; } @@ -314,10 +329,15 @@ int git_filebuf_commit(git_filebuf *file, mode_t mode) if (verify_last_error(file) < 0) goto on_error; - p_close(file->fd); - file->fd = -1; file->fd_is_open = false; + if (p_close(file->fd) < 0) { + giterr_set(GITERR_OS, "Failed to close file at '%s'", file->path_lock); + goto on_error; + } + + file->fd = -1; + if (p_chmod(file->path_lock, mode)) { giterr_set(GITERR_OS, "Failed to set attributes for file at '%s'", file->path_lock); goto on_error; @@ -450,3 +470,26 @@ int git_filebuf_printf(git_filebuf *file, const char *format, ...) return res; } +int git_filebuf_stats(time_t *mtime, size_t *size, git_filebuf *file) +{ + int res; + struct stat st; + + if (file->fd_is_open) + res = p_fstat(file->fd, &st); + else + res = p_stat(file->path_original, &st); + + if (res < 0) { + giterr_set(GITERR_OS, "Could not get stat info for '%s'", + file->path_original); + return res; + } + + if (mtime) + *mtime = st.st_mtime; + if (size) + *size = (size_t)st.st_size; + + return 0; +} diff --git a/src/filebuf.h b/src/filebuf.h index 72563b57a..823af81bf 100644 --- a/src/filebuf.h +++ b/src/filebuf.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -31,7 +31,8 @@ struct git_filebuf { int (*write)(struct git_filebuf *file, void *source, size_t len); - git_hash_ctx *digest; + bool compute_digest; + git_hash_ctx digest; unsigned char *buffer; unsigned char *z_buf; @@ -81,5 +82,7 @@ int git_filebuf_commit(git_filebuf *lock, mode_t mode); int git_filebuf_commit_at(git_filebuf *lock, const char *path, mode_t mode); void git_filebuf_cleanup(git_filebuf *lock); int git_filebuf_hash(git_oid *oid, git_filebuf *file); +int git_filebuf_flush(git_filebuf *file); +int git_filebuf_stats(time_t *mtime, size_t *size, git_filebuf *file); #endif diff --git a/src/fileops.c b/src/fileops.c index ee9d4212d..d6244711f 100644 --- a/src/fileops.c +++ b/src/fileops.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -7,22 +7,15 @@ #include "common.h" #include "fileops.h" #include <ctype.h> +#if GIT_WIN32 +#include "win32/findfile.h" +#endif int git_futils_mkpath2file(const char *file_path, const mode_t mode) { - int result = 0; - git_buf target_folder = GIT_BUF_INIT; - - if (git_path_dirname_r(&target_folder, file_path) < 0) - return -1; - - /* Does the containing folder exist? */ - if (git_path_isdir(target_folder.ptr) == false) - /* Let's create the tree structure */ - result = git_futils_mkdir_r(target_folder.ptr, NULL, mode); - - git_buf_free(&target_folder); - return result; + return git_futils_mkdir( + file_path, NULL, mode, + GIT_MKDIR_PATH | GIT_MKDIR_SKIP_LAST | GIT_MKDIR_VERIFY_DIR); } int git_futils_mktmp(git_buf *path_out, const char *filename) @@ -65,11 +58,10 @@ int git_futils_creat_locked(const char *path, const mode_t mode) int fd; #ifdef GIT_WIN32 - wchar_t* buf; + wchar_t buf[GIT_WIN_PATH]; - buf = gitwin_to_utf16(path); + git__utf8_to_16(buf, GIT_WIN_PATH, path); fd = _wopen(buf, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | O_EXCL, mode); - git__free(buf); #else fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | O_EXCL, mode); #endif @@ -94,7 +86,7 @@ int git_futils_open_ro(const char *path) { int fd = p_open(path, O_RDONLY); if (fd < 0) { - if (errno == ENOENT) + if (errno == ENOENT || errno == ENOTDIR) fd = GIT_ENOTFOUND; giterr_set(GITERR_OS, "Failed to open '%s'", path); } @@ -127,11 +119,35 @@ mode_t git_futils_canonical_mode(mode_t raw_mode) return 0; } -int git_futils_readbuffer_updated(git_buf *buf, const char *path, time_t *mtime, int *updated) +int git_futils_readbuffer_fd(git_buf *buf, git_file fd, size_t len) +{ + ssize_t read_size = 0; + + git_buf_clear(buf); + + if (git_buf_grow(buf, len + 1) < 0) + return -1; + + /* p_read loops internally to read len bytes */ + read_size = p_read(fd, buf->ptr, len); + + if (read_size != (ssize_t)len) { + giterr_set(GITERR_OS, "Failed to read descriptor"); + return -1; + } + + buf->ptr[read_size] = '\0'; + buf->size = read_size; + + return 0; +} + +int git_futils_readbuffer_updated( + git_buf *buf, const char *path, time_t *mtime, size_t *size, int *updated) { git_file fd; - size_t len; struct stat st; + bool changed = false; assert(buf && path && *path); @@ -148,41 +164,31 @@ int git_futils_readbuffer_updated(git_buf *buf, const char *path, time_t *mtime, } /* - * If we were given a time, we only want to read the file if it - * has been modified. + * If we were given a time and/or a size, we only want to read the file + * if it has been modified. */ - if (mtime != NULL && *mtime >= st.st_mtime) { + if (size && *size != (size_t)st.st_size) + changed = true; + if (mtime && *mtime != st.st_mtime) + changed = true; + if (!size && !mtime) + changed = true; + + if (!changed) { p_close(fd); return 0; } if (mtime != NULL) *mtime = st.st_mtime; + if (size != NULL) + *size = (size_t)st.st_size; - len = (size_t) st.st_size; - - git_buf_clear(buf); - - if (git_buf_grow(buf, len + 1) < 0) { + if (git_futils_readbuffer_fd(buf, fd, (size_t)st.st_size) < 0) { p_close(fd); return -1; } - buf->ptr[len] = '\0'; - - while (len > 0) { - ssize_t read_size = p_read(fd, buf->ptr, len); - - if (read_size < 0) { - p_close(fd); - giterr_set(GITERR_OS, "Failed to read descriptor for '%s'", path); - return -1; - } - - len -= read_size; - buf->size += read_size; - } - p_close(fd); if (updated != NULL) @@ -193,7 +199,7 @@ int git_futils_readbuffer_updated(git_buf *buf, const char *path, time_t *mtime, int git_futils_readbuffer(git_buf *buf, const char *path) { - return git_futils_readbuffer_updated(buf, path, NULL, NULL); + return git_futils_readbuffer_updated(buf, path, NULL, NULL, NULL); } int git_futils_mv_withpath(const char *from, const char *to, const mode_t dirmode) @@ -239,237 +245,771 @@ void git_futils_mmap_free(git_map *out) p_munmap(out); } -int git_futils_mkdir_r(const char *path, const char *base, const mode_t mode) +int git_futils_mkdir( + const char *path, + const char *base, + mode_t mode, + uint32_t flags) { - int root_path_offset; + int error = -1; git_buf make_path = GIT_BUF_INIT; - size_t start; - char *pp, *sp; - bool failed = false; - - if (base != NULL) { - start = strlen(base); - if (git_buf_joinpath(&make_path, base, path) < 0) - return -1; - } else { - start = 0; - if (git_buf_puts(&make_path, path) < 0) - return -1; - } + ssize_t root = 0; + char lastch, *tail; - pp = make_path.ptr + start; + /* build path and find "root" where we should start calling mkdir */ + if (git_path_join_unrooted(&make_path, path, base, &root) < 0) + return -1; - root_path_offset = git_path_root(make_path.ptr); - if (root_path_offset > 0) - pp += root_path_offset; /* On Windows, will skip the drive name (eg. C: or D:) */ + if (make_path.size == 0) { + giterr_set(GITERR_OS, "Attempt to create empty path"); + goto fail; + } - while (!failed && (sp = strchr(pp, '/')) != NULL) { - if (sp != pp && git_path_isdir(make_path.ptr) == false) { - *sp = 0; + /* remove trailing slashes on path */ + while (make_path.ptr[make_path.size - 1] == '/') { + make_path.size--; + make_path.ptr[make_path.size] = '\0'; + } - /* Do not choke while trying to recreate an existing directory */ - if (p_mkdir(make_path.ptr, mode) < 0 && errno != EEXIST) - failed = true; + /* if we are not supposed to made the last element, truncate it */ + if ((flags & GIT_MKDIR_SKIP_LAST2) != 0) { + git_buf_rtruncate_at_char(&make_path, '/'); + flags |= GIT_MKDIR_SKIP_LAST; + } + if ((flags & GIT_MKDIR_SKIP_LAST) != 0) + git_buf_rtruncate_at_char(&make_path, '/'); + + /* if we are not supposed to make the whole path, reset root */ + if ((flags & GIT_MKDIR_PATH) == 0) + root = git_buf_rfind(&make_path, '/'); + + /* clip root to make_path length */ + if (root >= (ssize_t)make_path.size) + root = (ssize_t)make_path.size - 1; + if (root < 0) + root = 0; + + tail = & make_path.ptr[root]; + + while (*tail) { + /* advance tail to include next path component */ + while (*tail == '/') + tail++; + while (*tail && *tail != '/') + tail++; + + /* truncate path at next component */ + lastch = *tail; + *tail = '\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; + } + + if (already_exists && (flags & GIT_MKDIR_EXCL) != 0) { + giterr_set(GITERR_OS, "Directory already exists '%s'", + make_path.ptr); + error = GIT_EEXISTS; + goto fail; + } + } - *sp = '/'; + /* 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; + } } - pp = sp + 1; + *tail = lastch; } - if (*pp != '\0' && !failed) { - if (p_mkdir(make_path.ptr, mode) < 0 && errno != EEXIST) - failed = true; - } + git_buf_free(&make_path); + return 0; +fail: git_buf_free(&make_path); + return error; +} - if (failed) { - giterr_set(GITERR_OS, - "Failed to create directory structure at '%s'", path); - return -1; +int git_futils_mkdir_r(const char *path, const char *base, const mode_t mode) +{ + return git_futils_mkdir(path, base, mode, GIT_MKDIR_PATH); +} + +typedef struct { + const char *base; + size_t baselen; + uint32_t flags; + int error; +} futils__rmdir_data; + +static int futils__error_cannot_rmdir(const char *path, const char *filemsg) +{ + if (filemsg) + giterr_set(GITERR_OS, "Could not remove directory. File '%s' %s", + path, filemsg); + else + giterr_set(GITERR_OS, "Could not remove directory '%s'", path); + + return -1; +} + +static int futils__rm_first_parent(git_buf *path, const char *ceiling) +{ + int error = GIT_ENOTFOUND; + struct stat st; + + while (error == GIT_ENOTFOUND) { + git_buf_rtruncate_at_char(path, '/'); + + if (!path->size || git__prefixcmp(path->ptr, ceiling) != 0) + error = 0; + else if (p_lstat_posixly(path->ptr, &st) == 0) { + if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) + error = p_unlink(path->ptr); + else if (!S_ISDIR(st.st_mode)) + error = -1; /* fail to remove non-regular file */ + } else if (errno != ENOTDIR) + error = -1; } - return 0; + if (error) + futils__error_cannot_rmdir(path->ptr, "cannot remove parent"); + + return error; } -static int _rmdir_recurs_foreach(void *opaque, git_buf *path) +static int futils__rmdir_recurs_foreach(void *opaque, git_buf *path) { - git_directory_removal_type removal_type = *(git_directory_removal_type *)opaque; + struct stat st; + futils__rmdir_data *data = opaque; - assert(removal_type == GIT_DIRREMOVAL_EMPTY_HIERARCHY - || removal_type == GIT_DIRREMOVAL_FILES_AND_DIRS - || removal_type == GIT_DIRREMOVAL_ONLY_EMPTY_DIRS); + if ((data->error = p_lstat_posixly(path->ptr, &st)) < 0) { + if (errno == ENOENT) + data->error = 0; + else if (errno == ENOTDIR) { + /* asked to remove a/b/c/d/e and a/b is a normal file */ + if ((data->flags & GIT_RMDIR_REMOVE_BLOCKERS) != 0) + data->error = futils__rm_first_parent(path, data->base); + else + futils__error_cannot_rmdir( + path->ptr, "parent is not directory"); + } + else + futils__error_cannot_rmdir(path->ptr, "cannot access"); + } - if (git_path_isdir(path->ptr) == true) { - if (git_path_direach(path, _rmdir_recurs_foreach, opaque) < 0) - return -1; + else if (S_ISDIR(st.st_mode)) { + int error = git_path_direach(path, futils__rmdir_recurs_foreach, data); + if (error < 0) + return (error == GIT_EUSER) ? data->error : error; - if (p_rmdir(path->ptr) < 0) { - if (removal_type == GIT_DIRREMOVAL_ONLY_EMPTY_DIRS && (errno == ENOTEMPTY || errno == EEXIST)) - return 0; + data->error = p_rmdir(path->ptr); - giterr_set(GITERR_OS, "Could not remove directory '%s'", path->ptr); - return -1; + if (data->error < 0) { + if ((data->flags & GIT_RMDIR_SKIP_NONEMPTY) != 0 && + (errno == ENOTEMPTY || errno == EEXIST)) + data->error = 0; + else + futils__error_cannot_rmdir(path->ptr, NULL); } + } - return 0; + else if ((data->flags & GIT_RMDIR_REMOVE_FILES) != 0) { + data->error = p_unlink(path->ptr); + + if (data->error < 0) + futils__error_cannot_rmdir(path->ptr, "cannot be removed"); } - if (removal_type == GIT_DIRREMOVAL_FILES_AND_DIRS) { - if (p_unlink(path->ptr) < 0) { - giterr_set(GITERR_OS, "Could not remove directory. File '%s' cannot be removed", path->ptr); - return -1; - } + else if ((data->flags & GIT_RMDIR_SKIP_NONEMPTY) == 0) + data->error = futils__error_cannot_rmdir(path->ptr, "still present"); - return 0; + return data->error; +} + +static int futils__rmdir_empty_parent(void *opaque, git_buf *path) +{ + futils__rmdir_data *data = opaque; + int error; + + if (git_buf_len(path) <= data->baselen) + return GIT_ITEROVER; + + error = p_rmdir(git_buf_cstr(path)); + + if (error) { + int en = errno; + + if (en == ENOENT || en == ENOTDIR) { + giterr_clear(); + error = 0; + } else if (en == ENOTEMPTY || en == EEXIST) { + giterr_clear(); + error = GIT_ITEROVER; + } else { + futils__error_cannot_rmdir(git_buf_cstr(path), NULL); + } } - if (removal_type == GIT_DIRREMOVAL_EMPTY_HIERARCHY) { - giterr_set(GITERR_OS, "Could not remove directory. File '%s' still present", path->ptr); + return error; +} + +int git_futils_rmdir_r( + const char *path, const char *base, uint32_t flags) +{ + int error; + git_buf fullpath = GIT_BUF_INIT; + futils__rmdir_data data; + + /* build path and find "root" where we should start calling mkdir */ + if (git_path_join_unrooted(&fullpath, path, base, NULL) < 0) return -1; + + data.base = base ? base : ""; + data.baselen = base ? strlen(base) : 0; + data.flags = flags; + data.error = 0; + + error = futils__rmdir_recurs_foreach(&data, &fullpath); + + /* remove now-empty parents if requested */ + if (!error && (flags & GIT_RMDIR_EMPTY_PARENTS) != 0) { + error = git_path_walk_up( + &fullpath, base, futils__rmdir_empty_parent, &data); + + if (error == GIT_ITEROVER) + error = 0; } - return 0; + git_buf_free(&fullpath); + + return error; } -int git_futils_rmdir_r(const char *path, git_directory_removal_type removal_type) +int git_futils_cleanupdir_r(const char *path) { int error; - git_buf p = GIT_BUF_INIT; + git_buf fullpath = GIT_BUF_INIT; + futils__rmdir_data data; - error = git_buf_sets(&p, path); - if (!error) - error = _rmdir_recurs_foreach(&removal_type, &p); - git_buf_free(&p); + if ((error = git_buf_put(&fullpath, path, strlen(path))) < 0) + goto clean_up; + + data.base = ""; + data.baselen = 0; + data.flags = GIT_RMDIR_REMOVE_FILES; + data.error = 0; + + if (!git_path_exists(path)) { + giterr_set(GITERR_OS, "Path does not exist: %s" , path); + error = GIT_ERROR; + goto clean_up; + } + + if (!git_path_isdir(path)) { + giterr_set(GITERR_OS, "Path is not a directory: %s" , path); + error = GIT_ERROR; + goto clean_up; + } + + error = git_path_direach(&fullpath, futils__rmdir_recurs_foreach, &data); + if (error == GIT_EUSER) + error = data.error; + +clean_up: + git_buf_free(&fullpath); return error; } -int git_futils_find_global_file(git_buf *path, const char *filename) + +static int git_futils_guess_system_dirs(git_buf *out) { - const char *home = getenv("HOME"); +#ifdef GIT_WIN32 + return git_win32__find_system_dirs(out); +#else + return git_buf_sets(out, "/etc"); +#endif +} +static int git_futils_guess_global_dirs(git_buf *out) +{ #ifdef GIT_WIN32 - if (home == NULL) - home = getenv("USERPROFILE"); + return git_win32__find_global_dirs(out); +#else + return git_buf_sets(out, getenv("HOME")); #endif +} - if (home == NULL) { - giterr_set(GITERR_OS, "Global file lookup failed. " - "Cannot locate the user's home directory"); - return -1; - } +static int git_futils_guess_xdg_dirs(git_buf *out) +{ +#ifdef GIT_WIN32 + return git_win32__find_xdg_dirs(out); +#else + const char *env = NULL; - if (git_buf_joinpath(path, home, filename) < 0) - return -1; + if ((env = getenv("XDG_CONFIG_HOME")) != NULL) + return git_buf_joinpath(out, env, "git"); + else if ((env = getenv("HOME")) != NULL) + return git_buf_joinpath(out, env, ".config/git"); - if (git_path_exists(path->ptr) == false) { - git_buf_clear(path); - return GIT_ENOTFOUND; + git_buf_clear(out); + return 0; +#endif +} + +typedef int (*git_futils_dirs_guess_cb)(git_buf *out); + +static git_buf git_futils__dirs[GIT_FUTILS_DIR__MAX] = + { GIT_BUF_INIT, GIT_BUF_INIT, GIT_BUF_INIT }; + +static git_futils_dirs_guess_cb git_futils__dir_guess[GIT_FUTILS_DIR__MAX] = { + git_futils_guess_system_dirs, + git_futils_guess_global_dirs, + git_futils_guess_xdg_dirs, +}; + +static int git_futils_check_selector(git_futils_dir_t which) +{ + if (which < GIT_FUTILS_DIR__MAX) + return 0; + giterr_set(GITERR_INVALID, "config directory selector out of range"); + return -1; +} + +int git_futils_dirs_get(const git_buf **out, git_futils_dir_t which) +{ + assert(out); + + *out = NULL; + + GITERR_CHECK_ERROR(git_futils_check_selector(which)); + + if (!git_buf_len(&git_futils__dirs[which])) + GITERR_CHECK_ERROR( + git_futils__dir_guess[which](&git_futils__dirs[which])); + + *out = &git_futils__dirs[which]; + return 0; +} + +int git_futils_dirs_get_str(char *out, size_t outlen, git_futils_dir_t which) +{ + const git_buf *path = NULL; + + GITERR_CHECK_ERROR(git_futils_check_selector(which)); + GITERR_CHECK_ERROR(git_futils_dirs_get(&path, which)); + + if (!out || path->size >= outlen) { + giterr_set(GITERR_NOMEMORY, "Buffer is too short for the path"); + return GIT_EBUFS; } + git_buf_copy_cstr(out, outlen, path); return 0; } -#ifdef GIT_WIN32 -typedef struct { - wchar_t *path; - DWORD len; -} win32_path; +#define PATH_MAGIC "$PATH" -static const win32_path *win32_system_root(void) +int git_futils_dirs_set(git_futils_dir_t which, const char *search_path) { - static win32_path s_root = { 0, 0 }; + const char *expand_path = NULL; + git_buf merge = GIT_BUF_INIT; - if (s_root.path == NULL) { - const wchar_t *root_tmpl = L"%PROGRAMFILES%\\Git\\etc\\"; + GITERR_CHECK_ERROR(git_futils_check_selector(which)); - s_root.len = ExpandEnvironmentStringsW(root_tmpl, NULL, 0); - if (s_root.len <= 0) { - giterr_set(GITERR_OS, "Failed to expand environment strings"); - return NULL; - } + if (search_path != NULL) + expand_path = strstr(search_path, PATH_MAGIC); - s_root.path = git__calloc(s_root.len, sizeof(wchar_t)); - if (s_root.path == NULL) - return NULL; + /* init with default if not yet done and needed (ignoring error) */ + if ((!search_path || expand_path) && + !git_buf_len(&git_futils__dirs[which])) + git_futils__dir_guess[which](&git_futils__dirs[which]); - if (ExpandEnvironmentStringsW(root_tmpl, s_root.path, s_root.len) != s_root.len) { - giterr_set(GITERR_OS, "Failed to expand environment strings"); - git__free(s_root.path); - s_root.path = NULL; - return NULL; - } - } + /* if $PATH is not referenced, then just set the path */ + if (!expand_path) + return git_buf_sets(&git_futils__dirs[which], search_path); + + /* otherwise set to join(before $PATH, old value, after $PATH) */ + if (expand_path > search_path) + git_buf_set(&merge, search_path, expand_path - search_path); + + if (git_buf_len(&git_futils__dirs[which])) + git_buf_join(&merge, GIT_PATH_LIST_SEPARATOR, + merge.ptr, git_futils__dirs[which].ptr); + + expand_path += strlen(PATH_MAGIC); + if (*expand_path) + git_buf_join(&merge, GIT_PATH_LIST_SEPARATOR, merge.ptr, expand_path); + + git_buf_swap(&git_futils__dirs[which], &merge); + git_buf_free(&merge); - return &s_root; + return git_buf_oom(&git_futils__dirs[which]) ? -1 : 0; } -static int win32_find_system_file(git_buf *path, const char *filename) +void git_futils_dirs_free(void) +{ + int i; + for (i = 0; i < GIT_FUTILS_DIR__MAX; ++i) + git_buf_free(&git_futils__dirs[i]); +} + +static int git_futils_find_in_dirlist( + git_buf *path, const char *name, git_futils_dir_t which, const char *label) { - int error = 0; - const win32_path *root = win32_system_root(); size_t len; - wchar_t *file_utf16 = NULL, *scan; - char *file_utf8 = NULL; + const char *scan, *next = NULL; + const git_buf *syspath; - if (!root || !filename || (len = strlen(filename)) == 0) - return GIT_ENOTFOUND; + GITERR_CHECK_ERROR(git_futils_dirs_get(&syspath, which)); - /* allocate space for wchar_t path to file */ - file_utf16 = git__calloc(root->len + len + 2, sizeof(wchar_t)); - GITERR_CHECK_ALLOC(file_utf16); + for (scan = git_buf_cstr(syspath); scan; scan = next) { + for (next = strchr(scan, GIT_PATH_LIST_SEPARATOR); + next && next > scan && next[-1] == '\\'; + next = strchr(next + 1, GIT_PATH_LIST_SEPARATOR)) + /* find unescaped separator or end of string */; - /* append root + '\\' + filename as wchar_t */ - memcpy(file_utf16, root->path, root->len * sizeof(wchar_t)); + len = next ? (size_t)(next++ - scan) : strlen(scan); + if (!len) + continue; - if (*filename == '/' || *filename == '\\') - filename++; + GITERR_CHECK_ERROR(git_buf_set(path, scan, len)); + GITERR_CHECK_ERROR(git_buf_joinpath(path, path->ptr, name)); - if (gitwin_append_utf16(file_utf16 + root->len - 1, filename, len + 1) != - (int)len + 1) { - error = -1; - goto cleanup; + if (git_path_exists(path->ptr)) + return 0; } - for (scan = file_utf16; *scan; scan++) - if (*scan == L'/') - *scan = L'\\'; + git_buf_clear(path); + giterr_set(GITERR_OS, "The %s file '%s' doesn't exist", label, name); + return GIT_ENOTFOUND; +} + +int git_futils_find_system_file(git_buf *path, const char *filename) +{ + return git_futils_find_in_dirlist( + path, filename, GIT_FUTILS_DIR_SYSTEM, "system"); +} + +int git_futils_find_global_file(git_buf *path, const char *filename) +{ + return git_futils_find_in_dirlist( + path, filename, GIT_FUTILS_DIR_GLOBAL, "global"); +} - /* check access */ - if (_waccess(file_utf16, F_OK) < 0) { - error = GIT_ENOTFOUND; - goto cleanup; +int git_futils_find_xdg_file(git_buf *path, const char *filename) +{ + return git_futils_find_in_dirlist( + path, filename, GIT_FUTILS_DIR_XDG, "global/xdg"); +} + +int git_futils_fake_symlink(const char *old, const char *new) +{ + int retcode = GIT_ERROR; + int fd = git_futils_creat_withpath(new, 0755, 0644); + if (fd >= 0) { + retcode = p_write(fd, old, strlen(old)); + p_close(fd); + } + return retcode; +} + +static int cp_by_fd(int ifd, int ofd, bool close_fd_when_done) +{ + int error = 0; + char buffer[4096]; + ssize_t len = 0; + + while (!error && (len = p_read(ifd, buffer, sizeof(buffer))) > 0) + /* p_write() does not have the same semantics as write(). It loops + * internally and will return 0 when it has completed writing. + */ + error = p_write(ofd, buffer, len); + + if (len < 0) { + giterr_set(GITERR_OS, "Read error while copying file"); + error = (int)len; + } + + if (close_fd_when_done) { + p_close(ifd); + p_close(ofd); + } + + return error; +} + +int git_futils_cp(const char *from, const char *to, mode_t filemode) +{ + int ifd, ofd; + + if ((ifd = git_futils_open_ro(from)) < 0) + return ifd; + + if ((ofd = p_open(to, O_WRONLY | O_CREAT | O_EXCL, filemode)) < 0) { + if (errno == ENOENT || errno == ENOTDIR) + ofd = GIT_ENOTFOUND; + giterr_set(GITERR_OS, "Failed to open '%s' for writing", to); + p_close(ifd); + return ofd; } - /* convert to utf8 */ - if ((file_utf8 = gitwin_from_utf16(file_utf16)) == NULL) + return cp_by_fd(ifd, ofd, true); +} + +static int cp_link(const char *from, const char *to, size_t link_size) +{ + int error = 0; + ssize_t read_len; + char *link_data = git__malloc(link_size + 1); + GITERR_CHECK_ALLOC(link_data); + + read_len = p_readlink(from, link_data, link_size); + if (read_len != (ssize_t)link_size) { + giterr_set(GITERR_OS, "Failed to read symlink data for '%s'", from); error = -1; + } else { - git_path_mkposix(file_utf8); - git_buf_attach(path, file_utf8, 0); + link_data[read_len] = '\0'; + + if (p_symlink(link_data, to) < 0) { + giterr_set(GITERR_OS, "Could not symlink '%s' as '%s'", + link_data, to); + error = -1; + } + } + + git__free(link_data); + return error; +} + +typedef struct { + const char *to_root; + git_buf to; + ssize_t from_prefix; + uint32_t flags; + uint32_t mkdir_flags; + mode_t dirmode; +} cp_r_info; + +#define GIT_CPDIR__MKDIR_DONE_FOR_TO_ROOT (1u << 10) + +static int _cp_r_mkdir(cp_r_info *info, git_buf *from) +{ + int error = 0; + + /* create root directory the first time we need to create a directory */ + if ((info->flags & GIT_CPDIR__MKDIR_DONE_FOR_TO_ROOT) == 0) { + error = git_futils_mkdir( + info->to_root, NULL, info->dirmode, + (info->flags & GIT_CPDIR_CHMOD_DIRS) ? GIT_MKDIR_CHMOD : 0); + + info->flags |= GIT_CPDIR__MKDIR_DONE_FOR_TO_ROOT; } -cleanup: - git__free(file_utf16); + /* create directory with root as base to prevent excess chmods */ + if (!error) + error = git_futils_mkdir( + from->ptr + info->from_prefix, info->to_root, + info->dirmode, info->mkdir_flags); return error; } -#endif -int git_futils_find_system_file(git_buf *path, const char *filename) +static int _cp_r_callback(void *ref, git_buf *from) { - if (git_buf_joinpath(path, "/etc", filename) < 0) + int error = 0; + cp_r_info *info = ref; + struct stat from_st, to_st; + bool exists = false; + + if ((info->flags & GIT_CPDIR_COPY_DOTFILES) == 0 && + from->ptr[git_path_basename_offset(from)] == '.') + return 0; + + if (git_buf_joinpath( + &info->to, info->to_root, from->ptr + info->from_prefix) < 0) return -1; - if (git_path_exists(path->ptr) == true) + if (p_lstat(info->to.ptr, &to_st) < 0) { + if (errno != ENOENT && errno != ENOTDIR) { + giterr_set(GITERR_OS, + "Could not access %s while copying files", info->to.ptr); + return -1; + } + } else + exists = true; + + if ((error = git_path_lstat(from->ptr, &from_st)) < 0) + return error; + + if (S_ISDIR(from_st.st_mode)) { + mode_t oldmode = info->dirmode; + + /* if we are not chmod'ing, then overwrite dirmode */ + if ((info->flags & GIT_CPDIR_CHMOD_DIRS) == 0) + info->dirmode = from_st.st_mode; + + /* make directory now if CREATE_EMPTY_DIRS is requested and needed */ + if (!exists && (info->flags & GIT_CPDIR_CREATE_EMPTY_DIRS) != 0) + error = _cp_r_mkdir(info, from); + + /* recurse onto target directory */ + if (!error && (!exists || S_ISDIR(to_st.st_mode))) + error = git_path_direach(from, _cp_r_callback, info); + + if (oldmode != 0) + info->dirmode = oldmode; + + return error; + } + + if (exists) { + if ((info->flags & GIT_CPDIR_OVERWRITE) == 0) + return 0; + + if (p_unlink(info->to.ptr) < 0) { + giterr_set(GITERR_OS, "Cannot overwrite existing file '%s'", + info->to.ptr); + return -1; + } + } + + /* Done if this isn't a regular file or a symlink */ + if (!S_ISREG(from_st.st_mode) && + (!S_ISLNK(from_st.st_mode) || + (info->flags & GIT_CPDIR_COPY_SYMLINKS) == 0)) return 0; - git_buf_clear(path); + /* Make container directory on demand if needed */ + if ((info->flags & GIT_CPDIR_CREATE_EMPTY_DIRS) == 0 && + (error = _cp_r_mkdir(info, from)) < 0) + return error; -#ifdef GIT_WIN32 - return win32_find_system_file(path, filename); -#else - return GIT_ENOTFOUND; -#endif + /* make symlink or regular file */ + if (S_ISLNK(from_st.st_mode)) + error = cp_link(from->ptr, info->to.ptr, (size_t)from_st.st_size); + else { + mode_t usemode = from_st.st_mode; + + if ((info->flags & GIT_CPDIR_SIMPLE_TO_MODE) != 0) + usemode = (usemode & 0111) ? 0777 : 0666; + + error = git_futils_cp(from->ptr, info->to.ptr, usemode); + } + + return error; +} + +int git_futils_cp_r( + const char *from, + const char *to, + uint32_t flags, + mode_t dirmode) +{ + int error; + git_buf path = GIT_BUF_INIT; + cp_r_info info; + + if (git_buf_joinpath(&path, from, "") < 0) /* ensure trailing slash */ + return -1; + + info.to_root = to; + info.flags = flags; + info.dirmode = dirmode; + info.from_prefix = path.size; + git_buf_init(&info.to, 0); + + /* precalculate mkdir flags */ + if ((flags & GIT_CPDIR_CREATE_EMPTY_DIRS) == 0) { + /* if not creating empty dirs, then use mkdir to create the path on + * demand right before files are copied. + */ + info.mkdir_flags = GIT_MKDIR_PATH | GIT_MKDIR_SKIP_LAST; + if ((flags & GIT_CPDIR_CHMOD_DIRS) != 0) + info.mkdir_flags |= GIT_MKDIR_CHMOD_PATH; + } else { + /* otherwise, we will do simple mkdir as directories are encountered */ + info.mkdir_flags = + ((flags & GIT_CPDIR_CHMOD_DIRS) != 0) ? GIT_MKDIR_CHMOD : 0; + } + + error = _cp_r_callback(&info, &path); + + git_buf_free(&path); + git_buf_free(&info.to); + + return error; +} + +int git_futils_filestamp_check( + git_futils_filestamp *stamp, const char *path) +{ + struct stat st; + + /* if the stamp is NULL, then always reload */ + if (stamp == NULL) + return 1; + + if (p_stat(path, &st) < 0) + return GIT_ENOTFOUND; + + if (stamp->mtime == (git_time_t)st.st_mtime && + stamp->size == (git_off_t)st.st_size && + stamp->ino == (unsigned int)st.st_ino) + return 0; + + stamp->mtime = (git_time_t)st.st_mtime; + stamp->size = (git_off_t)st.st_size; + stamp->ino = (unsigned int)st.st_ino; + + return 1; +} + +void git_futils_filestamp_set( + git_futils_filestamp *target, const git_futils_filestamp *source) +{ + assert(target); + + if (source) + memcpy(target, source, sizeof(*target)); + else + memset(target, 0, sizeof(*target)); } diff --git a/src/fileops.h b/src/fileops.h index be619d620..627a6923d 100644 --- a/src/fileops.h +++ b/src/fileops.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -18,7 +18,9 @@ * Read whole files into an in-memory buffer for processing */ extern int git_futils_readbuffer(git_buf *obj, const char *path); -extern int git_futils_readbuffer_updated(git_buf *obj, const char *path, time_t *mtime, int *updated); +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); /** * File utils @@ -49,34 +51,99 @@ extern int git_futils_creat_locked_withpath(const char *path, const mode_t dirmo /** * Create a path recursively + * + * If a base parameter is being passed, it's expected to be valued with a + * path pointing to an already existing directory. */ extern int git_futils_mkdir_r(const char *path, const char *base, const mode_t mode); /** + * Flags to pass to `git_futils_mkdir`. + * + * * GIT_MKDIR_EXCL is "exclusive" - i.e. generate an error if dir exists. + * * GIT_MKDIR_PATH says to make all components in the path. + * * GIT_MKDIR_CHMOD says to chmod the final directory entry after creation + * * GIT_MKDIR_CHMOD_PATH says to chmod each directory component in the path + * * GIT_MKDIR_SKIP_LAST says to leave off the last element of the path + * * GIT_MKDIR_SKIP_LAST2 says to leave off the last 2 elements of the path + * * GIT_MKDIR_VERIFY_DIR says confirm final item is a dir, not just EEXIST + * + * Note that the chmod options will be executed even if the directory already + * exists, unless GIT_MKDIR_EXCL is given. + */ +typedef enum { + GIT_MKDIR_EXCL = 1, + GIT_MKDIR_PATH = 2, + GIT_MKDIR_CHMOD = 4, + GIT_MKDIR_CHMOD_PATH = 8, + GIT_MKDIR_SKIP_LAST = 16, + GIT_MKDIR_SKIP_LAST2 = 32, + GIT_MKDIR_VERIFY_DIR = 64, +} git_futils_mkdir_flags; + +/** + * Create a directory or entire path. + * + * This makes a directory (and the entire path leading up to it if requested), + * and optionally chmods the directory immediately after (or each part of the + * path if requested). + * + * @param path The path to create. + * @param base Root for relative path. These directories will never be made. + * @param mode The mode to use for created directories. + * @param flags Combination of the mkdir flags above. + * @return 0 on success, else error code + */ +extern int git_futils_mkdir(const char *path, const char *base, mode_t mode, uint32_t flags); + +/** * Create all the folders required to contain * the full path of a file */ extern int git_futils_mkpath2file(const char *path, const mode_t mode); +/** + * Flags to pass to `git_futils_rmdir_r`. + * + * * GIT_RMDIR_EMPTY_HIERARCHY - the default; remove hierarchy of empty + * dirs and generate error if any files are found. + * * GIT_RMDIR_REMOVE_FILES - attempt to remove files in the hierarchy. + * * GIT_RMDIR_SKIP_NONEMPTY - skip non-empty directories with no error. + * * GIT_RMDIR_EMPTY_PARENTS - remove containing directories up to base + * if removing this item leaves them empty + * * GIT_RMDIR_REMOVE_BLOCKERS - remove blocking file that causes ENOTDIR + * + * The old values translate into the new as follows: + * + * * GIT_DIRREMOVAL_EMPTY_HIERARCHY == GIT_RMDIR_EMPTY_HIERARCHY + * * GIT_DIRREMOVAL_FILES_AND_DIRS ~= GIT_RMDIR_REMOVE_FILES + * * GIT_DIRREMOVAL_ONLY_EMPTY_DIRS == GIT_RMDIR_SKIP_NONEMPTY + */ typedef enum { - GIT_DIRREMOVAL_EMPTY_HIERARCHY = 0, - GIT_DIRREMOVAL_FILES_AND_DIRS = 1, - GIT_DIRREMOVAL_ONLY_EMPTY_DIRS = 2, -} git_directory_removal_type; + GIT_RMDIR_EMPTY_HIERARCHY = 0, + GIT_RMDIR_REMOVE_FILES = (1 << 0), + GIT_RMDIR_SKIP_NONEMPTY = (1 << 1), + GIT_RMDIR_EMPTY_PARENTS = (1 << 2), + GIT_RMDIR_REMOVE_BLOCKERS = (1 << 3), +} git_futils_rmdir_flags; /** * Remove path and any files and directories beneath it. * - * @param path Path to to top level directory to process. - * - * @param removal_type GIT_DIRREMOVAL_EMPTY_HIERARCHY to remove a hierarchy - * of empty directories (will fail if any file is found), GIT_DIRREMOVAL_FILES_AND_DIRS - * to remove a hierarchy of files and folders, GIT_DIRREMOVAL_ONLY_EMPTY_DIRS to only remove - * empty directories (no failure on file encounter). + * @param path Path to the top level directory to process. + * @param base Root for relative path. + * @param flags Combination of git_futils_rmdir_flags values + * @return 0 on success; -1 on error. + */ +extern int git_futils_rmdir_r(const char *path, const char *base, uint32_t flags); + +/** + * Remove all files and directories beneath the specified path. * + * @param path Path to the top level directory to process. * @return 0 on success; -1 on error. */ -extern int git_futils_rmdir_r(const char *path, git_directory_removal_type removal_type); +extern int git_futils_cleanupdir_r(const char *path); /** * Create and open a temporary file with a `_git2_` suffix. @@ -92,6 +159,58 @@ extern int git_futils_mktmp(git_buf *path_out, const char *filename); extern int git_futils_mv_withpath(const char *from, const char *to, const mode_t dirmode); /** + * Copy a file + * + * The filemode will be used for the newly created file. + */ +extern int git_futils_cp( + const char *from, + const char *to, + mode_t filemode); + +/** + * Flags that can be passed to `git_futils_cp_r`. + * + * - GIT_CPDIR_CREATE_EMPTY_DIRS: create directories even if there are no + * files under them (otherwise directories will only be created lazily + * when a file inside them is copied). + * - GIT_CPDIR_COPY_SYMLINKS: copy symlinks, otherwise they are ignored. + * - GIT_CPDIR_COPY_DOTFILES: copy files with leading '.', otherwise ignored. + * - GIT_CPDIR_OVERWRITE: overwrite pre-existing files with source content, + * otherwise they are silently skipped. + * - GIT_CPDIR_CHMOD_DIRS: explicitly chmod directories to `dirmode` + * - GIT_CPDIR_SIMPLE_TO_MODE: default tries to replicate the mode of the + * source file to the target; with this flag, always use 0666 (or 0777 if + * source has exec bits set) for target. + */ +typedef enum { + GIT_CPDIR_CREATE_EMPTY_DIRS = (1u << 0), + GIT_CPDIR_COPY_SYMLINKS = (1u << 1), + GIT_CPDIR_COPY_DOTFILES = (1u << 2), + GIT_CPDIR_OVERWRITE = (1u << 3), + GIT_CPDIR_CHMOD_DIRS = (1u << 4), + GIT_CPDIR_SIMPLE_TO_MODE = (1u << 5), +} git_futils_cpdir_flags; + +/** + * Copy a directory tree. + * + * This copies directories and files from one root to another. You can + * pass a combinationof GIT_CPDIR flags as defined above. + * + * If you pass the CHMOD flag, then the dirmode will be applied to all + * directories that are created during the copy, overiding the natural + * permissions. If you do not pass the CHMOD flag, then the dirmode + * will actually be copied from the source files and the `dirmode` arg + * will be ignored. + */ +extern int git_futils_cp_r( + const char *from, + const char *to, + uint32_t flags, + mode_t dirmode); + +/** * Open a file readonly and set error if needed. */ extern int git_futils_open_ro(const char *path); @@ -157,23 +276,120 @@ extern void git_futils_mmap_free(git_map *map); * * @param pathbuf 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; - * - -1 on an unspecified OS related error. + * @return 0 if found, GIT_ENOTFOUND if not found, or -1 on other OS error */ 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 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 + */ +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 filename name of file to find in the home directory - * @return - * - 0 if found; - * - GIT_ENOTFOUND if not found; - * - -1 on an unspecified OS related error. + * @return 0 if found, GIT_ENOTFOUND if not found, or -1 on other OS error */ extern int git_futils_find_system_file(git_buf *path, const char *filename); +typedef enum { + GIT_FUTILS_DIR_SYSTEM = 0, + GIT_FUTILS_DIR_GLOBAL = 1, + GIT_FUTILS_DIR_XDG = 2, + GIT_FUTILS_DIR__MAX = 3, +} git_futils_dir_t; + +/** + * Get the search path for global/system/xdg files + * + * @param out pointer to git_buf containing search path + * @param which which list of paths to return + * @return 0 on success, <0 on failure + */ +extern int git_futils_dirs_get(const git_buf **out, git_futils_dir_t which); + +/** + * Get search path into a preallocated buffer + * + * @param out String buffer to write into + * @param outlen Size of string buffer + * @param which Which search path to return + * @return 0 on success, GIT_EBUFS if out is too small, <0 on other failure + */ + +extern int git_futils_dirs_get_str( + char *out, size_t outlen, git_futils_dir_t which); + +/** + * Set search paths for global/system/xdg files + * + * The first occurrence of the magic string "$PATH" in the new value will + * be replaced with the old value of the search path. + * + * @param which Which search path to modify + * @param paths New search path (separated by GIT_PATH_LIST_SEPARATOR) + * @return 0 on success, <0 on failure (allocation error) + */ +extern int git_futils_dirs_set(git_futils_dir_t which, const char *paths); + +/** + * Release / reset all search paths + */ +extern void git_futils_dirs_free(void); + +/** + * Create a "fake" symlink (text file containing the target path). + * + * @param new symlink file to be created + * @param old original symlink target + * @return 0 on success, -1 on error + */ +extern int git_futils_fake_symlink(const char *new, const char *old); + +/** + * A file stamp represents a snapshot of information about a file that can + * be used to test if the file changes. This portable implementation is + * based on stat data about that file, but it is possible that OS specific + * versions could be implemented in the future. + */ +typedef struct { + git_time_t mtime; + git_off_t size; + unsigned int ino; +} git_futils_filestamp; + +/** + * Compare stat information for file with reference info. + * + * This function updates the file stamp to current data for the given path + * and returns 0 if the file is up-to-date relative to the prior setting or + * 1 if the file has been changed. (This also may return GIT_ENOTFOUND if + * the file doesn't exist.) + * + * @param stamp File stamp to be checked + * @param path Path to stat and check if changed + * @return 0 if up-to-date, 1 if out-of-date, <0 on error + */ +extern int git_futils_filestamp_check( + git_futils_filestamp *stamp, const char *path); + +/** + * Set or reset file stamp data + * + * This writes the target file stamp. If the source is NULL, this will set + * the target stamp to values that will definitely be out of date. If the + * source is not NULL, this copies the source values to the target. + * + * @param tgt File stamp to write to + * @param src File stamp to copy from or NULL to clear the target + */ +extern void git_futils_filestamp_set( + git_futils_filestamp *tgt, const git_futils_filestamp *src); + #endif /* INCLUDE_fileops_h__ */ diff --git a/src/filter.c b/src/filter.c index 8fa3eb684..9f749dcbd 100644 --- a/src/filter.c +++ b/src/filter.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -11,79 +11,7 @@ #include "filter.h" #include "repository.h" #include "git2/config.h" - -/* Tweaked from Core Git. I wonder what we could use this for... */ -void git_text_gather_stats(git_text_stats *stats, const git_buf *text) -{ - size_t i; - - memset(stats, 0, sizeof(*stats)); - - for (i = 0; i < git_buf_len(text); i++) { - unsigned char c = text->ptr[i]; - - if (c == '\r') { - stats->cr++; - - if (i + 1 < git_buf_len(text) && text->ptr[i + 1] == '\n') - stats->crlf++; - } - - else if (c == '\n') - stats->lf++; - - else if (c == 0x85) - /* Unicode CR+LF */ - stats->crlf++; - - else if (c == 127) - /* DEL */ - stats->nonprintable++; - - else if (c <= 0x1F || (c >= 0x80 && c <= 0x9F)) { - switch (c) { - /* BS, HT, ESC and FF */ - case '\b': case '\t': case '\033': case '\014': - stats->printable++; - break; - case 0: - stats->nul++; - /* fall through */ - default: - stats->nonprintable++; - } - } - - else - stats->printable++; - } - - /* If file ends with EOF then don't count this EOF as non-printable. */ - if (git_buf_len(text) >= 1 && text->ptr[text->size - 1] == '\032') - stats->nonprintable--; -} - -/* - * Fresh from Core Git - */ -int git_text_is_binary(git_text_stats *stats) -{ - if (stats->nul) - return 1; - - if ((stats->printable >> 7) < stats->nonprintable) - return 1; - /* - * Other heuristics? Average line length might be relevant, - * as might LF vs CR vs CRLF counts.. - * - * NOTE! It might be normal to have a low ratio of CRLF to LF - * (somebody starts with a LF-only file and edits it with an editor - * that adds CRLF only to lines that are added..). But do we - * want to support CR-only? Probably not. - */ - return 0; -} +#include "blob.h" int git_filters_load(git_vector *filters, git_repository *repo, const char *path, int mode) { @@ -95,8 +23,9 @@ int git_filters_load(git_vector *filters, git_repository *repo, const char *path if (error < 0) return error; } else { - giterr_set(GITERR_INVALID, "Worktree filters are not implemented yet"); - return -1; + error = git_filter_add__crlf_to_workdir(filters, repo, path); + if (error < 0) + return error; } return (int)filters->length; @@ -119,7 +48,8 @@ void git_filters_free(git_vector *filters) int git_filters_apply(git_buf *dest, git_buf *source, git_vector *filters) { - unsigned int i, src; + size_t i; + unsigned int src; git_buf *dbuffer[2]; dbuffer[0] = source; @@ -162,4 +92,3 @@ int git_filters_apply(git_buf *dest, git_buf *source, git_vector *filters) return 0; } - diff --git a/src/filter.h b/src/filter.h index 66e370aef..42a44ebdb 100644 --- a/src/filter.h +++ b/src/filter.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -31,14 +31,6 @@ typedef enum { GIT_CRLF_AUTO, } git_crlf_t; -typedef struct { - /* NUL, CR, LF and CRLF counts */ - unsigned int nul, cr, lf, crlf; - - /* These are just approximations! */ - unsigned int printable, nonprintable; -} git_text_stats; - /* * FILTER API */ @@ -96,24 +88,7 @@ extern void git_filters_free(git_vector *filters); /* Strip CRLF, from Worktree to ODB */ extern int git_filter_add__crlf_to_odb(git_vector *filters, git_repository *repo, const char *path); - -/* - * PLAINTEXT API - */ - -/* - * Gather stats for a piece of text - * - * Fill the `stats` structure with information on the number of - * unreadable characters, carriage returns, etc, so it can be - * used in heuristics. - */ -extern void git_text_gather_stats(git_text_stats *stats, const git_buf *text); - -/* - * Process `git_text_stats` data generated by `git_text_stat` to see - * if it qualifies as a binary file - */ -extern int git_text_is_binary(git_text_stats *stats); +/* Add CRLF, from ODB to worktree */ +extern int git_filter_add__crlf_to_workdir(git_vector *filters, git_repository *repo, const char *path); #endif diff --git a/src/compat/fnmatch.c b/src/fnmatch.c index 835d811bc..e3e47f37b 100644 --- a/src/compat/fnmatch.c +++ b/src/fnmatch.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -24,13 +24,16 @@ static int rangematch(const char *, char, int, char **); -int -p_fnmatch(const char *pattern, const char *string, int flags) +static int +p_fnmatchx(const char *pattern, const char *string, int flags, size_t recurs) { const char *stringstart; char *newp; char c, test; + if (recurs-- == 0) + return FNM_NORES; + for (stringstart = string;;) switch (c = *pattern++) { case EOS: @@ -75,8 +78,11 @@ p_fnmatch(const char *pattern, const char *string, int flags) /* General case, use recursion. */ while ((test = *string) != EOS) { - if (!p_fnmatch(pattern, string, flags & ~FNM_PERIOD)) - return (0); + int e; + + e = p_fnmatchx(pattern, string, flags & ~FNM_PERIOD, recurs); + if (e != FNM_NOMATCH) + return e; if (test == '/' && (flags & FNM_PATHNAME)) break; ++string; @@ -178,3 +184,9 @@ rangematch(const char *pattern, char test, int flags, char **newp) return (ok == negate ? RANGE_NOMATCH : RANGE_MATCH); } +int +p_fnmatch(const char *pattern, const char *string, int flags) +{ + return p_fnmatchx(pattern, string, flags, 64); +} + diff --git a/src/compat/fnmatch.h b/src/fnmatch.h index 7faef09b3..920e7de4d 100644 --- a/src/compat/fnmatch.h +++ b/src/fnmatch.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -11,12 +11,13 @@ #define FNM_NOMATCH 1 /* Match failed. */ #define FNM_NOSYS 2 /* Function not supported (unused). */ +#define FNM_NORES 3 /* Out of resources */ -#define FNM_NOESCAPE 0x01 /* Disable backslash escaping. */ -#define FNM_PATHNAME 0x02 /* Slash must be matched by slash. */ +#define FNM_NOESCAPE 0x01 /* Disable backslash escaping. */ +#define FNM_PATHNAME 0x02 /* Slash must be matched by slash. */ #define FNM_PERIOD 0x04 /* Period must be matched by period. */ #define FNM_LEADING_DIR 0x08 /* Ignore /<tail> after Imatch. */ -#define FNM_CASEFOLD 0x10 /* Case insensitive search. */ +#define FNM_CASEFOLD 0x10 /* Case insensitive search. */ #define FNM_IGNORECASE FNM_CASEFOLD #define FNM_FILE_NAME FNM_PATHNAME diff --git a/src/global.c b/src/global.c index 368c6c664..b7fd8e257 100644 --- a/src/global.c +++ b/src/global.c @@ -1,14 +1,19 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ #include "common.h" #include "global.h" -#include "git2/threads.h" +#include "hash.h" +#include "fileops.h" +#include "git2/threads.h" #include "thread-utils.h" + +git_mutex git__mwindow_mutex; + /** * Handle the global state with TLS * @@ -35,30 +40,58 @@ * functions are not available in that case. */ +/* + * `git_threads_init()` allows subsystems to perform global setup, + * which may take place in the global scope. An explicit memory + * fence exists at the exit of `git_threads_init()`. Without this, + * CPU cores are free to reorder cache invalidation of `_tls_init` + * before cache invalidation of the subsystems' newly written global + * state. + */ #if defined(GIT_THREADS) && defined(GIT_WIN32) static DWORD _tls_index; static int _tls_init = 0; -void git_threads_init(void) +int git_threads_init(void) { + int error; + if (_tls_init) - return; + return 0; _tls_index = TlsAlloc(); - _tls_init = 1; + git_mutex_init(&git__mwindow_mutex); + + /* Initialize any other subsystems that have global state */ + if ((error = git_hash_global_init()) >= 0) + _tls_init = 1; + + if (error == 0) + _tls_init = 1; + + GIT_MEMORY_BARRIER; + + return error; } void git_threads_shutdown(void) { TlsFree(_tls_index); _tls_init = 0; + git_mutex_free(&git__mwindow_mutex); + + /* Shut down any subsystems that have global state */ + git_hash_global_shutdown(); + git_futils_dirs_free(); } git_global_st *git__global_state(void) { void *ptr; + assert(_tls_init); + if ((ptr = TlsGetValue(_tls_index)) != NULL) return ptr; @@ -81,25 +114,42 @@ static void cb__free_status(void *st) git__free(st); } -void git_threads_init(void) +int git_threads_init(void) { + int error = 0; + if (_tls_init) - return; + return 0; + git_mutex_init(&git__mwindow_mutex); pthread_key_create(&_tls_key, &cb__free_status); - _tls_init = 1; + + /* Initialize any other subsystems that have global state */ + if ((error = git_hash_global_init()) >= 0) + _tls_init = 1; + + GIT_MEMORY_BARRIER; + + return error; } void git_threads_shutdown(void) { pthread_key_delete(_tls_key); _tls_init = 0; + git_mutex_free(&git__mwindow_mutex); + + /* Shut down any subsystems that have global state */ + git_hash_global_shutdown(); + git_futils_dirs_free(); } git_global_st *git__global_state(void) { void *ptr; + assert(_tls_init); + if ((ptr = pthread_getspecific(_tls_key)) != NULL) return ptr; @@ -116,14 +166,17 @@ git_global_st *git__global_state(void) static git_global_st __state; -void git_threads_init(void) +int git_threads_init(void) { /* noop */ + return 0; } void git_threads_shutdown(void) { - /* noop */ + /* Shut down any subsystems that have global state */ + git_hash_global_shutdown(); + git_futils_dirs_free(); } git_global_st *git__global_state(void) diff --git a/src/global.h b/src/global.h index 2b525ce07..f0ad1df29 100644 --- a/src/global.h +++ b/src/global.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -8,20 +8,25 @@ #define INCLUDE_global_h__ #include "mwindow.h" +#include "hash.h" -typedef struct { - struct { - char last[1024]; - } error; +#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; - - git_mwindow_ctl mem_ctl; } git_global_st; git_global_st *git__global_state(void); +extern git_mutex git__mwindow_mutex; + #define GIT_GLOBAL (git__global_state()) #endif diff --git a/src/graph.c b/src/graph.c new file mode 100644 index 000000000..277f588ca --- /dev/null +++ b/src/graph.c @@ -0,0 +1,178 @@ + +/* + * 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 "revwalk.h" +#include "merge.h" +#include "git2/graph.h" + +static int interesting(git_pqueue *list, git_commit_list *roots) +{ + unsigned int i; + /* element 0 isn't used - we need to start at 1 */ + for (i = 1; i < list->size; i++) { + git_commit_list_node *commit = list->d[i]; + if ((commit->flags & STALE) == 0) + return 1; + } + + while(roots) { + if ((roots->item->flags & STALE) == 0) + return 1; + roots = roots->next; + } + + return 0; +} + +static int mark_parents(git_revwalk *walk, git_commit_list_node *one, + git_commit_list_node *two) +{ + unsigned int i; + git_commit_list *roots = NULL; + git_pqueue list; + + /* if the commit is repeated, we have a our merge base already */ + if (one == two) { + one->flags |= PARENT1 | PARENT2 | RESULT; + return 0; + } + + if (git_pqueue_init(&list, 2, git_commit_list_time_cmp) < 0) + return -1; + + if (git_commit_list_parse(walk, one) < 0) + goto on_error; + one->flags |= PARENT1; + if (git_pqueue_insert(&list, one) < 0) + goto on_error; + + if (git_commit_list_parse(walk, two) < 0) + goto on_error; + two->flags |= PARENT2; + if (git_pqueue_insert(&list, two) < 0) + goto on_error; + + /* as long as there are non-STALE commits */ + while (interesting(&list, roots)) { + git_commit_list_node *commit; + int flags; + + commit = git_pqueue_pop(&list); + if (commit == NULL) + break; + + flags = commit->flags & (PARENT1 | PARENT2 | STALE); + if (flags == (PARENT1 | PARENT2)) { + if (!(commit->flags & RESULT)) + commit->flags |= RESULT; + /* we mark the parents of a merge stale */ + flags |= STALE; + } + + for (i = 0; i < commit->out_degree; i++) { + git_commit_list_node *p = commit->parents[i]; + if ((p->flags & flags) == flags) + continue; + + if (git_commit_list_parse(walk, p) < 0) + goto on_error; + + p->flags |= flags; + if (git_pqueue_insert(&list, p) < 0) + goto on_error; + } + + /* Keep track of root commits, to make sure the path gets marked */ + if (commit->out_degree == 0) { + if (git_commit_list_insert(commit, &roots) == NULL) + goto on_error; + } + } + + git_commit_list_free(&roots); + git_pqueue_free(&list); + return 0; + +on_error: + git_commit_list_free(&roots); + git_pqueue_free(&list); + return -1; +} + + +static int ahead_behind(git_commit_list_node *one, git_commit_list_node *two, + size_t *ahead, size_t *behind) +{ + git_commit_list_node *commit; + git_pqueue pq; + int i; + *ahead = 0; + *behind = 0; + + if (git_pqueue_init(&pq, 2, git_commit_list_time_cmp) < 0) + return -1; + if (git_pqueue_insert(&pq, one) < 0) + goto on_error; + if (git_pqueue_insert(&pq, two) < 0) + goto on_error; + + while ((commit = git_pqueue_pop(&pq)) != NULL) { + if (commit->flags & RESULT || + (commit->flags & (PARENT1 | PARENT2)) == (PARENT1 | PARENT2)) + continue; + else if (commit->flags & PARENT1) + (*behind)++; + else if (commit->flags & PARENT2) + (*ahead)++; + + for (i = 0; i < commit->out_degree; i++) { + git_commit_list_node *p = commit->parents[i]; + if (git_pqueue_insert(&pq, p) < 0) + return -1; + } + commit->flags |= RESULT; + } + + git_pqueue_free(&pq); + return 0; + +on_error: + git_pqueue_free(&pq); + return -1; +} + +int git_graph_ahead_behind(size_t *ahead, size_t *behind, git_repository *repo, + const git_oid *local, const git_oid *upstream) +{ + git_revwalk *walk; + git_commit_list_node *commit_u, *commit_l; + + if (git_revwalk_new(&walk, repo) < 0) + return -1; + + commit_u = git_revwalk__commit_lookup(walk, upstream); + if (commit_u == NULL) + goto on_error; + + commit_l = git_revwalk__commit_lookup(walk, local); + if (commit_l == NULL) + goto on_error; + + if (mark_parents(walk, commit_l, commit_u) < 0) + goto on_error; + if (ahead_behind(commit_l, commit_u, ahead, behind) < 0) + goto on_error; + + git_revwalk_free(walk); + + return 0; + +on_error: + git_revwalk_free(walk); + return -1; +} diff --git a/src/hash.c b/src/hash.c index 460756913..f3645a913 100644 --- a/src/hash.c +++ b/src/hash.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -8,67 +8,40 @@ #include "common.h" #include "hash.h" -#if defined(PPC_SHA1) -# include "ppc/sha1.h" -#else -# include "sha1.h" -#endif - -struct git_hash_ctx { - SHA_CTX c; -}; - -git_hash_ctx *git_hash_new_ctx(void) +int git_hash_buf(git_oid *out, const void *data, size_t len) { - git_hash_ctx *ctx = git__malloc(sizeof(*ctx)); - - if (!ctx) - return NULL; - - SHA1_Init(&ctx->c); + git_hash_ctx ctx; + int error = 0; - return ctx; -} + if (git_hash_ctx_init(&ctx) < 0) + return -1; -void git_hash_free_ctx(git_hash_ctx *ctx) -{ - git__free(ctx); -} + if ((error = git_hash_update(&ctx, data, len)) >= 0) + error = git_hash_final(out, &ctx); -void git_hash_init(git_hash_ctx *ctx) -{ - assert(ctx); - SHA1_Init(&ctx->c); + git_hash_ctx_cleanup(&ctx); + + return error; } -void git_hash_update(git_hash_ctx *ctx, const void *data, size_t len) +int git_hash_vec(git_oid *out, git_buf_vec *vec, size_t n) { - assert(ctx); - SHA1_Update(&ctx->c, data, len); -} + git_hash_ctx ctx; + size_t i; + int error = 0; -void git_hash_final(git_oid *out, git_hash_ctx *ctx) -{ - assert(ctx); - SHA1_Final(out->id, &ctx->c); -} + if (git_hash_ctx_init(&ctx) < 0) + return -1; -void git_hash_buf(git_oid *out, const void *data, size_t len) -{ - SHA_CTX c; + for (i = 0; i < n; i++) { + if ((error = git_hash_update(&ctx, vec[i].data, vec[i].len)) < 0) + goto done; + } - SHA1_Init(&c); - SHA1_Update(&c, data, len); - SHA1_Final(out->id, &c); -} + error = git_hash_final(out, &ctx); -void git_hash_vec(git_oid *out, git_buf_vec *vec, size_t n) -{ - SHA_CTX c; - size_t i; +done: + git_hash_ctx_cleanup(&ctx); - SHA1_Init(&c); - for (i = 0; i < n; i++) - SHA1_Update(&c, vec[i].data, vec[i].len); - SHA1_Final(out->id, &c); + return error; } diff --git a/src/hash.h b/src/hash.h index 33d7b20cd..5b848981f 100644 --- a/src/hash.h +++ b/src/hash.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -9,21 +9,33 @@ #include "git2/oid.h" +typedef struct git_hash_prov git_hash_prov; typedef struct git_hash_ctx git_hash_ctx; +int git_hash_global_init(void); +void git_hash_global_shutdown(void); + +int git_hash_ctx_init(git_hash_ctx *ctx); +void git_hash_ctx_cleanup(git_hash_ctx *ctx); + +#if defined(OPENSSL_SHA1) +# include "hash/hash_openssl.h" +#elif defined(WIN32_SHA1) +# include "hash/hash_win32.h" +#else +# include "hash/hash_generic.h" +#endif + typedef struct { void *data; size_t len; } git_buf_vec; -git_hash_ctx *git_hash_new_ctx(void); -void git_hash_free_ctx(git_hash_ctx *ctx); - -void git_hash_init(git_hash_ctx *c); -void git_hash_update(git_hash_ctx *c, const void *data, size_t len); -void git_hash_final(git_oid *out, git_hash_ctx *c); +int git_hash_init(git_hash_ctx *c); +int git_hash_update(git_hash_ctx *c, const void *data, size_t len); +int git_hash_final(git_oid *out, git_hash_ctx *c); -void git_hash_buf(git_oid *out, const void *data, size_t len); -void git_hash_vec(git_oid *out, git_buf_vec *vec, size_t n); +int git_hash_buf(git_oid *out, const void *data, size_t len); +int git_hash_vec(git_oid *out, git_buf_vec *vec, size_t n); #endif /* INCLUDE_hash_h__ */ diff --git a/src/sha1.c b/src/hash/hash_generic.c index 8aaedeb8f..32fcd869c 100644 --- a/src/sha1.c +++ b/src/hash/hash_generic.c @@ -1,12 +1,13 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ #include "common.h" -#include "sha1.h" +#include "hash.h" +#include "hash/hash_generic.h" #if defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__)) @@ -112,7 +113,7 @@ #define T_40_59(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, ((B&C)+(D&(B^C))) , 0x8f1bbcdc, A, B, C, D, E ) #define T_60_79(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, (B^C^D) , 0xca62c1d6, A, B, C, D, E ) -static void blk_SHA1_Block(blk_SHA_CTX *ctx, const unsigned int *data) +static void hash__block(git_hash_ctx *ctx, const unsigned int *data) { unsigned int A,B,C,D,E; unsigned int array[16]; @@ -220,7 +221,7 @@ static void blk_SHA1_Block(blk_SHA_CTX *ctx, const unsigned int *data) ctx->H[4] += E; } -void git__blk_SHA1_Init(blk_SHA_CTX *ctx) +int git_hash_init(git_hash_ctx *ctx) { ctx->size = 0; @@ -230,9 +231,11 @@ void git__blk_SHA1_Init(blk_SHA_CTX *ctx) ctx->H[2] = 0x98badcfe; ctx->H[3] = 0x10325476; ctx->H[4] = 0xc3d2e1f0; + + return 0; } -void git__blk_SHA1_Update(blk_SHA_CTX *ctx, const void *data, size_t len) +int git_hash_update(git_hash_ctx *ctx, const void *data, size_t len) { unsigned int lenW = ctx->size & 63; @@ -248,19 +251,21 @@ void git__blk_SHA1_Update(blk_SHA_CTX *ctx, const void *data, size_t len) len -= left; data = ((const char *)data + left); if (lenW) - return; - blk_SHA1_Block(ctx, ctx->W); + return 0; + hash__block(ctx, ctx->W); } while (len >= 64) { - blk_SHA1_Block(ctx, data); + hash__block(ctx, data); data = ((const char *)data + 64); len -= 64; } if (len) memcpy(ctx->W, data, len); + + return 0; } -void git__blk_SHA1_Final(unsigned char hashout[20], blk_SHA_CTX *ctx) +int git_hash_final(git_oid *out, git_hash_ctx *ctx) { static const unsigned char pad[64] = { 0x80 }; unsigned int padlen[2]; @@ -271,10 +276,13 @@ void git__blk_SHA1_Final(unsigned char hashout[20], blk_SHA_CTX *ctx) padlen[1] = htonl((uint32_t)(ctx->size << 3)); i = ctx->size & 63; - git__blk_SHA1_Update(ctx, pad, 1+ (63 & (55 - i))); - git__blk_SHA1_Update(ctx, padlen, 8); + git_hash_update(ctx, pad, 1+ (63 & (55 - i))); + git_hash_update(ctx, padlen, 8); /* Output hash */ for (i = 0; i < 5; i++) - put_be32(hashout + i*4, ctx->H[i]); + put_be32(out->id + i*4, ctx->H[i]); + + return 0; } + diff --git a/src/hash/hash_generic.h b/src/hash/hash_generic.h new file mode 100644 index 000000000..b731de8b3 --- /dev/null +++ b/src/hash/hash_generic.h @@ -0,0 +1,24 @@ +/* + * 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_hash_generic_h__ +#define INCLUDE_hash_generic_h__ + +#include "hash.h" + +struct git_hash_ctx { + unsigned long long size; + unsigned int H[5]; + unsigned int W[16]; +}; + +#define git_hash_global_init() 0 +#define git_hash_global_shutdown() /* noop */ +#define git_hash_ctx_init(ctx) git_hash_init(ctx) +#define git_hash_ctx_cleanup(ctx) + +#endif /* INCLUDE_hash_generic_h__ */ diff --git a/src/hash/hash_openssl.h b/src/hash/hash_openssl.h new file mode 100644 index 000000000..f83279a5a --- /dev/null +++ b/src/hash/hash_openssl.h @@ -0,0 +1,45 @@ +/* + * 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_hash_openssl_h__ +#define INCLUDE_hash_openssl_h__ + +#include "hash.h" + +#include <openssl/sha.h> + +struct git_hash_ctx { + SHA_CTX c; +}; + +#define git_hash_global_init() 0 +#define git_hash_global_shutdown() /* noop */ +#define git_hash_ctx_init(ctx) git_hash_init(ctx) +#define git_hash_ctx_cleanup(ctx) + +GIT_INLINE(int) git_hash_init(git_hash_ctx *ctx) +{ + assert(ctx); + SHA1_Init(&ctx->c); + return 0; +} + +GIT_INLINE(int) git_hash_update(git_hash_ctx *ctx, const void *data, size_t len) +{ + assert(ctx); + SHA1_Update(&ctx->c, data, len); + return 0; +} + +GIT_INLINE(int) git_hash_final(git_oid *out, git_hash_ctx *ctx) +{ + assert(ctx); + SHA1_Final(out->id, &ctx->c); + return 0; +} + +#endif /* INCLUDE_hash_openssl_h__ */ diff --git a/src/hash/hash_win32.c b/src/hash/hash_win32.c new file mode 100644 index 000000000..43d54ca6d --- /dev/null +++ b/src/hash/hash_win32.c @@ -0,0 +1,291 @@ +/* + * 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 "global.h" +#include "hash.h" +#include "hash/hash_win32.h" + +#include <wincrypt.h> +#include <strsafe.h> + +static struct git_hash_prov hash_prov = {0}; + +/* Hash initialization */ + +/* Initialize CNG, if available */ +GIT_INLINE(int) hash_cng_prov_init(void) +{ + OSVERSIONINFOEX version_test = {0}; + DWORD version_test_mask; + DWORDLONG version_condition_mask = 0; + char dll_path[MAX_PATH]; + DWORD dll_path_len, size_len; + + return -1; + + /* Only use CNG on Windows 2008 / Vista SP1 or better (Windows 6.0 SP1) */ + version_test.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); + version_test.dwMajorVersion = 6; + version_test.dwMinorVersion = 0; + version_test.wServicePackMajor = 1; + version_test.wServicePackMinor = 0; + + version_test_mask = (VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR | VER_SERVICEPACKMINOR); + + VER_SET_CONDITION(version_condition_mask, VER_MAJORVERSION, VER_GREATER_EQUAL); + VER_SET_CONDITION(version_condition_mask, VER_MINORVERSION, VER_GREATER_EQUAL); + VER_SET_CONDITION(version_condition_mask, VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL); + VER_SET_CONDITION(version_condition_mask, VER_SERVICEPACKMINOR, VER_GREATER_EQUAL); + + if (!VerifyVersionInfo(&version_test, version_test_mask, version_condition_mask)) + return -1; + + /* Load bcrypt.dll explicitly from the system directory */ + if ((dll_path_len = GetSystemDirectory(dll_path, MAX_PATH)) == 0 || dll_path_len > MAX_PATH || + StringCchCat(dll_path, MAX_PATH, "\\") < 0 || + StringCchCat(dll_path, MAX_PATH, GIT_HASH_CNG_DLL_NAME) < 0 || + (hash_prov.prov.cng.dll = LoadLibrary(dll_path)) == NULL) + return -1; + + /* Load the function addresses */ + if ((hash_prov.prov.cng.open_algorithm_provider = (hash_win32_cng_open_algorithm_provider_fn)GetProcAddress(hash_prov.prov.cng.dll, "BCryptOpenAlgorithmProvider")) == NULL || + (hash_prov.prov.cng.get_property = (hash_win32_cng_get_property_fn)GetProcAddress(hash_prov.prov.cng.dll, "BCryptGetProperty")) == NULL || + (hash_prov.prov.cng.create_hash = (hash_win32_cng_create_hash_fn)GetProcAddress(hash_prov.prov.cng.dll, "BCryptCreateHash")) == NULL || + (hash_prov.prov.cng.finish_hash = (hash_win32_cng_finish_hash_fn)GetProcAddress(hash_prov.prov.cng.dll, "BCryptFinishHash")) == NULL || + (hash_prov.prov.cng.hash_data = (hash_win32_cng_hash_data_fn)GetProcAddress(hash_prov.prov.cng.dll, "BCryptHashData")) == NULL || + (hash_prov.prov.cng.destroy_hash = (hash_win32_cng_destroy_hash_fn)GetProcAddress(hash_prov.prov.cng.dll, "BCryptDestroyHash")) == NULL || + (hash_prov.prov.cng.close_algorithm_provider = (hash_win32_cng_close_algorithm_provider_fn)GetProcAddress(hash_prov.prov.cng.dll, "BCryptCloseAlgorithmProvider")) == NULL) { + FreeLibrary(hash_prov.prov.cng.dll); + return -1; + } + + /* Load the SHA1 algorithm */ + if (hash_prov.prov.cng.open_algorithm_provider(&hash_prov.prov.cng.handle, GIT_HASH_CNG_HASH_TYPE, NULL, GIT_HASH_CNG_HASH_REUSABLE) < 0) { + FreeLibrary(hash_prov.prov.cng.dll); + return -1; + } + + /* Get storage space for the hash object */ + if (hash_prov.prov.cng.get_property(hash_prov.prov.cng.handle, GIT_HASH_CNG_HASH_OBJECT_LEN, (PBYTE)&hash_prov.prov.cng.hash_object_size, sizeof(DWORD), &size_len, 0) < 0) { + hash_prov.prov.cng.close_algorithm_provider(hash_prov.prov.cng.handle, 0); + FreeLibrary(hash_prov.prov.cng.dll); + return -1; + } + + hash_prov.type = CNG; + return 0; +} + +GIT_INLINE(void) hash_cng_prov_shutdown(void) +{ + hash_prov.prov.cng.close_algorithm_provider(hash_prov.prov.cng.handle, 0); + FreeLibrary(hash_prov.prov.cng.dll); + + hash_prov.type = INVALID; +} + +/* Initialize CryptoAPI */ +GIT_INLINE(int) hash_cryptoapi_prov_init() +{ + if (!CryptAcquireContext(&hash_prov.prov.cryptoapi.handle, NULL, 0, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) + return -1; + + hash_prov.type = CRYPTOAPI; + return 0; +} + +GIT_INLINE(void) hash_cryptoapi_prov_shutdown(void) +{ + CryptReleaseContext(hash_prov.prov.cryptoapi.handle, 0); + + hash_prov.type = INVALID; +} + +int git_hash_global_init() +{ + int error = 0; + + if (hash_prov.type != INVALID) + return 0; + + if ((error = hash_cng_prov_init()) < 0) + error = hash_cryptoapi_prov_init(); + + return error; +} + +void git_hash_global_shutdown() +{ + if (hash_prov.type == CNG) + hash_cng_prov_shutdown(); + else if(hash_prov.type == CRYPTOAPI) + hash_cryptoapi_prov_shutdown(); +} + +/* CryptoAPI: available in Windows XP and newer */ + +GIT_INLINE(int) hash_ctx_cryptoapi_init(git_hash_ctx *ctx) +{ + ctx->type = CRYPTOAPI; + ctx->prov = &hash_prov; + + return git_hash_init(ctx); +} + +GIT_INLINE(int) hash_cryptoapi_init(git_hash_ctx *ctx) +{ + if (ctx->ctx.cryptoapi.valid) + CryptDestroyHash(ctx->ctx.cryptoapi.hash_handle); + + if (!CryptCreateHash(ctx->prov->prov.cryptoapi.handle, CALG_SHA1, 0, 0, &ctx->ctx.cryptoapi.hash_handle)) { + ctx->ctx.cryptoapi.valid = 0; + return -1; + } + + ctx->ctx.cryptoapi.valid = 1; + return 0; +} + +GIT_INLINE(int) hash_cryptoapi_update(git_hash_ctx *ctx, const void *data, size_t len) +{ + assert(ctx->ctx.cryptoapi.valid); + + if (!CryptHashData(ctx->ctx.cryptoapi.hash_handle, (const BYTE *)data, (DWORD)len, 0)) + return -1; + + return 0; +} + +GIT_INLINE(int) hash_cryptoapi_final(git_oid *out, git_hash_ctx *ctx) +{ + DWORD len = 20; + int error = 0; + + assert(ctx->ctx.cryptoapi.valid); + + if (!CryptGetHashParam(ctx->ctx.cryptoapi.hash_handle, HP_HASHVAL, out->id, &len, 0)) + error = -1; + + CryptDestroyHash(ctx->ctx.cryptoapi.hash_handle); + ctx->ctx.cryptoapi.valid = 0; + + return error; +} + +GIT_INLINE(void) hash_ctx_cryptoapi_cleanup(git_hash_ctx *ctx) +{ + if (ctx->ctx.cryptoapi.valid) + CryptDestroyHash(ctx->ctx.cryptoapi.hash_handle); +} + +/* CNG: Available in Windows Server 2008 and newer */ + +GIT_INLINE(int) hash_ctx_cng_init(git_hash_ctx *ctx) +{ + if ((ctx->ctx.cng.hash_object = git__malloc(hash_prov.prov.cng.hash_object_size)) == NULL) + return -1; + + if (hash_prov.prov.cng.create_hash(hash_prov.prov.cng.handle, &ctx->ctx.cng.hash_handle, ctx->ctx.cng.hash_object, hash_prov.prov.cng.hash_object_size, NULL, 0, 0) < 0) { + git__free(ctx->ctx.cng.hash_object); + return -1; + } + + ctx->type = CNG; + ctx->prov = &hash_prov; + + return 0; +} + +GIT_INLINE(int) hash_cng_init(git_hash_ctx *ctx) +{ + BYTE hash[GIT_OID_RAWSZ]; + + if (!ctx->ctx.cng.updated) + return 0; + + /* CNG needs to be finished to restart */ + if (ctx->prov->prov.cng.finish_hash(ctx->ctx.cng.hash_handle, hash, GIT_OID_RAWSZ, 0) < 0) + return -1; + + ctx->ctx.cng.updated = 0; + + return 0; +} + +GIT_INLINE(int) hash_cng_update(git_hash_ctx *ctx, const void *data, size_t len) +{ + if (ctx->prov->prov.cng.hash_data(ctx->ctx.cng.hash_handle, (PBYTE)data, (ULONG)len, 0) < 0) + return -1; + + return 0; +} + +GIT_INLINE(int) hash_cng_final(git_oid *out, git_hash_ctx *ctx) +{ + if (ctx->prov->prov.cng.finish_hash(ctx->ctx.cng.hash_handle, out->id, GIT_OID_RAWSZ, 0) < 0) + return -1; + + ctx->ctx.cng.updated = 0; + + return 0; +} + +GIT_INLINE(void) hash_ctx_cng_cleanup(git_hash_ctx *ctx) +{ + ctx->prov->prov.cng.destroy_hash(ctx->ctx.cng.hash_handle); + git__free(ctx->ctx.cng.hash_object); +} + +/* Indirection between CryptoAPI and CNG */ + +int git_hash_ctx_init(git_hash_ctx *ctx) +{ + int error = 0; + + assert(ctx); + + /* + * When compiled with GIT_THREADS, the global hash_prov data is + * initialized with git_threads_init. Otherwise, it must be initialized + * at first use. + */ + if (hash_prov.type == INVALID && (error = git_hash_global_init()) < 0) + return error; + + memset(ctx, 0x0, sizeof(git_hash_ctx)); + + return (hash_prov.type == CNG) ? hash_ctx_cng_init(ctx) : hash_ctx_cryptoapi_init(ctx); +} + +int git_hash_init(git_hash_ctx *ctx) +{ + assert(ctx && ctx->type); + return (ctx->type == CNG) ? hash_cng_init(ctx) : hash_cryptoapi_init(ctx); +} + +int git_hash_update(git_hash_ctx *ctx, const void *data, size_t len) +{ + assert(ctx && ctx->type); + return (ctx->type == CNG) ? hash_cng_update(ctx, data, len) : hash_cryptoapi_update(ctx, data, len); +} + +int git_hash_final(git_oid *out, git_hash_ctx *ctx) +{ + assert(ctx && ctx->type); + return (ctx->type == CNG) ? hash_cng_final(out, ctx) : hash_cryptoapi_final(out, ctx); +} + +void git_hash_ctx_cleanup(git_hash_ctx *ctx) +{ + assert(ctx); + + if (ctx->type == CNG) + hash_ctx_cng_cleanup(ctx); + else if(ctx->type == CRYPTOAPI) + hash_ctx_cryptoapi_cleanup(ctx); +} diff --git a/src/hash/hash_win32.h b/src/hash/hash_win32.h new file mode 100644 index 000000000..daa769b59 --- /dev/null +++ b/src/hash/hash_win32.h @@ -0,0 +1,140 @@ +/* + * 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_hash_win32_h__ +#define INCLUDE_hash_win32_h__ + +#include "common.h" +#include "hash.h" + +#include <wincrypt.h> +#include <strsafe.h> + +enum hash_win32_prov_type { + INVALID = 0, + CRYPTOAPI, + CNG +}; + +/* + * CryptoAPI is available for hashing on Windows XP and newer. + */ + +struct hash_cryptoapi_prov { + HCRYPTPROV handle; +}; + +/* + * CNG (bcrypt.dll) is significantly more performant than CryptoAPI and is + * preferred, however it is only available on Windows 2008 and newer and + * must therefore be dynamically loaded, and we must inline constants that + * would not exist when building in pre-Windows 2008 environments. + */ + +#define GIT_HASH_CNG_DLL_NAME "bcrypt.dll" + +/* BCRYPT_SHA1_ALGORITHM */ +#define GIT_HASH_CNG_HASH_TYPE L"SHA1" + +/* BCRYPT_OBJECT_LENGTH */ +#define GIT_HASH_CNG_HASH_OBJECT_LEN L"ObjectLength" + +/* BCRYPT_HASH_REUSEABLE_FLAGS */ +#define GIT_HASH_CNG_HASH_REUSABLE 0x00000020 + +/* 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); + +typedef NTSTATUS (WINAPI *hash_win32_cng_get_property_fn)( + HANDLE /* BCRYPT_HANDLE */ hObject, + LPCWSTR pszProperty, + PUCHAR pbOutput, + ULONG cbOutput, + ULONG *pcbResult, + ULONG dwFlags); + +typedef NTSTATUS (WINAPI *hash_win32_cng_create_hash_fn)( + HANDLE /* BCRYPT_ALG_HANDLE */ hAlgorithm, + HANDLE /* BCRYPT_HASH_HANDLE */ *phHash, + PUCHAR pbHashObject, ULONG cbHashObject, + PUCHAR pbSecret, + ULONG cbSecret, + ULONG dwFlags); + +typedef NTSTATUS (WINAPI *hash_win32_cng_finish_hash_fn)( + HANDLE /* BCRYPT_HASH_HANDLE */ hHash, + PUCHAR pbOutput, + ULONG cbOutput, + ULONG dwFlags); + +typedef NTSTATUS (WINAPI *hash_win32_cng_hash_data_fn)( + HANDLE /* BCRYPT_HASH_HANDLE */ hHash, + PUCHAR pbInput, + ULONG cbInput, + ULONG dwFlags); + +typedef NTSTATUS (WINAPI *hash_win32_cng_destroy_hash_fn)( + HANDLE /* BCRYPT_HASH_HANDLE */ hHash); + +typedef NTSTATUS (WINAPI *hash_win32_cng_close_algorithm_provider_fn)( + HANDLE /* BCRYPT_ALG_HANDLE */ hAlgorithm, + ULONG dwFlags); + +struct hash_cng_prov { + /* DLL for CNG */ + HINSTANCE dll; + + /* Function pointers for CNG */ + hash_win32_cng_open_algorithm_provider_fn open_algorithm_provider; + hash_win32_cng_get_property_fn get_property; + hash_win32_cng_create_hash_fn create_hash; + hash_win32_cng_finish_hash_fn finish_hash; + hash_win32_cng_hash_data_fn hash_data; + hash_win32_cng_destroy_hash_fn destroy_hash; + hash_win32_cng_close_algorithm_provider_fn close_algorithm_provider; + + HANDLE /* BCRYPT_ALG_HANDLE */ handle; + DWORD hash_object_size; +}; + +struct git_hash_prov { + enum hash_win32_prov_type type; + + union { + struct hash_cryptoapi_prov cryptoapi; + struct hash_cng_prov cng; + } prov; +}; + +/* Hash contexts */ + +struct hash_cryptoapi_ctx { + bool valid; + HCRYPTHASH hash_handle; +}; + +struct hash_cng_ctx { + bool updated; + HANDLE /* BCRYPT_HASH_HANDLE */ hash_handle; + PBYTE hash_object; +}; + +struct git_hash_ctx { + enum hash_win32_prov_type type; + git_hash_prov *prov; + + union { + struct hash_cryptoapi_ctx cryptoapi; + struct hash_cng_ctx cng; + } ctx; +}; + +#endif /* INCLUDE_hash_openssl_h__ */ diff --git a/src/hashsig.c b/src/hashsig.c new file mode 100644 index 000000000..3a75aaaed --- /dev/null +++ b/src/hashsig.c @@ -0,0 +1,368 @@ +/* + * 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 "hashsig.h" +#include "fileops.h" +#include "util.h" + +typedef uint32_t hashsig_t; +typedef uint64_t hashsig_state; + +#define HASHSIG_SCALE 100 + +#define HASHSIG_HASH_WINDOW 32 +#define HASHSIG_HASH_START 0 +#define HASHSIG_HASH_SHIFT 5 +#define HASHSIG_HASH_MASK 0x7FFFFFFF + +#define HASHSIG_HEAP_SIZE ((1 << 7) - 1) + +typedef int (*hashsig_cmp)(const void *a, const void *b, void *); + +typedef struct { + int size, asize; + hashsig_cmp cmp; + 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; + git_hashsig_option_t opt; + int considered; +}; + +#define HEAP_LCHILD_OF(I) (((I)*2)+1) +#define HEAP_RCHILD_OF(I) (((I)*2)+2) +#define HEAP_PARENT_OF(I) (((I)-1)>>1) + +static void hashsig_heap_init(hashsig_heap *h, hashsig_cmp cmp) +{ + h->size = 0; + h->asize = HASHSIG_HEAP_SIZE; + h->cmp = cmp; +} + +static int hashsig_cmp_max(const void *a, const void *b, void *payload) +{ + hashsig_t av = *(const hashsig_t *)a, bv = *(const hashsig_t *)b; + GIT_UNUSED(payload); + return (av < bv) ? -1 : (av > bv) ? 1 : 0; +} + +static int hashsig_cmp_min(const void *a, const void *b, void *payload) +{ + hashsig_t av = *(const hashsig_t *)a, bv = *(const hashsig_t *)b; + GIT_UNUSED(payload); + return (av > bv) ? -1 : (av < bv) ? 1 : 0; +} + +static void hashsig_heap_up(hashsig_heap *h, int el) +{ + int parent_el = HEAP_PARENT_OF(el); + + while (el > 0 && h->cmp(&h->values[parent_el], &h->values[el], NULL) > 0) { + hashsig_t t = h->values[el]; + h->values[el] = h->values[parent_el]; + h->values[parent_el] = t; + + el = parent_el; + parent_el = HEAP_PARENT_OF(el); + } +} + +static void hashsig_heap_down(hashsig_heap *h, int el) +{ + hashsig_t v, lv, rv; + + /* 'el < h->size / 2' tests if el is bottom row of heap */ + + while (el < h->size / 2) { + int lel = HEAP_LCHILD_OF(el), rel = HEAP_RCHILD_OF(el), swapel; + + v = h->values[el]; + lv = h->values[lel]; + rv = h->values[rel]; + + if (h->cmp(&v, &lv, NULL) < 0 && h->cmp(&v, &rv, NULL) < 0) + break; + + swapel = (h->cmp(&lv, &rv, NULL) < 0) ? lel : rel; + + h->values[el] = h->values[swapel]; + h->values[swapel] = v; + + el = swapel; + } +} + +static void hashsig_heap_sort(hashsig_heap *h) +{ + /* only need to do this at the end for signature comparison */ + git__qsort_r(h->values, h->size, sizeof(hashsig_t), h->cmp, NULL); +} + +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'); + } + + 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 */ + + 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; + } + + prog->state = state; + prog->win_len = win_len; + prog->shift_n = shift_n; + + *data = scan; +} + +static int hashsig_add_hashes( + git_hashsig *sig, + const char *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]; + + state = (state - rmv) & HASHSIG_HASH_MASK; + state = (state * HASHSIG_HASH_SHIFT) & HASHSIG_HASH_MASK; + state = (state + ch) & HASHSIG_HASH_MASK; + + hashsig_heap_insert(&sig->mins, (hashsig_t)state); + hashsig_heap_insert(&sig->maxs, (hashsig_t)state); + sig->considered++; + + prog->window[prog->win_pos] = ch; + prog->win_pos = (prog->win_pos + 1) % HASHSIG_HASH_WINDOW; + } + + prog->state = state; + + return 0; +} + +static int hashsig_finalize_hashes(git_hashsig *sig) +{ + if (sig->mins.size < HASHSIG_HEAP_SIZE) { + giterr_set(GITERR_INVALID, + "File too small for similarity signature calculation"); + return GIT_EBUFS; + } + + hashsig_heap_sort(&sig->mins); + hashsig_heap_sort(&sig->maxs); + + return 0; +} + +static git_hashsig *hashsig_alloc(git_hashsig_option_t opts) +{ + git_hashsig *sig = git__calloc(1, sizeof(git_hashsig)); + if (!sig) + return NULL; + + hashsig_heap_init(&sig->mins, hashsig_cmp_min); + hashsig_heap_init(&sig->maxs, hashsig_cmp_max); + sig->opt = opts; + + return sig; +} + +int git_hashsig_create( + git_hashsig **out, + const char *buf, + size_t buflen, + git_hashsig_option_t opts) +{ + int error; + hashsig_in_progress prog = HASHSIG_IN_PROGRESS_INIT; + git_hashsig *sig = hashsig_alloc(opts); + GITERR_CHECK_ALLOC(sig); + + error = hashsig_add_hashes(sig, buf, buflen, &prog); + + if (!error) + error = hashsig_finalize_hashes(sig); + + if (!error) + *out = sig; + else + git_hashsig_free(sig); + + return error; +} + +int git_hashsig_create_fromfile( + git_hashsig **out, + const char *path, + git_hashsig_option_t opts) +{ + char buf[4096]; + ssize_t buflen = 0; + int error = 0, fd; + hashsig_in_progress prog = HASHSIG_IN_PROGRESS_INIT; + git_hashsig *sig = hashsig_alloc(opts); + GITERR_CHECK_ALLOC(sig); + + if ((fd = git_futils_open_ro(path)) < 0) { + git__free(sig); + return fd; + } + + while (!error) { + if ((buflen = p_read(fd, buf, sizeof(buf))) <= 0) { + if ((error = (int)buflen) < 0) + giterr_set(GITERR_OS, + "Read error on '%s' calculating similarity hashes", path); + break; + } + + error = hashsig_add_hashes(sig, buf, buflen, &prog); + } + + p_close(fd); + + if (!error) + error = hashsig_finalize_hashes(sig); + + if (!error) + *out = sig; + else + git_hashsig_free(sig); + + return error; +} + +void git_hashsig_free(git_hashsig *sig) +{ + git__free(sig); +} + +static int hashsig_heap_compare(const hashsig_heap *a, const hashsig_heap *b) +{ + int matches = 0, i, j, cmp; + + assert(a->cmp == b->cmp); + + /* hash heaps are sorted - just look for overlap vs total */ + + for (i = 0, j = 0; i < a->size && j < b->size; ) { + cmp = a->cmp(&a->values[i], &b->values[j], NULL); + + if (cmp < 0) + ++i; + else if (cmp > 0) + ++j; + else { + ++i; ++j; ++matches; + } + } + + return HASHSIG_SCALE * (matches * 2) / (a->size + b->size); +} + +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; +} + diff --git a/src/hashsig.h b/src/hashsig.h new file mode 100644 index 000000000..8c920cbf1 --- /dev/null +++ b/src/hashsig.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_hashsig_h__ +#define INCLUDE_hashsig_h__ + +#include "common.h" + +/** + * Similarity signature of line hashes for a buffer + */ +typedef struct git_hashsig git_hashsig; + +typedef enum { + GIT_HASHSIG_NORMAL = 0, /* use all data */ + GIT_HASHSIG_IGNORE_WHITESPACE = 1, /* ignore whitespace */ + GIT_HASHSIG_SMART_WHITESPACE = 2, /* ignore \r and all space after \n */ +} git_hashsig_option_t; + +/** + * Build a similarity signature for a buffer + * + * If you have passed a whitespace-ignoring buffer, then the whitespace + * will be removed from the buffer while it is being processed, modifying + * the buffer in place. Sorry about that! + * + * This will return an error if the buffer doesn't contain enough data to + * compute a valid signature. + * + * @param out The array of hashed runs representing the file content + * @param buf The contents of the file to hash + * @param buflen The length of the data at `buf` + * @param generate_pairwise_hashes Should pairwise runs be hashed + */ +extern int git_hashsig_create( + git_hashsig **out, + const char *buf, + size_t buflen, + git_hashsig_option_t opts); + +/** + * Build a similarity signature from a file + * + * This walks through the file, only loading a maximum of 4K of file data at + * a time. Otherwise, it acts just like `git_hashsig_create`. + * + * This will return an error if the file doesn't contain enough data to + * compute a valid signature. + */ +extern int git_hashsig_create_fromfile( + git_hashsig **out, + const char *path, + git_hashsig_option_t opts); + +/** + * Release memory for a content similarity signature + */ +extern void git_hashsig_free(git_hashsig *sig); + +/** + * Measure similarity between two files + * + * @return <0 for error, [0 to 100] as similarity score + */ +extern int git_hashsig_compare( + const git_hashsig *a, + const git_hashsig *b); + +#endif diff --git a/src/ignore.c b/src/ignore.c index fc6194bb5..17779522c 100644 --- a/src/ignore.c +++ b/src/ignore.c @@ -1,19 +1,38 @@ +#include "git2/ignore.h" #include "ignore.h" +#include "attr.h" #include "path.h" +#include "config.h" #define GIT_IGNORE_INTERNAL "[internal]exclude" -#define GIT_IGNORE_FILE_INREPO "info/exclude" -#define GIT_IGNORE_FILE ".gitignore" + +#define GIT_IGNORE_DEFAULT_RULES ".\n..\n.git\n" static int parse_ignore_file( - git_repository *repo, const char *buffer, git_attr_file *ignores) + git_repository *repo, void *parsedata, const char *buffer, git_attr_file *ignores) { int error = 0; git_attr_fnmatch *match = NULL; const char *scan = NULL; char *context = NULL; - - GIT_UNUSED(repo); + 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); + + git_config_free(cfg); + } if (ignores->key && git__suffixcmp(ignores->key, "/" GIT_IGNORE_FILE) == 0) { context = ignores->key + 2; @@ -28,10 +47,16 @@ static int parse_ignore_file( GITERR_CHECK_ALLOC(match); } + match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE; + if (!(error = git_attr_fnmatch__parse( match, ignores->pool, context, &scan))) { - match->flags = match->flags | GIT_ATTR_FNMATCH_IGNORE; + match->flags |= GIT_ATTR_FNMATCH_IGNORE; + + if (ignore_case) + match->flags |= GIT_ATTR_FNMATCH_ICASE; + scan = git__next_line(scan); error = git_vector_insert(&ignores->rules, match); } @@ -55,13 +80,26 @@ static int parse_ignore_file( return error; } -#define push_ignore_file(R,S,B,F) \ - git_attr_cache__push_file((R),(B),(F),GIT_ATTR_FILE_FROM_FILE,parse_ignore_file,(S)) +#define push_ignore_file(R,IGN,S,B,F) \ + git_attr_cache__push_file((R),(B),(F),GIT_ATTR_FILE_FROM_FILE,parse_ignore_file,(IGN),(S)) static int push_one_ignore(void *ref, git_buf *path) { git_ignores *ign = (git_ignores *)ref; - return push_ignore_file(ign->repo, &ign->ign_path, path->ptr, GIT_IGNORE_FILE); + return push_ignore_file(ign->repo, ign, &ign->ign_path, path->ptr, GIT_IGNORE_FILE); +} + +static int get_internal_ignores(git_attr_file **ign, git_repository *repo) +{ + int error; + + if (!(error = git_attr_cache__init(repo))) + error = git_attr_cache__internal_file(repo, GIT_IGNORE_INTERNAL, ign); + + if (!error && !(*ign)->rules.length) + error = parse_ignore_file(repo, NULL, GIT_IGNORE_DEFAULT_RULES, *ign); + + return error; } int git_ignore__for_path( @@ -71,6 +109,8 @@ int git_ignore__for_path( { int error = 0; const char *workdir = git_repository_workdir(repo); + git_config *cfg = NULL; + int val; assert(ignores); @@ -78,6 +118,17 @@ 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) + 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) @@ -92,8 +143,7 @@ int git_ignore__for_path( goto cleanup; /* set up internals */ - error = git_attr_cache__internal_file( - repo, GIT_IGNORE_INTERNAL, &ignores->ign_internal); + error = get_internal_ignores(&ignores->ign_internal, repo); if (error < 0) goto cleanup; @@ -106,14 +156,14 @@ int git_ignore__for_path( } /* load .git/info/exclude */ - error = push_ignore_file(repo, &ignores->ign_global, + error = push_ignore_file(repo, ignores, &ignores->ign_global, git_repository_path(repo), GIT_IGNORE_FILE_INREPO); if (error < 0) goto cleanup; /* load core.excludesfile */ if (git_repository_attr_cache(repo)->cfg_excl_file != NULL) - error = push_ignore_file(repo, &ignores->ign_global, NULL, + error = push_ignore_file(repo, ignores, &ignores->ign_global, NULL, git_repository_attr_cache(repo)->cfg_excl_file); cleanup: @@ -129,7 +179,7 @@ int git_ignore__push_dir(git_ignores *ign, const char *dir) return -1; else return push_ignore_file( - ign->repo, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE); + ign->repo, ign, &ign->ign_path, ign->dir.ptr, GIT_IGNORE_FILE); } int git_ignore__pop_dir(git_ignores *ign) @@ -154,7 +204,7 @@ void git_ignore__free(git_ignores *ignores) static bool ignore_lookup_in_rules( git_vector *rules, git_attr_path *path, int *ignored) { - unsigned int j; + size_t j; git_attr_fnmatch *match; git_vector_rforeach(rules, j, match) { @@ -201,3 +251,110 @@ cleanup: git_attr_path__free(&path); return 0; } + +int git_ignore_add_rule( + git_repository *repo, + const char *rules) +{ + int error; + git_attr_file *ign_internal; + + if (!(error = get_internal_ignores(&ign_internal, repo))) + error = parse_ignore_file(repo, NULL, rules, ign_internal); + + return error; +} + +int git_ignore_clear_internal_rules( + git_repository *repo) +{ + int error; + git_attr_file *ign_internal; + + if (!(error = get_internal_ignores(&ign_internal, repo))) { + git_attr_file__clear_rules(ign_internal); + + return parse_ignore_file( + repo, NULL, GIT_IGNORE_DEFAULT_RULES, ign_internal); + } + + return error; +} + +int git_ignore_path_is_ignored( + int *ignored, + git_repository *repo, + const char *pathname) +{ + int error; + const char *workdir; + git_attr_path path; + char *tail, *end; + bool full_is_dir; + git_ignores ignores; + unsigned int i; + git_attr_file *file; + + assert(ignored && pathname); + + workdir = repo ? git_repository_workdir(repo) : NULL; + + if ((error = git_attr_path__init(&path, pathname, workdir)) < 0) + return error; + + tail = path.path; + end = &path.full.ptr[path.full.size]; + full_is_dir = path.is_dir; + + while (1) { + /* advance to next component of path */ + path.basename = tail; + + while (tail < end && *tail != '/') tail++; + *tail = '\0'; + + path.full.size = (tail - path.full.ptr); + path.is_dir = (tail == end) ? full_is_dir : true; + + /* update ignores for new path fragment */ + if (path.basename == path.path) + error = git_ignore__for_path(repo, path.path, &ignores); + else + error = git_ignore__push_dir(&ignores, path.basename); + if (error < 0) + break; + + /* first process builtins - success means path was found */ + if (ignore_lookup_in_rules( + &ignores.ign_internal->rules, &path, ignored)) + goto cleanup; + + /* next process files in the path */ + git_vector_foreach(&ignores.ign_path, i, file) { + if (ignore_lookup_in_rules(&file->rules, &path, ignored)) + goto cleanup; + } + + /* last process global ignores */ + git_vector_foreach(&ignores.ign_global, i, file) { + if (ignore_lookup_in_rules(&file->rules, &path, ignored)) + goto cleanup; + } + + /* if we found no rules before reaching the end, we're done */ + if (tail == end) + break; + + /* reinstate divider in path */ + *tail = '/'; + while (*tail == '/') tail++; + } + + *ignored = 0; + +cleanup: + git_attr_path__free(&path); + git_ignore__free(&ignores); + return error; +} + diff --git a/src/ignore.h b/src/ignore.h index 809d2edbd..5af8e8e7d 100644 --- a/src/ignore.h +++ b/src/ignore.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -9,6 +9,11 @@ #include "repository.h" #include "vector.h" +#include "attr_file.h" + +#define GIT_IGNORE_FILE ".gitignore" +#define GIT_IGNORE_FILE_INREPO "info/exclude" +#define GIT_IGNORE_FILE_XDG "ignore" /* The git_ignores structure maintains three sets of ignores: * - internal ignores @@ -23,6 +28,7 @@ typedef struct { git_attr_file *ign_internal; git_vector ign_path; git_vector ign_global; + unsigned int ignore_case:1; } git_ignores; extern int git_ignore__for_path(git_repository *repo, const char *path, git_ignores *ign); diff --git a/src/index.c b/src/index.c index f1ae9a710..6290ec4e8 100644 --- a/src/index.c +++ b/src/index.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -13,8 +13,12 @@ #include "tree.h" #include "tree-cache.h" #include "hash.h" +#include "iterator.h" +#include "pathspec.h" #include "git2/odb.h" +#include "git2/oid.h" #include "git2/blob.h" +#include "git2/config.h" #define entry_size(type,len) ((offsetof(type, path) + (len) + 8) & ~7) #define short_entry_size(len) entry_size(struct entry_short, len) @@ -79,94 +83,225 @@ struct entry_long { char path[1]; /* arbitrary length */ }; +struct entry_srch_key { + const char *path; + int stage; +}; + /* local declarations */ static size_t read_extension(git_index *index, const char *buffer, size_t buffer_size); static size_t read_entry(git_index_entry *dest, const void *buffer, size_t buffer_size); static int read_header(struct index_header *dest, const void *buffer); static int parse_index(git_index *index, const char *buffer, size_t buffer_size); -static int is_index_extended(git_index *index); +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; + const git_index_entry *entry = array_member; + int ret; + + ret = strcmp(srch_key->path, entry->path); + + if (ret == 0) + ret = srch_key->stage - index_entry_stage(entry); + + return ret; +} + +static int index_isrch(const void *key, const void *array_member) +{ + const struct entry_srch_key *srch_key = key; const git_index_entry *entry = array_member; + int ret; + + ret = strcasecmp(srch_key->path, entry->path); - return strcmp(key, entry->path); + if (ret == 0) + ret = srch_key->stage - index_entry_stage(entry); + + return ret; +} + +static int index_cmp_path(const void *a, const void *b) +{ + return strcmp((const char *)a, (const char *)b); +} + +static int index_icmp_path(const void *a, const void *b) +{ + return strcasecmp((const char *)a, (const char *)b); +} + +static int index_srch_path(const void *path, const void *array_member) +{ + const git_index_entry *entry = array_member; + + return strcmp((const char *)path, entry->path); +} + +static int index_isrch_path(const void *path, const void *array_member) +{ + const git_index_entry *entry = array_member; + + return strcasecmp((const char *)path, entry->path); } static int index_cmp(const void *a, const void *b) { + int diff; const git_index_entry *entry_a = a; const git_index_entry *entry_b = b; - return strcmp(entry_a->path, entry_b->path); + diff = strcmp(entry_a->path, entry_b->path); + + if (diff == 0) + diff = (index_entry_stage(entry_a) - index_entry_stage(entry_b)); + + return diff; } -static int unmerged_srch(const void *key, const void *array_member) +static int index_icmp(const void *a, const void *b) { - const git_index_entry_unmerged *entry = array_member; + int diff; + const git_index_entry *entry_a = a; + const git_index_entry *entry_b = b; - return strcmp(key, entry->path); + diff = strcasecmp(entry_a->path, entry_b->path); + + if (diff == 0) + diff = (index_entry_stage(entry_a) - index_entry_stage(entry_b)); + + return diff; +} + +static int reuc_srch(const void *key, const void *array_member) +{ + const git_index_reuc_entry *reuc = array_member; + + return strcmp(key, reuc->path); } -static int unmerged_cmp(const void *a, const void *b) +static int reuc_isrch(const void *key, const void *array_member) { - const git_index_entry_unmerged *info_a = a; - const git_index_entry_unmerged *info_b = b; + const git_index_reuc_entry *reuc = array_member; + + return strcasecmp(key, reuc->path); +} + +static int reuc_cmp(const void *a, const void *b) +{ + const git_index_reuc_entry *info_a = a; + const git_index_reuc_entry *info_b = b; return strcmp(info_a->path, info_b->path); } +static int reuc_icmp(const void *a, const void *b) +{ + const git_index_reuc_entry *info_a = a; + const git_index_reuc_entry *info_b = b; + + return strcasecmp(info_a->path, info_b->path); +} + static unsigned int index_create_mode(unsigned int mode) { if (S_ISLNK(mode)) return S_IFLNK; + if (S_ISDIR(mode) || (mode & S_IFMT) == (S_IFLNK | S_IFDIR)) return (S_IFLNK | S_IFDIR); + return S_IFREG | ((mode & 0100) ? 0755 : 0644); } +static unsigned int index_merge_mode( + git_index *index, git_index_entry *existing, unsigned int mode) +{ + if (index->no_symlinks && S_ISREG(mode) && + existing && S_ISLNK(existing->mode)) + return existing->mode; + + if (index->distrust_filemode && S_ISREG(mode)) + return (existing && S_ISREG(existing->mode)) ? + existing->mode : index_create_mode(0666); + + return index_create_mode(mode); +} + +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_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_sort(&index->reuc); +} + int git_index_open(git_index **index_out, const char *index_path) { git_index *index; - assert(index_out && index_path); + assert(index_out); index = git__calloc(1, sizeof(git_index)); GITERR_CHECK_ALLOC(index); - index->index_file_path = git__strdup(index_path); - GITERR_CHECK_ALLOC(index->index_file_path); + if (index_path != NULL) { + index->index_file_path = git__strdup(index_path); + GITERR_CHECK_ALLOC(index->index_file_path); + + /* Check if index file is stored on disk already */ + if (git_path_exists(index->index_file_path) == true) + index->on_disk = 1; + } - if (git_vector_init(&index->entries, 32, index_cmp) < 0) + if (git_vector_init(&index->entries, 32, index_cmp) < 0 || + git_vector_init(&index->reuc, 32, reuc_cmp) < 0) return -1; - /* Check if index file is stored on disk already */ - if (git_path_exists(index->index_file_path) == true) - index->on_disk = 1; + index->entries_cmp_path = index_cmp_path; + index->entries_search = index_srch; + index->entries_search_path = index_srch_path; + index->reuc_search = reuc_srch; *index_out = index; GIT_REFCOUNT_INC(index); - return git_index_read(index); + + return (index_path != NULL) ? git_index_read(index) : 0; } -static void index_free(git_index *index) +int git_index_new(git_index **out) { - git_index_entry *e; - unsigned int i; + return git_index_open(out, NULL); +} +static void index_free(git_index *index) +{ git_index_clear(index); - git_vector_foreach(&index->entries, i, e) { - index_entry_free(e); - } git_vector_free(&index->entries); - git_vector_foreach(&index->unmerged, i, e) { - index_entry_free(e); - } - git_vector_free(&index->unmerged); + git_vector_free(&index->reuc); git__free(index->index_file_path); git__free(index); @@ -182,7 +317,7 @@ void git_index_free(git_index *index) void git_index_clear(git_index *index) { - unsigned int i; + size_t i; assert(index); @@ -192,29 +327,75 @@ void git_index_clear(git_index *index) git__free(e->path); git__free(e); } - - for (i = 0; i < index->unmerged.length; ++i) { - git_index_entry_unmerged *e; - e = git_vector_get(&index->unmerged, i); - git__free(e->path); - git__free(e); - } - git_vector_clear(&index->entries); - git_vector_clear(&index->unmerged); - index->last_modified = 0; + + git_index_reuc_clear(index); + + git_futils_filestamp_set(&index->stamp, NULL); git_tree_cache_free(index->tree); index->tree = NULL; } +static int create_index_error(int error, const char *msg) +{ + giterr_set(GITERR_INDEX, msg); + return error; +} + +int git_index_set_caps(git_index *index, unsigned int caps) +{ + int old_ignore_case; + + assert(index); + + old_ignore_case = index->ignore_case; + + if (caps == GIT_INDEXCAP_FROM_OWNER) { + git_config *cfg; + 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 (git_config_get_bool(&val, cfg, "core.ignorecase") == 0) + index->ignore_case = (val != 0); + if (git_config_get_bool(&val, cfg, "core.filemode") == 0) + index->distrust_filemode = (val == 0); + if (git_config_get_bool(&val, cfg, "core.symlinks") == 0) + index->no_symlinks = (val == 0); + } + else { + index->ignore_case = ((caps & GIT_INDEXCAP_IGNORE_CASE) != 0); + index->distrust_filemode = ((caps & GIT_INDEXCAP_NO_FILEMODE) != 0); + index->no_symlinks = ((caps & GIT_INDEXCAP_NO_SYMLINKS) != 0); + } + + if (old_ignore_case != index->ignore_case) { + git_index__set_ignore_case(index, index->ignore_case); + } + + return 0; +} + +unsigned int git_index_caps(const git_index *index) +{ + return ((index->ignore_case ? GIT_INDEXCAP_IGNORE_CASE : 0) | + (index->distrust_filemode ? GIT_INDEXCAP_NO_FILEMODE : 0) | + (index->no_symlinks ? GIT_INDEXCAP_NO_SYMLINKS : 0)); +} + int git_index_read(git_index *index) { - int error, updated; + int error = 0, updated; git_buf buffer = GIT_BUF_INIT; - time_t mtime; + git_futils_filestamp stamp = {0}; - assert(index->index_file_path); + if (!index->index_file_path) + return create_index_error(-1, + "Failed to read index: The index is in-memory only"); if (!index->on_disk || git_path_exists(index->index_file_path) == false) { git_index_clear(index); @@ -222,33 +403,35 @@ int git_index_read(git_index *index) return 0; } - /* We don't want to update the mtime if we fail to parse the index */ - mtime = index->last_modified; - error = git_futils_readbuffer_updated( - &buffer, index->index_file_path, &mtime, &updated); + updated = git_futils_filestamp_check(&stamp, index->index_file_path); + if (updated <= 0) + return updated; + + error = git_futils_readbuffer(&buffer, index->index_file_path); if (error < 0) return error; - if (updated) { - git_index_clear(index); - error = parse_index(index, buffer.ptr, buffer.size); - - if (!error) - index->last_modified = mtime; + git_index_clear(index); + error = parse_index(index, buffer.ptr, buffer.size); - git_buf_free(&buffer); - } + if (!error) + git_futils_filestamp_set(&index->stamp, &stamp); + git_buf_free(&buffer); return error; } int git_index_write(git_index *index) { git_filebuf file = GIT_FILEBUF_INIT; - struct stat indexst; int error; + if (!index->index_file_path) + return create_index_error(-1, + "Failed to read index: The index is in-memory only"); + git_vector_sort(&index->entries); + git_vector_sort(&index->reuc); if ((error = git_filebuf_open( &file, index->index_file_path, GIT_FILEBUF_HASH_CONTENTS)) < 0) @@ -262,33 +445,65 @@ int git_index_write(git_index *index) if ((error = git_filebuf_commit(&file, GIT_INDEX_FILE_MODE)) < 0) return error; - if (p_stat(index->index_file_path, &indexst) == 0) { - index->last_modified = indexst.st_mtime; - index->on_disk = 1; - } + error = git_futils_filestamp_check(&index->stamp, index->index_file_path); + if (error < 0) + return error; + index->on_disk = 1; return 0; } -unsigned int git_index_entrycount(git_index *index) +int git_index_write_tree(git_oid *oid, git_index *index) +{ + git_repository *repo; + + assert(oid && index); + + repo = INDEX_OWNER(index); + + if (repo == NULL) + return create_index_error(-1, "Failed to write tree. " + "The index file is not backed up by an existing repository"); + + return git_tree__write_index(oid, index, repo); +} + +int git_index_write_tree_to(git_oid *oid, git_index *index, git_repository *repo) +{ + assert(oid && index && repo); + return git_tree__write_index(oid, index, repo); +} + +size_t git_index_entrycount(const git_index *index) { assert(index); return index->entries.length; } -unsigned int git_index_entrycount_unmerged(git_index *index) +const git_index_entry *git_index_get_byindex( + git_index *index, size_t n) { assert(index); - return index->unmerged.length; + git_vector_sort(&index->entries); + return git_vector_get(&index->entries, n); } -git_index_entry *git_index_get(git_index *index, unsigned int n) +const git_index_entry *git_index_get_bypath( + git_index *index, const char *path, int stage) { + size_t pos; + + assert(index); + git_vector_sort(&index->entries); - return git_vector_get(&index->entries, n); + + if (index_find(&pos, index, path, stage) < 0) + return NULL; + + return git_index_get_byindex(index, pos); } -void git_index__init_entry_from_stat(struct stat *st, git_index_entry *entry) +void git_index_entry__init_from_stat(git_index_entry *entry, struct stat *st) { entry->ctime.seconds = (git_time_t)st->st_ctime; entry->mtime.seconds = (git_time_t)st->st_mtime; @@ -302,7 +517,23 @@ void git_index__init_entry_from_stat(struct stat *st, git_index_entry *entry) entry->file_size = st->st_size; } -static int index_entry_init(git_index_entry **entry_out, git_index *index, const char *rel_path, int stage) +int git_index_entry__cmp(const void *a, const void *b) +{ + const git_index_entry *entry_a = a; + const git_index_entry *entry_b = b; + + return strcmp(entry_a->path, entry_b->path); +} + +int git_index_entry__cmp_icase(const void *a, const void *b) +{ + const git_index_entry *entry_a = a; + const git_index_entry *entry_b = 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) { git_index_entry *entry = NULL; struct stat st; @@ -311,15 +542,16 @@ static int index_entry_init(git_index_entry **entry_out, git_index *index, const git_buf full_path = GIT_BUF_INIT; int error; - assert(stage >= 0 && stage <= 3); + if (INDEX_OWNER(index) == NULL) + return create_index_error(-1, + "Could not initialize index entry. " + "Index is not backed up by an existing repository."); - if (INDEX_OWNER(index) == NULL || - (workdir = git_repository_workdir(INDEX_OWNER(index))) == NULL) - { - giterr_set(GITERR_INDEX, + workdir = git_repository_workdir(INDEX_OWNER(index)); + + if (!workdir) + return create_index_error(GIT_EBAREREPO, "Could not initialize index entry. Repository is bare"); - return -1; - } if ((error = git_buf_joinpath(&full_path, workdir, rel_path)) < 0) return error; @@ -336,16 +568,15 @@ static int index_entry_init(git_index_entry **entry_out, git_index *index, const */ /* write the blob to disk and get the oid */ - if ((error = git_blob_create_fromfile(&oid, INDEX_OWNER(index), rel_path)) < 0) + if ((error = git_blob_create_fromworkdir(&oid, INDEX_OWNER(index), rel_path)) < 0) return error; entry = git__calloc(1, sizeof(git_index_entry)); GITERR_CHECK_ALLOC(entry); - git_index__init_entry_from_stat(&st, entry); + git_index_entry__init_from_stat(entry, &st); entry->oid = oid; - entry->flags |= (stage << GIT_IDXENTRY_STAGESHIFT); entry->path = git__strdup(rel_path); GITERR_CHECK_ALLOC(entry->path); @@ -353,6 +584,46 @@ static int index_entry_init(git_index_entry **entry_out, git_index *index, const return 0; } +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) +{ + git_index_reuc_entry *reuc = NULL; + + assert(reuc_out && path); + + *reuc_out = NULL; + + reuc = git__calloc(1, sizeof(git_index_reuc_entry)); + GITERR_CHECK_ALLOC(reuc); + + reuc->path = git__strdup(path); + if (reuc->path == NULL) + return -1; + + if ((reuc->mode[0] = ancestor_mode) > 0) + git_oid_cpy(&reuc->oid[0], ancestor_oid); + + if ((reuc->mode[1] = our_mode) > 0) + git_oid_cpy(&reuc->oid[1], our_oid); + + if ((reuc->mode[2] = their_mode) > 0) + git_oid_cpy(&reuc->oid[2], their_oid); + + *reuc_out = reuc; + 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; @@ -381,9 +652,8 @@ static void index_entry_free(git_index_entry *entry) static int index_insert(git_index *index, git_index_entry *entry, int replace) { - size_t path_length; - int position; - git_index_entry **entry_array; + size_t path_length, position; + git_index_entry **existing = NULL; assert(index && entry && entry->path != NULL); @@ -395,62 +665,95 @@ static int index_insert(git_index *index, git_index_entry *entry, int replace) if (path_length < GIT_IDXENTRY_NAMEMASK) entry->flags |= path_length & GIT_IDXENTRY_NAMEMASK; else - entry->flags |= GIT_IDXENTRY_NAMEMASK;; - - /* - * replacing is not requested: just insert entry at the end; - * the index is no longer sorted - */ - if (!replace) - return git_vector_insert(&index->entries, entry); + entry->flags |= GIT_IDXENTRY_NAMEMASK; /* look if an entry with this path already exists */ - position = git_index_find(index, entry->path); + if (!index_find(&position, index, entry->path, index_entry_stage(entry))) { + existing = (git_index_entry **)&index->entries.contents[position]; + + /* update filemode to existing values if stat is not trusted */ + entry->mode = index_merge_mode(index, *existing, entry->mode); + } - /* - * if no entry exists add the entry at the end; - * the index is no longer sorted + /* if replacing is not requested or no existing entry exists, just + * insert entry at the end; the index is no longer sorted */ - if (position == GIT_ENOTFOUND) + if (!replace || !existing) return git_vector_insert(&index->entries, entry); /* exists, replace it */ - entry_array = (git_index_entry **) index->entries.contents; - git__free(entry_array[position]->path); - git__free(entry_array[position]); - entry_array[position] = entry; + git__free((*existing)->path); + git__free(*existing); + *existing = entry; return 0; } -static int index_add(git_index *index, const char *path, int stage, int replace) +static int index_conflict_to_reuc(git_index *index, const char *path) { - git_index_entry *entry = NULL; + git_index_entry *conflict_entries[3]; + int ancestor_mode, our_mode, their_mode; + git_oid *ancestor_oid, *our_oid, *their_oid; int ret; - if ((ret = index_entry_init(&entry, index, path, stage)) < 0 || - (ret = index_insert(index, entry, replace)) < 0) - { - index_entry_free(entry); + if ((ret = git_index_conflict_get(&conflict_entries[0], + &conflict_entries[1], &conflict_entries[2], index, path)) < 0) return ret; - } - git_tree_cache_invalidate_path(index->tree, entry->path); - return 0; + ancestor_mode = conflict_entries[0] == NULL ? 0 : conflict_entries[0]->mode; + our_mode = conflict_entries[1] == NULL ? 0 : conflict_entries[1]->mode; + their_mode = conflict_entries[2] == NULL ? 0 : conflict_entries[2]->mode; + + ancestor_oid = conflict_entries[0] == NULL ? NULL : &conflict_entries[0]->oid; + our_oid = conflict_entries[1] == NULL ? NULL : &conflict_entries[1]->oid; + their_oid = conflict_entries[2] == NULL ? NULL : &conflict_entries[2]->oid; + + if ((ret = git_index_reuc_add(index, path, ancestor_mode, ancestor_oid, + our_mode, our_oid, their_mode, their_oid)) >= 0) + ret = git_index_conflict_remove(index, path); + + return ret; } -int git_index_add(git_index *index, const char *path, int stage) +int git_index_add_bypath(git_index *index, const char *path) { - return index_add(index, path, stage, 1); + git_index_entry *entry = NULL; + int ret; + + assert(index && path); + + if ((ret = index_entry_init(&entry, index, path)) < 0 || + (ret = index_insert(index, entry, 1)) < 0) + goto on_error; + + /* Adding implies conflict was resolved, move conflict entries to REUC */ + if ((ret = index_conflict_to_reuc(index, path)) < 0 && ret != GIT_ENOTFOUND) + goto on_error; + + git_tree_cache_invalidate_path(index->tree, entry->path); + return 0; + +on_error: + index_entry_free(entry); + return ret; } -int git_index_append(git_index *index, const char *path, int stage) +int git_index_remove_bypath(git_index *index, const char *path) { - return index_add(index, path, stage, 0); + int ret; + + assert(index && path); + + if (((ret = git_index_remove(index, path, 0)) < 0 && + ret != GIT_ENOTFOUND) || + ((ret = index_conflict_to_reuc(index, path)) < 0 && + ret != GIT_ENOTFOUND)) + return ret; + + return 0; } -static int index_add2( - git_index *index, const git_index_entry *source_entry, int replace) +int git_index_add(git_index *index, const git_index_entry *source_entry) { git_index_entry *entry = NULL; int ret; @@ -459,7 +762,7 @@ static int index_add2( if (entry == NULL) return -1; - if ((ret = index_insert(index, entry, replace)) < 0) { + if ((ret = index_insert(index, entry, 1)) < 0) { index_entry_free(entry); return ret; } @@ -468,28 +771,22 @@ static int index_add2( return 0; } -int git_index_add2(git_index *index, const git_index_entry *source_entry) -{ - return index_add2(index, source_entry, 1); -} - -int git_index_append2(git_index *index, const git_index_entry *source_entry) -{ - return index_add2(index, source_entry, 1); -} - -int git_index_remove(git_index *index, int position) +int git_index_remove(git_index *index, const char *path, int stage) { + size_t position; int error; git_index_entry *entry; git_vector_sort(&index->entries); + if (index_find(&position, index, path, stage) < 0) + return GIT_ENOTFOUND; + entry = git_vector_get(&index->entries, position); if (entry != NULL) git_tree_cache_invalidate_path(index->tree, entry->path); - error = git_vector_remove(&index->entries, (unsigned int)position); + error = git_vector_remove(&index->entries, position); if (!error) index_entry_free(entry); @@ -497,45 +794,362 @@ int git_index_remove(git_index *index, int position) return error; } -int git_index_find(git_index *index, const char *path) +int git_index_remove_directory(git_index *index, const char *dir, int stage) { - return git_vector_bsearch2(&index->entries, index_srch, path); + git_buf pfx = GIT_BUF_INIT; + int error = 0; + size_t pos; + git_index_entry *entry; + + if (git_buf_sets(&pfx, dir) < 0 || git_path_to_dir(&pfx) < 0) + return -1; + + git_vector_sort(&index->entries); + + pos = git_index__prefix_position(index, pfx.ptr); + + while (1) { + entry = git_vector_get(&index->entries, pos); + if (!entry || git__prefixcmp(entry->path, pfx.ptr) != 0) + break; + + if (index_entry_stage(entry) != stage) { + ++pos; + continue; + } + + git_tree_cache_invalidate_path(index->tree, entry->path); + + if ((error = git_vector_remove(&index->entries, pos)) < 0) + break; + index_entry_free(entry); + + /* removed entry at 'pos' so we don't need to increment it */ + } + + git_buf_free(&pfx); + + return error; } -unsigned int git_index__prefix_position(git_index *index, const char *path) +static int index_find(size_t *at_pos, git_index *index, const char *path, int stage) { - unsigned int pos; + struct entry_srch_key srch_key; - git_vector_bsearch3(&pos, &index->entries, index_srch, path); + assert(path); + + srch_key.path = path; + srch_key.stage = stage; + + 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) +{ + size_t pos; + + assert(index && path); + + if (git_vector_bsearch2(&pos, &index->entries, index->entries_search_path, path) < 0) { + giterr_set(GITERR_INDEX, "Index does not contain %s", path); + return GIT_ENOTFOUND; + } + + /* Since our binary search only looked at path, we may be in the + * middle of a list of stages. + */ + while (pos > 0) { + const git_index_entry *prev = git_vector_get(&index->entries, pos-1); + + if (index->entries_cmp_path(prev->path, path) != 0) + break; + + --pos; + } + + if (at_pos) + *at_pos = pos; + + return 0; +} + +size_t git_index__prefix_position(git_index *index, const char *path) +{ + struct entry_srch_key srch_key; + size_t pos; + + srch_key.path = path; + srch_key.stage = 0; + + git_vector_sort(&index->entries); + git_vector_bsearch2( + &pos, &index->entries, index->entries_search, &srch_key); return pos; } -void git_index_uniq(git_index *index) +int git_index_conflict_add(git_index *index, + const git_index_entry *ancestor_entry, + const git_index_entry *our_entry, + const git_index_entry *their_entry) { - git_vector_uniq(&index->entries); + git_index_entry *entries[3] = { 0 }; + unsigned short i; + int ret = 0; + + assert (index); + + if ((ancestor_entry != NULL && (entries[0] = index_entry_dup(ancestor_entry)) == NULL) || + (our_entry != NULL && (entries[1] = index_entry_dup(our_entry)) == NULL) || + (their_entry != NULL && (entries[2] = index_entry_dup(their_entry)) == NULL)) + return -1; + + for (i = 0; i < 3; i++) { + if (entries[i] == NULL) + continue; + + /* Make sure stage is correct */ + entries[i]->flags = (entries[i]->flags & ~GIT_IDXENTRY_STAGEMASK) | + ((i+1) << GIT_IDXENTRY_STAGESHIFT); + + if ((ret = index_insert(index, entries[i], 1)) < 0) + goto on_error; + } + + return 0; + +on_error: + for (i = 0; i < 3; i++) { + if (entries[i] != NULL) + index_entry_free(entries[i]); + } + + return ret; } -const git_index_entry_unmerged *git_index_get_unmerged_bypath( +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) { - int pos; + size_t pos, posmax; + int stage; + git_index_entry *conflict_entry; + int error = GIT_ENOTFOUND; + + 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; + + for (posmax = git_index_entrycount(index); pos < posmax; ++pos) { + + conflict_entry = git_vector_get(&index->entries, pos); + + if (index->entries_cmp_path(conflict_entry->path, path) != 0) + break; + + stage = index_entry_stage(conflict_entry); + + switch (stage) { + case 3: + *their_out = conflict_entry; + error = 0; + break; + case 2: + *our_out = conflict_entry; + error = 0; + break; + case 1: + *ancestor_out = conflict_entry; + error = 0; + break; + default: + break; + }; + } + + return error; +} + +int git_index_conflict_remove(git_index *index, const char *path) +{ + size_t pos, posmax; + git_index_entry *conflict_entry; + int error = 0; + assert(index && path); - if (!index->unmerged.length) + if (git_index_find(&pos, index, path) < 0) + return GIT_ENOTFOUND; + + posmax = git_index_entrycount(index); + + while (pos < posmax) { + conflict_entry = git_vector_get(&index->entries, pos); + + if (index->entries_cmp_path(conflict_entry->path, path) != 0) + break; + + if (index_entry_stage(conflict_entry) == 0) { + pos++; + continue; + } + + if ((error = git_vector_remove(&index->entries, pos)) < 0) + return error; + + index_entry_free(conflict_entry); + posmax--; + } + + return 0; +} + +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) { + index_entry_free(entry); + return 1; + } + + return 0; +} + +void git_index_conflict_cleanup(git_index *index) +{ + assert(index); + git_vector_remove_matching(&index->entries, index_conflicts_match); +} + +int git_index_has_conflicts(const git_index *index) +{ + size_t i; + git_index_entry *entry; + + assert(index); + + git_vector_foreach(&index->entries, i, entry) { + if (index_entry_stage(entry) > 0) + return 1; + } + + return 0; +} + +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) +{ + git_index_reuc_entry **existing = NULL; + size_t position; + + assert(index && reuc && reuc->path != NULL); + + if (!git_index_reuc_find(&position, index, reuc->path)) + existing = (git_index_reuc_entry **)&index->reuc.contents[position]; + + if (!replace || !existing) + return git_vector_insert(&index->reuc, reuc); + + /* exists, replace it */ + git__free((*existing)->path); + git__free(*existing); + *existing = reuc; + + return 0; +} + +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) +{ + git_index_reuc_entry *reuc = NULL; + int error = 0; + + assert(index && path); + + if ((error = index_entry_reuc_init(&reuc, path, ancestor_mode, ancestor_oid, our_mode, our_oid, their_mode, their_oid)) < 0 || + (error = index_reuc_insert(index, reuc, 1)) < 0) + { + index_entry_reuc_free(reuc); + return error; + } + + return error; +} + +int git_index_reuc_find(size_t *at_pos, git_index *index, const char *path) +{ + return git_vector_bsearch2(at_pos, &index->reuc, index->reuc_search, path); +} + +const git_index_reuc_entry *git_index_reuc_get_bypath( + git_index *index, const char *path) +{ + size_t pos; + assert(index && path); + + if (!index->reuc.length) return NULL; - if ((pos = git_vector_bsearch2(&index->unmerged, unmerged_srch, path)) < 0) + git_vector_sort(&index->reuc); + + if (git_index_reuc_find(&pos, index, path) < 0) return NULL; - return git_vector_get(&index->unmerged, pos); + return git_vector_get(&index->reuc, pos); } -const git_index_entry_unmerged *git_index_get_unmerged_byindex( - git_index *index, unsigned int n) +const git_index_reuc_entry *git_index_reuc_get_byindex( + git_index *index, size_t n) { assert(index); - return git_vector_get(&index->unmerged, n); + + git_vector_sort(&index->reuc); + return git_vector_get(&index->reuc, n); +} + +int git_index_reuc_remove(git_index *index, size_t position) +{ + int error; + git_index_reuc_entry *reuc; + + git_vector_sort(&index->reuc); + + reuc = git_vector_get(&index->reuc, position); + error = git_vector_remove(&index->reuc, position); + + if (!error) + index_entry_reuc_free(reuc); + + return error; +} + +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); + } + + git_vector_clear(&index->reuc); } static int index_error_invalid(const char *message) @@ -544,26 +1158,27 @@ static int index_error_invalid(const char *message) return -1; } -static int read_unmerged(git_index *index, const char *buffer, size_t size) +static int read_reuc(git_index *index, const char *buffer, size_t size) { const char *endptr; size_t len; int i; - if (git_vector_init(&index->unmerged, 16, unmerged_cmp) < 0) + /* 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) return -1; while (size) { - git_index_entry_unmerged *lost; + git_index_reuc_entry *lost; len = strlen(buffer) + 1; if (size <= len) - return index_error_invalid("reading unmerged entries"); + return index_error_invalid("reading reuc entries"); - lost = git__malloc(sizeof(git_index_entry_unmerged)); + lost = git__malloc(sizeof(git_index_reuc_entry)); GITERR_CHECK_ALLOC(lost); - if (git_vector_insert(&index->unmerged, lost) < 0) + if (git_vector_insert(&index->reuc, lost) < 0) return -1; /* read NUL-terminated pathname for entry */ @@ -580,13 +1195,13 @@ static int read_unmerged(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) - return index_error_invalid("reading unmerged entry stage"); + return index_error_invalid("reading reuc entry stage"); lost->mode[i] = tmp; len = (endptr + 1) - buffer; if (size <= len) - return index_error_invalid("reading unmerged entry stage"); + return index_error_invalid("reading reuc entry stage"); size -= len; buffer += len; @@ -597,7 +1212,7 @@ static int read_unmerged(git_index *index, const char *buffer, size_t size) if (!lost->mode[i]) continue; if (size < 20) - return index_error_invalid("reading unmerged entry oid"); + return index_error_invalid("reading reuc entry oid"); git_oid_fromraw(&lost->oid[i], (const unsigned char *) buffer); size -= 20; @@ -605,6 +1220,9 @@ static int read_unmerged(git_index *index, const char *buffer, size_t size) } } + /* entries are guaranteed to be sorted on-disk */ + index->reuc.sorted = 1; + return 0; } @@ -710,7 +1328,7 @@ static size_t read_extension(git_index *index, const char *buffer, size_t buffer if (git_tree_cache_read(&index->tree, buffer + 8, dest.extension_size) < 0) return 0; } else if (memcmp(dest.signature, INDEX_EXT_UNMERGED_SIG, 4) == 0) { - if (read_unmerged(index, buffer + 8, dest.extension_size) < 0) + if (read_reuc(index, buffer + 8, dest.extension_size) < 0) return 0; } /* else, unsupported extension. We cannot parse this, but we can skip @@ -799,15 +1417,16 @@ static int parse_index(git_index *index, const char *buffer, size_t buffer_size) #undef seek_forward - /* force sorting in the vector: the entries are - * assured to be sorted on the index */ - index->entries.sorted = 1; + /* Entries are stored case-sensitively on disk. */ + index->entries.sorted = !index->ignore_case; + git_vector_sort(&index->entries); + return 0; } -static int is_index_extended(git_index *index) +static bool is_index_extended(git_index *index) { - unsigned int i, extended; + size_t i, extended; git_index_entry *entry; extended = 0; @@ -820,7 +1439,7 @@ static int is_index_extended(git_index *index) } } - return extended; + return (extended > 0); } static int write_disk_entry(git_filebuf *file, git_index_entry *entry) @@ -885,25 +1504,98 @@ static int write_disk_entry(git_filebuf *file, git_index_entry *entry) static int write_entries(git_index *index, git_filebuf *file) { - unsigned int i; + int error = 0; + size_t i; + git_vector case_sorted; + git_index_entry *entry; + git_vector *out = &index->entries; + + /* If index->entries is sorted case-insensitively, then we need + * to re-sort it case-sensitively before writing */ + if (index->ignore_case) { + git_vector_dup(&case_sorted, &index->entries, index_cmp); + git_vector_sort(&case_sorted); + out = &case_sorted; + } - for (i = 0; i < index->entries.length; ++i) { - git_index_entry *entry; - entry = git_vector_get(&index->entries, i); - if (write_disk_entry(file, entry) < 0) - return -1; + git_vector_foreach(out, i, entry) + if ((error = write_disk_entry(file, entry)) < 0) + break; + + if (index->ignore_case) + git_vector_free(&case_sorted); + + return error; +} + +static int write_extension(git_filebuf *file, struct index_extension *header, git_buf *data) +{ + struct index_extension ondisk; + int error = 0; + + memset(&ondisk, 0x0, sizeof(struct index_extension)); + memcpy(&ondisk, header, 4); + ondisk.extension_size = htonl(header->extension_size); + + if ((error = git_filebuf_write(file, &ondisk, sizeof(struct index_extension))) == 0) + error = git_filebuf_write(file, data->ptr, data->size); + + return error; +} + +static int create_reuc_extension_data(git_buf *reuc_buf, git_index_reuc_entry *reuc) +{ + int i; + int error = 0; + + if ((error = git_buf_put(reuc_buf, reuc->path, strlen(reuc->path) + 1)) < 0) + return error; + + for (i = 0; i < 3; i++) { + if ((error = git_buf_printf(reuc_buf, "%o", reuc->mode[i])) < 0 || + (error = git_buf_put(reuc_buf, "\0", 1)) < 0) + return error; + } + + for (i = 0; i < 3; i++) { + if (reuc->mode[i] && (error = git_buf_put(reuc_buf, (char *)&reuc->oid[i].id, GIT_OID_RAWSZ)) < 0) + return error; } return 0; } +static int write_reuc_extension(git_index *index, git_filebuf *file) +{ + git_buf reuc_buf = GIT_BUF_INIT; + git_vector *out = &index->reuc; + git_index_reuc_entry *reuc; + struct index_extension extension; + size_t i; + int error = 0; + + git_vector_foreach(out, i, reuc) { + if ((error = create_reuc_extension_data(&reuc_buf, reuc)) < 0) + goto done; + } + + memset(&extension, 0x0, sizeof(struct index_extension)); + memcpy(&extension.signature, INDEX_EXT_UNMERGED_SIG, 4); + extension.extension_size = (uint32_t)reuc_buf.size; + + error = write_extension(file, &extension, &reuc_buf); + + git_buf_free(&reuc_buf); + +done: + return error; +} + static int write_index(git_index *index, git_filebuf *file) { git_oid hash_final; - struct index_header header; - - int is_extended; + bool is_extended; assert(index && file); @@ -911,7 +1603,7 @@ static int write_index(git_index *index, git_filebuf *file) header.signature = htonl(INDEX_HEADER_SIG); header.version = htonl(is_extended ? INDEX_VERSION_NUMBER_EXT : INDEX_VERSION_NUMBER); - header.entry_count = htonl(index->entries.length); + header.entry_count = htonl((uint32_t)index->entries.length); if (git_filebuf_write(file, &header, sizeof(struct index_header)) < 0) return -1; @@ -919,7 +1611,11 @@ static int write_index(git_index *index, git_filebuf *file) if (write_entries(index, file) < 0) return -1; - /* TODO: write extensions (tree cache) */ + /* TODO: write tree cache extension */ + + /* write the reuc extension */ + if (index->reuc.length > 0 && write_reuc_extension(index, file) < 0) + return -1; /* get out the hash for all the contents we've appended to the file */ git_filebuf_hash(&hash_final, file); @@ -930,12 +1626,17 @@ static int write_index(git_index *index, git_filebuf *file) int git_index_entry_stage(const git_index_entry *entry) { - return (entry->flags & GIT_IDXENTRY_STAGEMASK) >> GIT_IDXENTRY_STAGESHIFT; + return index_entry_stage(entry); } -static int read_tree_cb(const char *root, git_tree_entry *tentry, void *data) +typedef struct read_tree_data { + git_index *index; + git_transfer_progress *stats; +} read_tree_data; + +static int read_tree_cb(const char *root, const git_tree_entry *tentry, void *data) { - git_index *index = data; + git_index *index = (git_index *)data; git_index_entry *entry = NULL; git_buf path = GIT_BUF_INIT; @@ -950,10 +1651,16 @@ static int read_tree_cb(const char *root, git_tree_entry *tentry, void *data) entry->mode = tentry->attr; entry->oid = tentry->oid; + + if (path.size < GIT_IDXENTRY_NAMEMASK) + entry->flags = path.size & GIT_IDXENTRY_NAMEMASK; + else + entry->flags = GIT_IDXENTRY_NAMEMASK; + entry->path = git_buf_detach(&path); git_buf_free(&path); - if (index_insert(index, entry, 0) < 0) { + if (git_vector_insert(&index->entries, entry) < 0) { index_entry_free(entry); return -1; } @@ -961,9 +1668,14 @@ static int read_tree_cb(const char *root, git_tree_entry *tentry, void *data) return 0; } -int git_index_read_tree(git_index *index, git_tree *tree) +int git_index_read_tree(git_index *index, const git_tree *tree) { git_index_clear(index); - return git_tree_walk(tree, read_tree_cb, GIT_TREEWALK_POST, index); + return git_tree_walk(tree, GIT_TREEWALK_POST, read_tree_cb, index); +} + +git_repository *git_index_owner(const git_index *index) +{ + return INDEX_OWNER(index); } diff --git a/src/index.h b/src/index.h index 8515f4fcb..9498907b6 100644 --- a/src/index.h +++ b/src/index.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -22,17 +22,32 @@ struct git_index { char *index_file_path; - time_t last_modified; + git_futils_filestamp stamp; git_vector entries; unsigned int on_disk:1; + + unsigned int ignore_case:1; + unsigned int distrust_filemode:1; + unsigned int no_symlinks:1; + git_tree_cache *tree; - git_vector unmerged; + git_vector reuc; + + git_vector_cmp entries_cmp_path; + git_vector_cmp entries_search; + git_vector_cmp entries_search_path; + git_vector_cmp reuc_search; }; -extern void git_index__init_entry_from_stat(struct stat *st, git_index_entry *entry); +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 unsigned int git_index__prefix_position(git_index *index, const char *path); +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 6f735e651..2cfbd3a5a 100644 --- a/src/indexer.c +++ b/src/indexer.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -17,7 +17,7 @@ #include "posix.h" #include "pack.h" #include "filebuf.h" -#include "sha1.h" +#include "oidmap.h" #define UINT31_MAX (0x7FFFFFFF) @@ -28,39 +28,32 @@ struct entry { uint64_t offset_long; }; -struct git_indexer { - struct git_pack_file *pack; - size_t nr_objects; - git_vector objects; - git_filebuf file; - unsigned int fanout[256]; - git_oid hash; -}; - struct git_indexer_stream { unsigned int parsed_header :1, - opened_pack; + opened_pack :1, + have_stream :1, + have_delta :1; struct git_pack_file *pack; git_filebuf pack_file; - git_filebuf index_file; git_off_t off; + git_off_t entry_start; + git_packfile_stream stream; size_t nr_objects; git_vector objects; git_vector deltas; unsigned int fanout[256]; + git_hash_ctx hash_ctx; git_oid hash; + git_transfer_progress_callback progress_cb; + void *progress_payload; + char objbuf[8*1024]; }; struct delta_info { git_off_t delta_off; }; -const git_oid *git_indexer_hash(git_indexer *idx) -{ - return &idx->hash; -} - -const git_oid *git_indexer_stream_hash(git_indexer_stream *idx) +const git_oid *git_indexer_stream_hash(const git_indexer_stream *idx) { return &idx->hash; } @@ -130,23 +123,21 @@ static int objects_cmp(const void *a, const void *b) return git_oid_cmp(&entrya->oid, &entryb->oid); } -static int cache_cmp(const void *a, const void *b) -{ - const struct git_pack_entry *ea = a; - const struct git_pack_entry *eb = b; - - return git_oid_cmp(&ea->sha1, &eb->sha1); -} - -int git_indexer_stream_new(git_indexer_stream **out, const char *prefix) +int git_indexer_stream_new( + git_indexer_stream **out, + const char *prefix, + git_transfer_progress_callback progress_cb, + void *progress_payload) { git_indexer_stream *idx; git_buf path = GIT_BUF_INIT; - static const char suff[] = "/objects/pack/pack-received"; + static const char suff[] = "/pack"; int error; idx = git__calloc(1, sizeof(git_indexer_stream)); GITERR_CHECK_ALLOC(idx); + idx->progress_cb = progress_cb; + idx->progress_payload = progress_payload; error = git_buf_joinpath(&path, prefix, suff); if (error < 0) @@ -171,69 +162,171 @@ cleanup: /* Try to store the delta so we can try to resolve it later */ static int store_delta(git_indexer_stream *idx) { - git_otype type; - git_mwindow *w = NULL; - git_mwindow_file *mwf = &idx->pack->mwf; - git_off_t entry_start = idx->off; struct delta_info *delta; - size_t entry_size; - git_rawobj obj; - int error; - /* - * ref-delta objects can refer to object that we haven't - * found yet, so give it another opportunity - */ - if (git_packfile_unpack_header(&entry_size, &type, mwf, &w, &idx->off) < 0) + delta = git__calloc(1, sizeof(struct delta_info)); + GITERR_CHECK_ALLOC(delta); + delta->delta_off = idx->entry_start; + + if (git_vector_insert(&idx->deltas, delta) < 0) return -1; - git_mwindow_close(&w); + return 0; +} - /* If it's not a delta, mark it as failure, we can't do anything with it */ - if (type != GIT_OBJ_REF_DELTA && type != GIT_OBJ_OFS_DELTA) - return -1; +static void hash_header(git_hash_ctx *ctx, git_off_t len, git_otype type) +{ + char buffer[64]; + size_t hdrlen; + + hdrlen = git_odb__format_object_header(buffer, sizeof(buffer), (size_t)len, type); + git_hash_update(ctx, buffer, hdrlen); +} + +static int hash_object_stream(git_indexer_stream *idx, git_packfile_stream *stream) +{ + ssize_t read; + + assert(idx && stream); + + do { + if ((read = git_packfile_stream_read(stream, idx->objbuf, sizeof(idx->objbuf))) < 0) + break; + + git_hash_update(&idx->hash_ctx, idx->objbuf, read); + } while (read > 0); + + if (read < 0) + return (int)read; + + return 0; +} + +/* In order to create the packfile stream, we need to skip over the delta base description */ +static int advance_delta_offset(git_indexer_stream *idx, git_otype type) +{ + git_mwindow *w = NULL; + + assert(type == GIT_OBJ_REF_DELTA || type == GIT_OBJ_OFS_DELTA); if (type == GIT_OBJ_REF_DELTA) { idx->off += GIT_OID_RAWSZ; } else { - git_off_t base_off; - - base_off = get_delta_base(idx->pack, &w, &idx->off, type, entry_start); + git_off_t base_off = get_delta_base(idx->pack, &w, &idx->off, type, idx->entry_start); git_mwindow_close(&w); if (base_off < 0) return (int)base_off; } - error = packfile_unpack_compressed(&obj, idx->pack, &w, &idx->off, entry_size, type); - if (error == GIT_EBUFS) { - idx->off = entry_start; - return GIT_EBUFS; - } else if (error < 0){ - return -1; + return 0; +} + +/* Read from the stream and discard any output */ +static int read_object_stream(git_indexer_stream *idx, git_packfile_stream *stream) +{ + ssize_t read; + + assert(stream); + + do { + read = git_packfile_stream_read(stream, idx->objbuf, sizeof(idx->objbuf)); + } while (read > 0); + + if (read < 0) + return (int)read; + + return 0; +} + +static int crc_object(uint32_t *crc_out, git_mwindow_file *mwf, git_off_t start, git_off_t size) +{ + void *ptr; + uint32_t crc; + unsigned int left, len; + git_mwindow *w = NULL; + + crc = crc32(0L, Z_NULL, 0); + while (size) { + ptr = git_mwindow_open(mwf, &w, start, (size_t)size, &left); + if (ptr == NULL) + return -1; + + len = min(left, (unsigned int)size); + crc = crc32(crc, ptr, len); + size -= len; + start += len; + git_mwindow_close(&w); } - delta = git__calloc(1, sizeof(struct delta_info)); - GITERR_CHECK_ALLOC(delta); - delta->delta_off = entry_start; + *crc_out = htonl(crc); + return 0; +} + +static int store_object(git_indexer_stream *idx) +{ + int i, error; + khiter_t k; + git_oid oid; + struct entry *entry; + git_off_t entry_size; + struct git_pack_entry *pentry; + git_hash_ctx *ctx = &idx->hash_ctx; + git_off_t entry_start = idx->entry_start; - git__free(obj.data); + entry = git__calloc(1, sizeof(*entry)); + GITERR_CHECK_ALLOC(entry); - if (git_vector_insert(&idx->deltas, delta) < 0) - return -1; + pentry = git__malloc(sizeof(struct git_pack_entry)); + GITERR_CHECK_ALLOC(pentry); + + git_hash_final(&oid, ctx); + entry_size = idx->off - entry_start; + if (entry_start > UINT31_MAX) { + entry->offset = UINT32_MAX; + entry->offset_long = entry_start; + } else { + entry->offset = (uint32_t)entry_start; + } + + git_oid_cpy(&pentry->sha1, &oid); + pentry->offset = entry_start; + + k = kh_put(oid, idx->pack->idx_cache, &pentry->sha1, &error); + if (!error) { + git__free(pentry); + goto on_error; + } + + kh_value(idx->pack->idx_cache, k) = pentry; + + git_oid_cpy(&entry->oid, &oid); + + if (crc_object(&entry->crc, &idx->pack->mwf, entry_start, entry_size) < 0) + goto on_error; + + /* Add the object to the list */ + if (git_vector_insert(&idx->objects, entry) < 0) + goto on_error; + + for (i = oid.id[0]; i < 256; ++i) { + idx->fanout[i]++; + } return 0; + +on_error: + git__free(entry); + + return -1; } static int hash_and_save(git_indexer_stream *idx, git_rawobj *obj, git_off_t entry_start) { - int i; + int i, error; + khiter_t k; git_oid oid; - void *packed; size_t entry_size; - unsigned int left; struct entry *entry; - git_mwindow *w = NULL; - git_mwindow_file *mwf = &idx->pack->mwf; struct git_pack_entry *pentry; entry = git__calloc(1, sizeof(*entry)); @@ -257,20 +350,21 @@ static int hash_and_save(git_indexer_stream *idx, git_rawobj *obj, git_off_t ent git_oid_cpy(&pentry->sha1, &oid); pentry->offset = entry_start; - if (git_vector_insert(&idx->pack->cache, pentry) < 0) + k = kh_put(oid, idx->pack->idx_cache, &pentry->sha1, &error); + if (!error) { + git__free(pentry); goto on_error; + } + + kh_value(idx->pack->idx_cache, k) = pentry; git_oid_cpy(&entry->oid, &oid); entry->crc = crc32(0L, Z_NULL, 0); entry_size = (size_t)(idx->off - entry_start); - packed = git_mwindow_open(mwf, &w, entry_start, entry_size, &left); - if (packed == NULL) + if (crc_object(&entry->crc, &idx->pack->mwf, entry_start, entry_size) < 0) goto on_error; - entry->crc = htonl(crc32(entry->crc, packed, (uInt)entry_size)); - git_mwindow_close(&w); - /* Add the object to the list */ if (git_vector_insert(&idx->objects, entry) < 0) goto on_error; @@ -283,20 +377,27 @@ static int hash_and_save(git_indexer_stream *idx, git_rawobj *obj, git_off_t ent on_error: git__free(entry); - git__free(pentry); git__free(obj->data); return -1; } -int git_indexer_stream_add(git_indexer_stream *idx, const void *data, size_t size, git_indexer_stats *stats) +static int do_progress_callback(git_indexer_stream *idx, git_transfer_progress *stats) { - int error; + if (!idx->progress_cb) return 0; + return idx->progress_cb(stats, idx->progress_payload); +} + +int git_indexer_stream_add(git_indexer_stream *idx, const void *data, size_t size, git_transfer_progress *stats) +{ + int error = -1; struct git_pack_header hdr; - size_t processed = stats->processed; + size_t processed; git_mwindow_file *mwf = &idx->pack->mwf; assert(idx && data && stats); + processed = stats->indexed_objects; + if (git_filebuf_write(&idx->pack_file, data, size) < 0) return -1; @@ -311,11 +412,11 @@ int git_indexer_stream_add(git_indexer_stream *idx, const void *data, size_t siz mwf = &idx->pack->mwf; if (git_mwindow_file_register(&idx->pack->mwf) < 0) return -1; - - return 0; } if (!idx->parsed_header) { + unsigned int total_objects; + if ((unsigned)idx->pack->mwf.size < sizeof(hdr)) return 0; @@ -328,19 +429,25 @@ int git_indexer_stream_add(git_indexer_stream *idx, const void *data, size_t siz /* for now, limit to 2^32 objects */ assert(idx->nr_objects == (size_t)((unsigned int)idx->nr_objects)); + if (idx->nr_objects == (size_t)((unsigned int)idx->nr_objects)) + total_objects = (unsigned int)idx->nr_objects; + else + total_objects = UINT_MAX; - if (git_vector_init(&idx->pack->cache, (unsigned int)idx->nr_objects, cache_cmp) < 0) - return -1; + idx->pack->idx_cache = git_oidmap_alloc(); + GITERR_CHECK_ALLOC(idx->pack->idx_cache); idx->pack->has_cache = 1; - if (git_vector_init(&idx->objects, (unsigned int)idx->nr_objects, objects_cmp) < 0) + if (git_vector_init(&idx->objects, total_objects, objects_cmp) < 0) return -1; - if (git_vector_init(&idx->deltas, (unsigned int)(idx->nr_objects / 2), NULL) < 0) + if (git_vector_init(&idx->deltas, total_objects / 2, NULL) < 0) return -1; - stats->total = (unsigned int)idx->nr_objects; - stats->processed = 0; + stats->received_objects = 0; + processed = stats->indexed_objects = 0; + stats->total_objects = total_objects; + do_progress_callback(idx, stats); } /* Now that we have data in the pack, let's try to parse it */ @@ -348,42 +455,91 @@ int git_indexer_stream_add(git_indexer_stream *idx, const void *data, size_t siz /* As the file grows any windows we try to use will be out of date */ git_mwindow_free_all(mwf); while (processed < idx->nr_objects) { - git_rawobj obj; + git_packfile_stream *stream = &idx->stream; git_off_t entry_start = idx->off; + size_t entry_size; + git_otype type; + git_mwindow *w = NULL; if (idx->pack->mwf.size <= idx->off + 20) return 0; - error = git_packfile_unpack(&obj, idx->pack, &idx->off); - if (error == GIT_EBUFS) { - idx->off = entry_start; - return 0; - } - - if (error < 0) { - idx->off = entry_start; - error = store_delta(idx); - if (error == GIT_EBUFS) + if (!idx->have_stream) { + error = git_packfile_unpack_header(&entry_size, &type, mwf, &w, &idx->off); + if (error == GIT_EBUFS) { + idx->off = entry_start; return 0; + } if (error < 0) - return error; + return -1; + + git_mwindow_close(&w); + idx->entry_start = entry_start; + git_hash_ctx_init(&idx->hash_ctx); + + if (type == GIT_OBJ_REF_DELTA || type == GIT_OBJ_OFS_DELTA) { + error = advance_delta_offset(idx, type); + if (error == GIT_EBUFS) { + idx->off = entry_start; + return 0; + } + if (error < 0) + return -1; + + idx->have_delta = 1; + } else { + idx->have_delta = 0; + hash_header(&idx->hash_ctx, entry_size, type); + } + + idx->have_stream = 1; + if (git_packfile_stream_open(stream, idx->pack, idx->off) < 0) + goto on_error; - continue; } - if (hash_and_save(idx, &obj, entry_start) < 0) + if (idx->have_delta) { + error = read_object_stream(idx, stream); + } else { + error = hash_object_stream(idx, stream); + } + + idx->off = stream->curpos; + if (error == GIT_EBUFS) + return 0; + + /* We want to free the stream reasorces no matter what here */ + idx->have_stream = 0; + git_packfile_stream_free(stream); + + if (error < 0) + goto on_error; + + if (idx->have_delta) { + error = store_delta(idx); + } else { + error = store_object(idx); + } + + if (error < 0) goto on_error; - git__free(obj.data); + if (!idx->have_delta) { + stats->indexed_objects = (unsigned int)++processed; + } + stats->received_objects++; - stats->processed = (unsigned int)++processed; + if (do_progress_callback(idx, stats) != 0) { + error = GIT_EUSER; + goto on_error; + } } return 0; on_error: git_mwindow_free_all(mwf); - return -1; + return error; } static int index_path_stream(git_buf *path, git_indexer_stream *idx, const char *suffix) @@ -408,7 +564,7 @@ static int index_path_stream(git_buf *path, git_indexer_stream *idx, const char return git_buf_oom(path) ? -1 : 0; } -static int resolve_deltas(git_indexer_stream *idx, git_indexer_stats *stats) +static int resolve_deltas(git_indexer_stream *idx, git_transfer_progress *stats) { unsigned int i; struct delta_info *delta; @@ -424,13 +580,14 @@ static int resolve_deltas(git_indexer_stream *idx, git_indexer_stats *stats) return -1; git__free(obj.data); - stats->processed++; + stats->indexed_objects++; + do_progress_callback(idx, stats); } return 0; } -int git_indexer_stream_finalize(git_indexer_stream *idx, git_indexer_stats *stats) +int git_indexer_stream_finalize(git_indexer_stream *idx, git_transfer_progress *stats) { git_mwindow *w = NULL; unsigned int i, long_offsets = 0, left; @@ -439,11 +596,15 @@ int git_indexer_stream_finalize(git_indexer_stream *idx, git_indexer_stats *stat struct entry *entry; void *packfile_hash; git_oid file_hash; - SHA_CTX ctx; + git_hash_ctx ctx; + git_filebuf index_file = {0}; + + if (git_hash_ctx_init(&ctx) < 0) + return -1; /* Test for this before resolve_deltas(), as it plays with idx->off */ if (idx->off < idx->pack->mwf.size - GIT_OID_RAWSZ) { - giterr_set(GITERR_INDEXER, "Indexing error: junk at the end of the pack"); + giterr_set(GITERR_INDEXER, "Indexing error: unexpected data at the end of the pack"); return -1; } @@ -451,7 +612,7 @@ int git_indexer_stream_finalize(git_indexer_stream *idx, git_indexer_stats *stat if (resolve_deltas(idx, stats) < 0) return -1; - if (stats->processed != stats->total) { + if (stats->indexed_objects != stats->total_objects) { giterr_set(GITERR_INDEXER, "Indexing error: early EOF"); return -1; } @@ -464,31 +625,30 @@ int git_indexer_stream_finalize(git_indexer_stream *idx, git_indexer_stats *stat if (git_buf_oom(&filename)) return -1; - if (git_filebuf_open(&idx->index_file, filename.ptr, GIT_FILEBUF_HASH_CONTENTS) < 0) + if (git_filebuf_open(&index_file, filename.ptr, GIT_FILEBUF_HASH_CONTENTS) < 0) goto on_error; /* Write out the header */ hdr.idx_signature = htonl(PACK_IDX_SIGNATURE); hdr.idx_version = htonl(2); - git_filebuf_write(&idx->index_file, &hdr, sizeof(hdr)); + git_filebuf_write(&index_file, &hdr, sizeof(hdr)); /* Write out the fanout table */ for (i = 0; i < 256; ++i) { uint32_t n = htonl(idx->fanout[i]); - git_filebuf_write(&idx->index_file, &n, sizeof(n)); + git_filebuf_write(&index_file, &n, sizeof(n)); } /* Write out the object names (SHA-1 hashes) */ - SHA1_Init(&ctx); git_vector_foreach(&idx->objects, i, entry) { - git_filebuf_write(&idx->index_file, &entry->oid, sizeof(git_oid)); - SHA1_Update(&ctx, &entry->oid, GIT_OID_RAWSZ); + git_filebuf_write(&index_file, &entry->oid, sizeof(git_oid)); + git_hash_update(&ctx, &entry->oid, GIT_OID_RAWSZ); } - SHA1_Final(idx->hash.id, &ctx); + git_hash_final(&idx->hash, &ctx); /* Write out the CRC32 values */ git_vector_foreach(&idx->objects, i, entry) { - git_filebuf_write(&idx->index_file, &entry->crc, sizeof(uint32_t)); + git_filebuf_write(&index_file, &entry->crc, sizeof(uint32_t)); } /* Write out the offsets */ @@ -500,7 +660,7 @@ int git_indexer_stream_finalize(git_indexer_stream *idx, git_indexer_stats *stat else n = htonl(entry->offset); - git_filebuf_write(&idx->index_file, &n, sizeof(uint32_t)); + git_filebuf_write(&index_file, &n, sizeof(uint32_t)); } /* Write out the long offsets */ @@ -513,7 +673,7 @@ int git_indexer_stream_finalize(git_indexer_stream *idx, git_indexer_stats *stat split[0] = htonl(entry->offset_long >> 32); split[1] = htonl(entry->offset_long & 0xffffffff); - git_filebuf_write(&idx->index_file, &split, sizeof(uint32_t) * 2); + git_filebuf_write(&index_file, &split, sizeof(uint32_t) * 2); } /* Write out the packfile trailer */ @@ -526,24 +686,26 @@ int git_indexer_stream_finalize(git_indexer_stream *idx, git_indexer_stats *stat memcpy(&file_hash, packfile_hash, GIT_OID_RAWSZ); git_mwindow_close(&w); - git_filebuf_write(&idx->index_file, &file_hash, sizeof(git_oid)); + git_filebuf_write(&index_file, &file_hash, sizeof(git_oid)); /* Write out the packfile trailer to the idx file as well */ - if (git_filebuf_hash(&file_hash, &idx->index_file) < 0) + if (git_filebuf_hash(&file_hash, &index_file) < 0) goto on_error; - git_filebuf_write(&idx->index_file, &file_hash, sizeof(git_oid)); + git_filebuf_write(&index_file, &file_hash, sizeof(git_oid)); /* Figure out what the final name should be */ if (index_path_stream(&filename, idx, ".idx") < 0) goto on_error; /* Commit file */ - if (git_filebuf_commit_at(&idx->index_file, filename.ptr, GIT_PACK_FILE_MODE) < 0) + if (git_filebuf_commit_at(&index_file, filename.ptr, GIT_PACK_FILE_MODE) < 0) goto on_error; git_mwindow_free_all(&idx->pack->mwf); + /* We need to close the descriptor here so Windows doesn't choke on commit_at */ p_close(idx->pack->mwf.fd); + idx->pack->mwf.fd = -1; if (index_path_stream(&filename, idx, ".pack") < 0) goto on_error; @@ -556,17 +718,17 @@ int git_indexer_stream_finalize(git_indexer_stream *idx, git_indexer_stats *stat on_error: git_mwindow_free_all(&idx->pack->mwf); - p_close(idx->pack->mwf.fd); - git_filebuf_cleanup(&idx->index_file); + git_filebuf_cleanup(&index_file); git_buf_free(&filename); + git_hash_ctx_cleanup(&ctx); return -1; } void git_indexer_stream_free(git_indexer_stream *idx) { + khiter_t k; unsigned int i; struct entry *e; - struct git_pack_entry *pe; struct delta_info *delta; if (idx == NULL) @@ -575,325 +737,20 @@ void git_indexer_stream_free(git_indexer_stream *idx) git_vector_foreach(&idx->objects, i, e) git__free(e); git_vector_free(&idx->objects); - git_vector_foreach(&idx->pack->cache, i, pe) - git__free(pe); - git_vector_free(&idx->pack->cache); - git_vector_foreach(&idx->deltas, i, delta) - git__free(delta); - git_vector_free(&idx->deltas); - git__free(idx->pack); - git__free(idx); -} - -int git_indexer_new(git_indexer **out, const char *packname) -{ - git_indexer *idx; - struct git_pack_header hdr; - int error; - - assert(out && packname); - - if (git_path_root(packname) < 0) { - giterr_set(GITERR_INDEXER, "Path is not absolute"); - return -1; - } - - idx = git__calloc(1, sizeof(git_indexer)); - GITERR_CHECK_ALLOC(idx); - - open_pack(&idx->pack, packname); - - if ((error = parse_header(&hdr, idx->pack)) < 0) - goto cleanup; - - idx->nr_objects = ntohl(hdr.hdr_entries); - - /* for now, limit to 2^32 objects */ - assert(idx->nr_objects == (size_t)((unsigned int)idx->nr_objects)); - - error = git_vector_init(&idx->pack->cache, (unsigned int)idx->nr_objects, cache_cmp); - if (error < 0) - goto cleanup; - - idx->pack->has_cache = 1; - error = git_vector_init(&idx->objects, (unsigned int)idx->nr_objects, objects_cmp); - if (error < 0) - goto cleanup; - - *out = idx; - - return 0; -cleanup: - git_indexer_free(idx); - - return -1; -} - -static int index_path(git_buf *path, git_indexer *idx) -{ - const char prefix[] = "pack-", suffix[] = ".idx"; - size_t slash = (size_t)path->size; - - /* search backwards for '/' */ - while (slash > 0 && path->ptr[slash - 1] != '/') - slash--; - - if (git_buf_grow(path, slash + 1 + strlen(prefix) + - GIT_OID_HEXSZ + strlen(suffix) + 1) < 0) - return -1; - - git_buf_truncate(path, slash); - git_buf_puts(path, prefix); - git_oid_fmt(path->ptr + git_buf_len(path), &idx->hash); - path->size += GIT_OID_HEXSZ; - git_buf_puts(path, suffix); - - return git_buf_oom(path) ? -1 : 0; -} - -int git_indexer_write(git_indexer *idx) -{ - git_mwindow *w = NULL; - int error; - unsigned int i, long_offsets = 0, left; - struct git_pack_idx_header hdr; - git_buf filename = GIT_BUF_INIT; - struct entry *entry; - void *packfile_hash; - git_oid file_hash; - SHA_CTX ctx; - - git_vector_sort(&idx->objects); - - git_buf_sets(&filename, idx->pack->pack_name); - git_buf_truncate(&filename, filename.size - strlen("pack")); - git_buf_puts(&filename, "idx"); - if (git_buf_oom(&filename)) - return -1; - - error = git_filebuf_open(&idx->file, filename.ptr, GIT_FILEBUF_HASH_CONTENTS); - if (error < 0) - goto cleanup; - - /* Write out the header */ - hdr.idx_signature = htonl(PACK_IDX_SIGNATURE); - hdr.idx_version = htonl(2); - error = git_filebuf_write(&idx->file, &hdr, sizeof(hdr)); - if (error < 0) - goto cleanup; - - /* Write out the fanout table */ - for (i = 0; i < 256; ++i) { - uint32_t n = htonl(idx->fanout[i]); - error = git_filebuf_write(&idx->file, &n, sizeof(n)); - if (error < 0) - goto cleanup; - } - - /* Write out the object names (SHA-1 hashes) */ - SHA1_Init(&ctx); - git_vector_foreach(&idx->objects, i, entry) { - error = git_filebuf_write(&idx->file, &entry->oid, sizeof(git_oid)); - SHA1_Update(&ctx, &entry->oid, GIT_OID_RAWSZ); - if (error < 0) - goto cleanup; - } - SHA1_Final(idx->hash.id, &ctx); - - /* Write out the CRC32 values */ - git_vector_foreach(&idx->objects, i, entry) { - error = git_filebuf_write(&idx->file, &entry->crc, sizeof(uint32_t)); - if (error < 0) - goto cleanup; - } - - /* Write out the offsets */ - git_vector_foreach(&idx->objects, i, entry) { - uint32_t n; - - if (entry->offset == UINT32_MAX) - n = htonl(0x80000000 | long_offsets++); - else - n = htonl(entry->offset); - - error = git_filebuf_write(&idx->file, &n, sizeof(uint32_t)); - if (error < 0) - goto cleanup; - } - - /* Write out the long offsets */ - git_vector_foreach(&idx->objects, i, entry) { - uint32_t split[2]; - - if (entry->offset != UINT32_MAX) - continue; - - split[0] = htonl(entry->offset_long >> 32); - split[1] = htonl(entry->offset_long & 0xffffffff); - - error = git_filebuf_write(&idx->file, &split, sizeof(uint32_t) * 2); - if (error < 0) - goto cleanup; - } - - /* Write out the packfile trailer */ - - packfile_hash = git_mwindow_open(&idx->pack->mwf, &w, idx->pack->mwf.size - GIT_OID_RAWSZ, GIT_OID_RAWSZ, &left); - git_mwindow_close(&w); - if (packfile_hash == NULL) { - error = -1; - goto cleanup; - } - - memcpy(&file_hash, packfile_hash, GIT_OID_RAWSZ); - - git_mwindow_close(&w); - - error = git_filebuf_write(&idx->file, &file_hash, sizeof(git_oid)); - if (error < 0) - goto cleanup; - - /* Write out the index sha */ - error = git_filebuf_hash(&file_hash, &idx->file); - if (error < 0) - goto cleanup; - - error = git_filebuf_write(&idx->file, &file_hash, sizeof(git_oid)); - if (error < 0) - goto cleanup; - - /* Figure out what the final name should be */ - error = index_path(&filename, idx); - if (error < 0) - goto cleanup; - - /* Commit file */ - error = git_filebuf_commit_at(&idx->file, filename.ptr, GIT_PACK_FILE_MODE); - -cleanup: - git_mwindow_free_all(&idx->pack->mwf); - if (error < 0) - git_filebuf_cleanup(&idx->file); - git_buf_free(&filename); - - return error; -} - -int git_indexer_run(git_indexer *idx, git_indexer_stats *stats) -{ - git_mwindow_file *mwf; - git_off_t off = sizeof(struct git_pack_header); - int error; - struct entry *entry; - unsigned int left, processed; - - assert(idx && stats); - - mwf = &idx->pack->mwf; - error = git_mwindow_file_register(mwf); - if (error < 0) - return error; - - stats->total = (unsigned int)idx->nr_objects; - stats->processed = processed = 0; - - while (processed < idx->nr_objects) { - git_rawobj obj; - git_oid oid; - struct git_pack_entry *pentry; - git_mwindow *w = NULL; - int i; - git_off_t entry_start = off; - void *packed; - size_t entry_size; - char fmt[GIT_OID_HEXSZ] = {0}; - - entry = git__calloc(1, sizeof(*entry)); - GITERR_CHECK_ALLOC(entry); - - if (off > UINT31_MAX) { - entry->offset = UINT32_MAX; - entry->offset_long = off; - } else { - entry->offset = (uint32_t)off; - } - - error = git_packfile_unpack(&obj, idx->pack, &off); - if (error < 0) - goto cleanup; - - /* FIXME: Parse the object instead of hashing it */ - error = git_odb__hashobj(&oid, &obj); - if (error < 0) { - giterr_set(GITERR_INDEXER, "Failed to hash object"); - goto cleanup; + if (idx->pack) { + for (k = kh_begin(idx->pack->idx_cache); k != kh_end(idx->pack->idx_cache); k++) { + if (kh_exist(idx->pack->idx_cache, k)) + git__free(kh_value(idx->pack->idx_cache, k)); } - pentry = git__malloc(sizeof(struct git_pack_entry)); - if (pentry == NULL) { - error = -1; - goto cleanup; - } - - git_oid_cpy(&pentry->sha1, &oid); - pentry->offset = entry_start; - git_oid_fmt(fmt, &oid); - printf("adding %s to cache\n", fmt); - error = git_vector_insert(&idx->pack->cache, pentry); - if (error < 0) - goto cleanup; - - git_oid_cpy(&entry->oid, &oid); - entry->crc = crc32(0L, Z_NULL, 0); - - entry_size = (size_t)(off - entry_start); - packed = git_mwindow_open(mwf, &w, entry_start, entry_size, &left); - if (packed == NULL) { - error = -1; - goto cleanup; - } - entry->crc = htonl(crc32(entry->crc, packed, (uInt)entry_size)); - git_mwindow_close(&w); - - /* Add the object to the list */ - error = git_vector_insert(&idx->objects, entry); - if (error < 0) - goto cleanup; - - for (i = oid.id[0]; i < 256; ++i) { - idx->fanout[i]++; - } - - git__free(obj.data); - - stats->processed = ++processed; + git_oidmap_free(idx->pack->idx_cache); } -cleanup: - git_mwindow_free_all(mwf); - - return error; - -} - -void git_indexer_free(git_indexer *idx) -{ - unsigned int i; - struct entry *e; - struct git_pack_entry *pe; - - if (idx == NULL) - return; - - p_close(idx->pack->mwf.fd); - git_vector_foreach(&idx->objects, i, e) - git__free(e); - git_vector_free(&idx->objects); - git_vector_foreach(&idx->pack->cache, i, pe) - git__free(pe); - git_vector_free(&idx->pack->cache); - git__free(idx->pack); + git_vector_foreach(&idx->deltas, i, delta) + git__free(delta); + git_vector_free(&idx->deltas); + git_packfile_free(idx->pack); + git_filebuf_cleanup(&idx->pack_file); git__free(idx); } - diff --git a/src/iterator.c b/src/iterator.c index 819b0e22a..5b5ed9525 100644 --- a/src/iterator.c +++ b/src/iterator.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -10,295 +10,565 @@ #include "ignore.h" #include "buffer.h" #include "git2/submodule.h" +#include <ctype.h> + +#define ITERATOR_SET_CB(P,NAME_LC) do { \ + (P)->cb.current = NAME_LC ## _iterator__current; \ + (P)->cb.advance = NAME_LC ## _iterator__advance; \ + (P)->cb.advance_into = NAME_LC ## _iterator__advance_into; \ + (P)->cb.seek = NAME_LC ## _iterator__seek; \ + (P)->cb.reset = NAME_LC ## _iterator__reset; \ + (P)->cb.at_end = NAME_LC ## _iterator__at_end; \ + (P)->cb.free = NAME_LC ## _iterator__free; \ + } while (0) + +#define ITERATOR_CASE_FLAGS \ + (GIT_ITERATOR_IGNORE_CASE | GIT_ITERATOR_DONT_IGNORE_CASE) -#define ITERATOR_BASE_INIT(P,NAME_LC,NAME_UC) do { \ +#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_ ## NAME_UC; \ + (P)->base.type = GIT_ITERATOR_TYPE_ ## NAME_UC; \ + (P)->base.cb = &(P)->cb; \ + ITERATOR_SET_CB(P,NAME_LC); \ + (P)->base.repo = (REPO); \ (P)->base.start = start ? git__strdup(start) : NULL; \ (P)->base.end = end ? git__strdup(end) : NULL; \ - (P)->base.current = NAME_LC ## _iterator__current; \ - (P)->base.at_end = NAME_LC ## _iterator__at_end; \ - (P)->base.advance = NAME_LC ## _iterator__advance; \ - (P)->base.seek = NAME_LC ## _iterator__seek; \ - (P)->base.reset = NAME_LC ## _iterator__reset; \ - (P)->base.free = NAME_LC ## _iterator__free; \ - if ((start && !(P)->base.start) || (end && !(P)->base.end)) \ - return -1; \ + if ((start && !(P)->base.start) || (end && !(P)->base.end)) { \ + git__free(P); return -1; } \ + (P)->base.prefixcomp = git__prefixcmp; \ + (P)->base.flags = flags & ~ITERATOR_CASE_FLAGS; \ + if ((P)->base.flags & GIT_ITERATOR_DONT_AUTOEXPAND) \ + (P)->base.flags |= GIT_ITERATOR_INCLUDE_TREES; \ } while (0) +#define iterator__flag(I,F) ((((git_iterator *)(I))->flags & GIT_ITERATOR_ ## F) != 0) +#define iterator__ignore_case(I) iterator__flag(I,IGNORE_CASE) +#define iterator__include_trees(I) iterator__flag(I,INCLUDE_TREES) +#define iterator__dont_autoexpand(I) iterator__flag(I,DONT_AUTOEXPAND) +#define iterator__do_autoexpand(I) !iterator__flag(I,DONT_AUTOEXPAND) + +#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) + -static int empty_iterator__no_item( - git_iterator *iter, const git_index_entry **entry) +static int iterator__reset_range( + git_iterator *iter, const char *start, const char *end) { - GIT_UNUSED(iter); - *entry = NULL; + if (start) { + if (iter->start) + git__free(iter->start); + iter->start = git__strdup(start); + GITERR_CHECK_ALLOC(iter->start); + } + + if (end) { + if (iter->end) + git__free(iter->end); + iter->end = git__strdup(end); + GITERR_CHECK_ALLOC(iter->end); + } + return 0; } -static int empty_iterator__at_end(git_iterator *iter) +static int iterator__update_ignore_case( + git_iterator *iter, + git_iterator_flag_t flags) { - GIT_UNUSED(iter); - return 1; + int error = 0, ignore_case = -1; + + if ((flags & GIT_ITERATOR_IGNORE_CASE) != 0) + ignore_case = true; + else if ((flags & GIT_ITERATOR_DONT_IGNORE_CASE) != 0) + ignore_case = false; + else { + git_index *index; + + if (!(error = git_repository_index__weakptr(&index, iter->repo))) + ignore_case = (index->ignore_case != false); + } + + if (ignore_case > 0) + iter->flags = (iter->flags | GIT_ITERATOR_IGNORE_CASE); + else if (ignore_case == 0) + iter->flags = (iter->flags & ~GIT_ITERATOR_IGNORE_CASE); + + iter->prefixcomp = iterator__ignore_case(iter) ? + git__prefixcmp_icase : git__prefixcmp; + + return error; +} + +GIT_INLINE(void) iterator__clear_entry(const git_index_entry **entry) +{ + if (entry) *entry = NULL; } -static int empty_iterator__noop(git_iterator *iter) + +static int empty_iterator__noop(const git_index_entry **e, git_iterator *i) { - GIT_UNUSED(iter); + GIT_UNUSED(i); + iterator__clear_entry(e); return 0; } -static int empty_iterator__seek(git_iterator *iter, const char *prefix) +static int empty_iterator__seek(git_iterator *i, const char *p) { - GIT_UNUSED(iter); - GIT_UNUSED(prefix); + GIT_UNUSED(i); GIT_UNUSED(p); return -1; } -static void empty_iterator__free(git_iterator *iter) +static int empty_iterator__reset(git_iterator *i, const char *s, const char *e) { - GIT_UNUSED(iter); + GIT_UNUSED(i); GIT_UNUSED(s); GIT_UNUSED(e); + return 0; } -int git_iterator_for_nothing(git_iterator **iter) +static int empty_iterator__at_end(git_iterator *i) { - git_iterator *i = git__calloc(1, sizeof(git_iterator)); - GITERR_CHECK_ALLOC(i); + GIT_UNUSED(i); + return 1; +} - i->type = GIT_ITERATOR_EMPTY; - i->current = empty_iterator__no_item; - i->at_end = empty_iterator__at_end; - i->advance = empty_iterator__no_item; - i->seek = empty_iterator__seek; - i->reset = empty_iterator__noop; - i->free = empty_iterator__free; +static void empty_iterator__free(git_iterator *i) +{ + GIT_UNUSED(i); +} + +typedef struct { + git_iterator base; + git_iterator_callbacks cb; +} empty_iterator; + +int git_iterator_for_nothing( + git_iterator **iter, + git_iterator_flag_t flags, + const char *start, + const char *end) +{ + empty_iterator *i; + +#define empty_iterator__current empty_iterator__noop +#define empty_iterator__advance empty_iterator__noop +#define empty_iterator__advance_into empty_iterator__noop + + ITERATOR_BASE_INIT(i, empty, EMPTY, NULL); - *iter = i; + if ((flags & GIT_ITERATOR_IGNORE_CASE) != 0) + i->base.flags |= GIT_ITERATOR_IGNORE_CASE; + *iter = (git_iterator *)i; return 0; } +typedef struct tree_iterator_entry tree_iterator_entry; +struct tree_iterator_entry { + tree_iterator_entry *parent; + const git_tree_entry *te; + git_tree *tree; +}; + typedef struct tree_iterator_frame tree_iterator_frame; struct tree_iterator_frame { - tree_iterator_frame *next; - git_tree *tree; - char *start; - unsigned int index; + tree_iterator_frame *up, *down; + + size_t n_entries; /* items in this frame */ + size_t current; /* start of currently active range in frame */ + size_t next; /* start of next range in frame */ + + const char *start; + size_t startlen; + + tree_iterator_entry *entries[GIT_FLEX_ARRAY]; }; typedef struct { git_iterator base; - git_repository *repo; - tree_iterator_frame *stack; + git_iterator_callbacks cb; + tree_iterator_frame *head, *root; + git_pool pool; git_index_entry entry; git_buf path; + int path_ambiguities; bool path_has_filename; + int (*strncomp)(const char *a, const char *b, size_t sz); } tree_iterator; -static const git_tree_entry *tree_iterator__tree_entry(tree_iterator *ti) -{ - return (ti->stack == NULL) ? NULL : - git_tree_entry_byindex(ti->stack->tree, ti->stack->index); -} - static char *tree_iterator__current_filename( tree_iterator *ti, const git_tree_entry *te) { if (!ti->path_has_filename) { if (git_buf_joinpath(&ti->path, ti->path.ptr, te->filename) < 0) return NULL; + + if (git_tree_entry__is_tree(te) && git_buf_putc(&ti->path, '/') < 0) + return NULL; + ti->path_has_filename = true; } return ti->path.ptr; } -static void tree_iterator__pop_frame(tree_iterator *ti) +static void tree_iterator__rewrite_filename(tree_iterator *ti) { - tree_iterator_frame *tf = ti->stack; - ti->stack = tf->next; - if (ti->stack != NULL) /* don't free the initial tree */ - git_tree_free(tf->tree); - git__free(tf); + tree_iterator_entry *scan = ti->head->entries[ti->head->current]; + ssize_t strpos = ti->path.size; + const git_tree_entry *te; + + if (strpos && ti->path.ptr[strpos - 1] == '/') + strpos--; + + for (; scan && (te = scan->te); scan = scan->parent) { + strpos -= te->filename_len; + memcpy(&ti->path.ptr[strpos], te->filename, te->filename_len); + strpos -= 1; /* separator */ + } +} + +static int tree_iterator__te_cmp( + const git_tree_entry *a, + const git_tree_entry *b, + int (*compare)(const char *, const char *, size_t)) +{ + return git_path_cmp( + a->filename, a->filename_len, a->attr == GIT_FILEMODE_TREE, + b->filename, b->filename_len, b->attr == GIT_FILEMODE_TREE, + compare); +} + +static int tree_iterator__ci_cmp(const void *a, const void *b, void *p) +{ + const tree_iterator_entry *ae = a, *be = b; + int cmp = tree_iterator__te_cmp(ae->te, be->te, git__strncasecmp); + + if (!cmp) { + /* stabilize sort order among equivalent names */ + if (!ae->parent->te || !be->parent->te) + cmp = tree_iterator__te_cmp(ae->te, be->te, git__strncmp); + else + cmp = tree_iterator__ci_cmp(ae->parent, be->parent, p); + } + + return cmp; } -static int tree_iterator__to_end(tree_iterator *ti) +static int tree_iterator__search_cmp(const void *key, const void *val, void *p) { - while (ti->stack && ti->stack->next) - tree_iterator__pop_frame(ti); + const tree_iterator_frame *tf = key; + const git_tree_entry *te = ((tree_iterator_entry *)val)->te; + + return git_path_cmp( + tf->start, tf->startlen, false, + te->filename, te->filename_len, te->attr == GIT_FILEMODE_TREE, + ((tree_iterator *)p)->strncomp); +} + +static int tree_iterator__set_next(tree_iterator *ti, tree_iterator_frame *tf) +{ + int error; + const git_tree_entry *te, *last = NULL; + + tf->next = tf->current; + + for (; tf->next < tf->n_entries; tf->next++, last = te) { + te = tf->entries[tf->next]->te; + + 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; + } - if (ti->stack) - ti->stack->index = git_tree_entrycount(ti->stack->tree); + if (tf->next > tf->current + 1) + ti->path_ambiguities++; + + if (last && !tree_iterator__current_filename(ti, last)) + return -1; return 0; } -static int tree_iterator__current( - git_iterator *self, const git_index_entry **entry) +GIT_INLINE(bool) tree_iterator__at_tree(tree_iterator *ti) { - tree_iterator *ti = (tree_iterator *)self; - const git_tree_entry *te = tree_iterator__tree_entry(ti); + return (ti->head->current < ti->head->n_entries && + ti->head->entries[ti->head->current]->tree != NULL); +} - if (entry) - *entry = NULL; +static int tree_iterator__push_frame(tree_iterator *ti) +{ + int error = 0; + tree_iterator_frame *head = ti->head, *tf = NULL; + size_t i, n_entries = 0; - if (te == NULL) + if (head->current >= head->n_entries || !head->entries[head->current]->tree) return 0; - ti->entry.mode = te->attr; - git_oid_cpy(&ti->entry.oid, &te->oid); + for (i = head->current; i < head->next; ++i) + n_entries += git_tree_entrycount(head->entries[i]->tree); - ti->entry.path = tree_iterator__current_filename(ti, te); - if (ti->entry.path == NULL) - return -1; + tf = git__calloc(sizeof(tree_iterator_frame) + + n_entries * sizeof(tree_iterator_entry *), 1); + GITERR_CHECK_ALLOC(tf); - if (ti->base.end && git__prefixcmp(ti->entry.path, ti->base.end) > 0) - return tree_iterator__to_end(ti); + tf->n_entries = n_entries; - if (entry) - *entry = &ti->entry; + tf->up = head; + head->down = tf; + ti->head = tf; + + for (i = head->current, n_entries = 0; i < head->next; ++i) { + git_tree *tree = head->entries[i]->tree; + size_t j, max_j = git_tree_entrycount(tree); + + for (j = 0; j < max_j; ++j) { + tree_iterator_entry *entry = git_pool_malloc(&ti->pool, 1); + GITERR_CHECK_ALLOC(entry); + + entry->parent = head->entries[i]; + entry->te = git_tree_entry_byindex(tree, j); + entry->tree = NULL; + + tf->entries[n_entries++] = entry; + } + } + + /* if ignore_case, sort entries case insensitively */ + if (iterator__ignore_case(ti)) + git__tsort_r( + (void **)tf->entries, tf->n_entries, tree_iterator__ci_cmp, tf); + + /* pick tf->current based on "start" (or start at zero) */ + if (head->startlen > 0) { + git__bsearch_r((void **)tf->entries, tf->n_entries, head, + tree_iterator__search_cmp, ti, &tf->current); + + while (tf->current && + !tree_iterator__search_cmp(head, tf->entries[tf->current-1], ti)) + tf->current--; + + if ((tf->start = strchr(head->start, '/')) != NULL) { + tf->start++; + tf->startlen = strlen(tf->start); + } + } + + ti->path_has_filename = false; + + if ((error = tree_iterator__set_next(ti, tf)) < 0) + return error; + + /* autoexpand as needed */ + if (!iterator__include_trees(ti) && tree_iterator__at_tree(ti)) + return tree_iterator__push_frame(ti); return 0; } -static int tree_iterator__at_end(git_iterator *self) +static bool tree_iterator__move_to_next( + tree_iterator *ti, tree_iterator_frame *tf) { - return (tree_iterator__tree_entry((tree_iterator *)self) == NULL); + 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 tree_iterator_frame *tree_iterator__alloc_frame( - git_tree *tree, char *start) +static bool tree_iterator__pop_frame(tree_iterator *ti, bool final) { - tree_iterator_frame *tf = git__calloc(1, sizeof(tree_iterator_frame)); - if (!tf) - return NULL; + tree_iterator_frame *tf = ti->head; - tf->tree = tree; + if (!tf->up) + return false; - if (start && *start) { - tf->start = start; - tf->index = git_tree__prefix_position(tree, start); + ti->head = tf->up; + ti->head->down = NULL; + + tree_iterator__move_to_next(ti, tf); + + if (!final) { /* if final, don't bother to clean up */ + git_pool_free_array(&ti->pool, tf->n_entries, (void **)tf->entries); + git_buf_rtruncate_at_char(&ti->path, '/'); } - return tf; + git__free(tf); + + return true; } -static int tree_iterator__expand_tree(tree_iterator *ti) +static int tree_iterator__pop_all(tree_iterator *ti, bool to_end, bool final) { - int error; - git_tree *subtree; - const git_tree_entry *te = tree_iterator__tree_entry(ti); - tree_iterator_frame *tf; - char *relpath; + while (tree_iterator__pop_frame(ti, final)) /* pop to root */; - while (te != NULL && git_tree_entry__is_tree(te)) { - if (git_buf_joinpath(&ti->path, ti->path.ptr, te->filename) < 0) - return -1; + if (!final) { + ti->head->current = to_end ? ti->head->n_entries : 0; + ti->path_ambiguities = 0; + git_buf_clear(&ti->path); + } - /* check that we have not passed the range end */ - if (ti->base.end != NULL && - git__prefixcmp(ti->path.ptr, ti->base.end) > 0) - return tree_iterator__to_end(ti); + return 0; +} - if ((error = git_tree_lookup(&subtree, ti->repo, &te->oid)) < 0) - return error; +static int tree_iterator__current( + const git_index_entry **entry, git_iterator *self) +{ + tree_iterator *ti = (tree_iterator *)self; + tree_iterator_frame *tf = ti->head; + const git_tree_entry *te; - relpath = NULL; + iterator__clear_entry(entry); - /* apply range start to new frame if relevant */ - if (ti->stack->start && - git__prefixcmp(ti->stack->start, te->filename) == 0) - { - size_t namelen = strlen(te->filename); - if (ti->stack->start[namelen] == '/') - relpath = ti->stack->start + namelen + 1; - } + if (tf->current >= tf->n_entries) + return 0; + te = tf->entries[tf->current]->te; - if ((tf = tree_iterator__alloc_frame(subtree, relpath)) == NULL) - return -1; + ti->entry.mode = te->attr; + git_oid_cpy(&ti->entry.oid, &te->oid); - tf->next = ti->stack; - ti->stack = tf; + ti->entry.path = tree_iterator__current_filename(ti, te); + GITERR_CHECK_ALLOC(ti->entry.path); - te = tree_iterator__tree_entry(ti); - } + 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 (entry) + *entry = &ti->entry; return 0; } -static int tree_iterator__advance( - git_iterator *self, const git_index_entry **entry) +static int tree_iterator__advance_into( + const git_index_entry **entry, git_iterator *self) { int error = 0; tree_iterator *ti = (tree_iterator *)self; - const git_tree_entry *te = NULL; - if (entry != NULL) - *entry = NULL; + iterator__clear_entry(entry); - if (ti->path_has_filename) { - git_buf_rtruncate_at_char(&ti->path, '/'); - ti->path_has_filename = false; - } + if (tree_iterator__at_tree(ti) && + !(error = tree_iterator__push_frame(ti))) + error = tree_iterator__current(entry, self); - while (ti->stack != NULL) { - te = git_tree_entry_byindex(ti->stack->tree, ++ti->stack->index); - if (te != NULL) - break; + return error; +} + +static int tree_iterator__advance( + const git_index_entry **entry, git_iterator *self) +{ + int error; + tree_iterator *ti = (tree_iterator *)self; + tree_iterator_frame *tf = ti->head; - tree_iterator__pop_frame(ti); + iterator__clear_entry(entry); + if (tf->current > tf->n_entries) + return 0; + + if (iterator__do_autoexpand(ti) && iterator__include_trees(ti) && + tree_iterator__at_tree(ti)) + return tree_iterator__advance_into(entry, self); + + if (ti->path_has_filename) { git_buf_rtruncate_at_char(&ti->path, '/'); + ti->path_has_filename = false; } - if (te && git_tree_entry__is_tree(te)) - error = tree_iterator__expand_tree(ti); + /* scan forward and up, advancing in frame or popping frame when done */ + while (!tree_iterator__move_to_next(ti, tf) && + tree_iterator__pop_frame(ti, false)) + tf = ti->head; - if (!error) - error = tree_iterator__current(self, entry); + /* find next and load trees */ + if ((error = tree_iterator__set_next(ti, tf)) < 0) + return error; - return error; + /* deal with include_trees / auto_expand as needed */ + if (!iterator__include_trees(ti) && tree_iterator__at_tree(ti)) + return tree_iterator__advance_into(entry, self); + + return tree_iterator__current(entry, self); } static int tree_iterator__seek(git_iterator *self, const char *prefix) { - GIT_UNUSED(self); - GIT_UNUSED(prefix); - /* pop stack until matches prefix */ - /* seek item in current frame matching prefix */ - /* push stack which matches prefix */ + GIT_UNUSED(self); GIT_UNUSED(prefix); return -1; } -static void tree_iterator__free(git_iterator *self) +static int tree_iterator__reset( + git_iterator *self, const char *start, const char *end) { tree_iterator *ti = (tree_iterator *)self; - while (ti->stack != NULL) - tree_iterator__pop_frame(ti); - git_buf_free(&ti->path); + + tree_iterator__pop_all(ti, false, false); + + if (iterator__reset_range(self, start, end) < 0) + return -1; + + return tree_iterator__push_frame(ti); /* re-expand root tree */ +} + +static int tree_iterator__at_end(git_iterator *self) +{ + tree_iterator *ti = (tree_iterator *)self; + return (ti->head->current >= ti->head->n_entries); } -static int tree_iterator__reset(git_iterator *self) +static void tree_iterator__free(git_iterator *self) { tree_iterator *ti = (tree_iterator *)self; - while (ti->stack && ti->stack->next) - tree_iterator__pop_frame(ti); + tree_iterator__pop_all(ti, true, false); + + git_tree_free(ti->head->entries[0]->tree); + git__free(ti->head); + git_pool_clear(&ti->pool); + git_buf_free(&ti->path); +} + +static int tree_iterator__create_root_frame(tree_iterator *ti, git_tree *tree) +{ + size_t sz = sizeof(tree_iterator_frame) + sizeof(tree_iterator_entry); + tree_iterator_frame *root = git__calloc(sz, sizeof(char)); + GITERR_CHECK_ALLOC(root); - if (ti->stack) - ti->stack->index = - git_tree__prefix_position(ti->stack->tree, ti->base.start); + root->n_entries = 1; + root->next = 1; + root->start = ti->base.start; + root->startlen = root->start ? strlen(root->start) : 0; + root->entries[0] = git_pool_mallocz(&ti->pool, 1); + GITERR_CHECK_ALLOC(root->entries[0]); + root->entries[0]->tree = tree; - git_buf_clear(&ti->path); + ti->head = ti->root = root; - return tree_iterator__expand_tree(ti); + return 0; } -int git_iterator_for_tree_range( +int git_iterator_for_tree( git_iterator **iter, - git_repository *repo, git_tree *tree, + git_iterator_flag_t flags, const char *start, const char *end) { @@ -306,42 +576,124 @@ int git_iterator_for_tree_range( tree_iterator *ti; if (tree == NULL) - return git_iterator_for_nothing(iter); + return git_iterator_for_nothing(iter, flags, start, end); - ITERATOR_BASE_INIT(ti, tree, TREE); + if ((error = git_object_dup((git_object **)&tree, (git_object *)tree)) < 0) + return error; - ti->repo = repo; - ti->stack = tree_iterator__alloc_frame(tree, ti->base.start); + ITERATOR_BASE_INIT(ti, tree, TREE, git_tree_owner(tree)); - if ((error = tree_iterator__expand_tree(ti)) < 0) - git_iterator_free((git_iterator *)ti); - else - *iter = (git_iterator *)ti; + if ((error = iterator__update_ignore_case((git_iterator *)ti, flags)) < 0) + goto fail; + ti->strncomp = iterator__ignore_case(ti) ? git__strncasecmp : git__strncmp; + + if ((error = git_pool_init(&ti->pool, sizeof(tree_iterator_entry),0)) < 0 || + (error = tree_iterator__create_root_frame(ti, tree)) < 0 || + (error = tree_iterator__push_frame(ti)) < 0) /* expand root now */ + goto fail; + *iter = (git_iterator *)ti; + return 0; + +fail: + git_iterator_free((git_iterator *)ti); return error; } typedef struct { git_iterator base; + git_iterator_callbacks cb; git_index *index; - unsigned int current; + size_t current; + /* when not in autoexpand mode, use these to represent "tree" state */ + git_buf partial; + size_t partial_pos; + char restore_terminator; + git_index_entry tree_entry; } index_iterator; -static int index_iterator__current( - git_iterator *self, const git_index_entry **entry) +static const git_index_entry *index_iterator__index_entry(index_iterator *ii) { - index_iterator *ii = (index_iterator *)self; - git_index_entry *ie = git_index_get(ii->index, ii->current); + const git_index_entry *ie = git_index_get_byindex(ii->index, ii->current); - if (ie != NULL && - ii->base.end != NULL && - git__prefixcmp(ie->path, ii->base.end) > 0) - { + if (ie != NULL && iterator__past_end(ii, ie->path)) { ii->current = git_index_entrycount(ii->index); ie = NULL; } + return ie; +} + +static const git_index_entry *index_iterator__skip_conflicts(index_iterator *ii) +{ + const git_index_entry *ie; + + while ((ie = index_iterator__index_entry(ii)) != NULL && + git_index_entry_stage(ie) != 0) + ii->current++; + + return ie; +} + +static void index_iterator__next_prefix_tree(index_iterator *ii) +{ + const char *slash; + + if (!iterator__include_trees(ii)) + return; + + slash = strchr(&ii->partial.ptr[ii->partial_pos], '/'); + + if (slash != NULL) { + ii->partial_pos = (slash - ii->partial.ptr) + 1; + ii->restore_terminator = ii->partial.ptr[ii->partial_pos]; + ii->partial.ptr[ii->partial_pos] = '\0'; + } else { + ii->partial_pos = ii->partial.size; + } + + if (index_iterator__index_entry(ii) == NULL) + ii->partial_pos = ii->partial.size; +} + +static int index_iterator__first_prefix_tree(index_iterator *ii) +{ + const git_index_entry *ie = index_iterator__skip_conflicts(ii); + const char *scan, *prior, *slash; + + if (!ie || !iterator__include_trees(ii)) + return 0; + + /* find longest common prefix with prior index entry */ + for (scan = slash = ie->path, prior = ii->partial.ptr; + *scan && *scan == *prior; ++scan, ++prior) + if (*scan == '/') + slash = scan; + + if (git_buf_sets(&ii->partial, ie->path) < 0) + return -1; + + ii->partial_pos = (slash - ie->path) + 1; + index_iterator__next_prefix_tree(ii); + + return 0; +} + +#define index_iterator__at_tree(I) \ + (iterator__include_trees(I) && (I)->partial_pos < (I)->partial.size) + +static int index_iterator__current( + const git_index_entry **entry, git_iterator *self) +{ + index_iterator *ii = (index_iterator *)self; + const git_index_entry *ie = git_index_get_byindex(ii->index, ii->current); + + if (ie != NULL && index_iterator__at_tree(ii)) { + ii->tree_entry.path = ii->partial.ptr; + ie = &ii->tree_entry; + } + if (entry) *entry = ie; @@ -355,28 +707,90 @@ static int index_iterator__at_end(git_iterator *self) } static int index_iterator__advance( - git_iterator *self, const git_index_entry **entry) + const git_index_entry **entry, git_iterator *self) { index_iterator *ii = (index_iterator *)self; + size_t entrycount = git_index_entrycount(ii->index); + const git_index_entry *ie; + + if (index_iterator__at_tree(ii)) { + if (iterator__do_autoexpand(ii)) { + ii->partial.ptr[ii->partial_pos] = ii->restore_terminator; + index_iterator__next_prefix_tree(ii); + } else { + /* advance to sibling tree (i.e. find entry with new prefix) */ + while (ii->current < entrycount) { + ii->current++; + + if (!(ie = git_index_get_byindex(ii->index, ii->current)) || + ii->base.prefixcomp(ie->path, ii->partial.ptr) != 0) + break; + } + + if (index_iterator__first_prefix_tree(ii) < 0) + return -1; + } + } else { + if (ii->current < entrycount) + ii->current++; - if (ii->current < git_index_entrycount(ii->index)) - ii->current++; + if (index_iterator__first_prefix_tree(ii) < 0) + return -1; + } + + return index_iterator__current(entry, self); +} + +static int index_iterator__advance_into( + const git_index_entry **entry, git_iterator *self) +{ + index_iterator *ii = (index_iterator *)self; + const git_index_entry *ie = git_index_get_byindex(ii->index, ii->current); + + if (ie != NULL && index_iterator__at_tree(ii)) { + if (ii->restore_terminator) + ii->partial.ptr[ii->partial_pos] = ii->restore_terminator; + index_iterator__next_prefix_tree(ii); + } - return index_iterator__current(self, entry); + return index_iterator__current(entry, self); } static int index_iterator__seek(git_iterator *self, const char *prefix) { - GIT_UNUSED(self); - GIT_UNUSED(prefix); - /* find last item before prefix */ + GIT_UNUSED(self); GIT_UNUSED(prefix); return -1; } -static int index_iterator__reset(git_iterator *self) +static int index_iterator__reset( + git_iterator *self, const char *start, const char *end) { index_iterator *ii = (index_iterator *)self; - ii->current = 0; + const git_index_entry *ie; + + if (iterator__reset_range(self, start, end) < 0) + return -1; + + ii->current = ii->base.start ? + git_index__prefix_position(ii->index, ii->base.start) : 0; + + if ((ie = index_iterator__skip_conflicts(ii)) == NULL) + return 0; + + if (git_buf_sets(&ii->partial, ie->path) < 0) + return -1; + + ii->partial_pos = 0; + + if (ii->base.start) { + size_t startlen = strlen(ii->base.start); + + ii->partial_pos = (startlen > ii->partial.size) ? + ii->partial.size : startlen; + } + + index_iterator__next_prefix_tree(ii); + return 0; } @@ -385,58 +799,98 @@ static void index_iterator__free(git_iterator *self) index_iterator *ii = (index_iterator *)self; git_index_free(ii->index); ii->index = NULL; + + git_buf_free(&ii->partial); } -int git_iterator_for_index_range( +int git_iterator_for_index( git_iterator **iter, - git_repository *repo, + git_index *index, + git_iterator_flag_t flags, const char *start, const char *end) { - int error; index_iterator *ii; - ITERATOR_BASE_INIT(ii, index, INDEX); + ITERATOR_BASE_INIT(ii, index, INDEX, git_index_owner(index)); - if ((error = git_repository_index(&ii->index, repo)) < 0) - git__free(ii); - else { - ii->current = start ? git_index__prefix_position(ii->index, start) : 0; - *iter = (git_iterator *)ii; + if (index->ignore_case) { + ii->base.flags |= GIT_ITERATOR_IGNORE_CASE; + ii->base.prefixcomp = git__prefixcmp_icase; } - return error; + ii->index = index; + GIT_REFCOUNT_INC(index); + + git_buf_init(&ii->partial, 0); + ii->tree_entry.mode = GIT_FILEMODE_TREE; + + index_iterator__reset((git_iterator *)ii, NULL, NULL); + + *iter = (git_iterator *)ii; + + return 0; } +#define WORKDIR_MAX_DEPTH 100 + typedef struct workdir_iterator_frame workdir_iterator_frame; struct workdir_iterator_frame { workdir_iterator_frame *next; git_vector entries; - unsigned int index; - char *start; + size_t index; }; typedef struct { git_iterator base; - git_repository *repo; - size_t root_len; + git_iterator_callbacks cb; workdir_iterator_frame *stack; git_ignores ignores; git_index_entry entry; git_buf path; + size_t root_len; int is_ignored; + int depth; } workdir_iterator; -static workdir_iterator_frame *workdir_iterator__alloc_frame(void) +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] == '/'); + } +} + +static workdir_iterator_frame *workdir_iterator__alloc_frame( + workdir_iterator *wi) { workdir_iterator_frame *wf = git__calloc(1, sizeof(workdir_iterator_frame)); + git_vector_cmp entry_compare = CASESELECT( + iterator__ignore_case(wi), + git_path_with_stat_cmp_icase, git_path_with_stat_cmp); + if (wf == NULL) return NULL; - if (git_vector_init(&wf->entries, 0, git_path_with_stat_cmp) != 0) { + + if (git_vector_init(&wf->entries, 0, entry_compare) != 0) { git__free(wf); return NULL; } + return wf; } @@ -453,53 +907,73 @@ static void workdir_iterator__free_frame(workdir_iterator_frame *wf) static int workdir_iterator__update_entry(workdir_iterator *wi); -static int workdir_iterator__entry_cmp(const void *prefix, const void *item) +static int workdir_iterator__entry_cmp(const void *i, const void *item) { + const workdir_iterator *wi = (const workdir_iterator *)i; const git_path_with_stat *ps = item; - return git__prefixcmp((const char *)prefix, ps->path); + return wi->base.prefixcomp(wi->base.start, ps->path); +} + +static void workdir_iterator__seek_frame_start( + workdir_iterator *wi, workdir_iterator_frame *wf) +{ + if (!wf) + return; + + if (wi->base.start) + git_vector_bsearch2( + &wf->index, &wf->entries, workdir_iterator__entry_cmp, wi); + else + wf->index = 0; + + if (path_is_dotgit(git_vector_get(&wf->entries, wf->index))) + wf->index++; } static int workdir_iterator__expand_dir(workdir_iterator *wi) { int error; - workdir_iterator_frame *wf = workdir_iterator__alloc_frame(); + workdir_iterator_frame *wf; + + wf = workdir_iterator__alloc_frame(wi); GITERR_CHECK_ALLOC(wf); - error = git_path_dirload_with_stat(wi->path.ptr, wi->root_len, &wf->entries); + error = git_path_dirload_with_stat( + wi->path.ptr, wi->root_len, iterator__ignore_case(wi), + wi->base.start, wi->base.end, &wf->entries); + if (error < 0 || wf->entries.length == 0) { workdir_iterator__free_frame(wf); return GIT_ENOTFOUND; } - git_vector_sort(&wf->entries); - - if (!wi->stack) - wf->start = wi->base.start; - else if (wi->stack->start && - git__prefixcmp(wi->stack->start, wi->path.ptr + wi->root_len) == 0) - wf->start = wi->stack->start; - - if (wf->start) - git_vector_bsearch3( - &wf->index, &wf->entries, workdir_iterator__entry_cmp, wf->start); + 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; + } - wf->next = wi->stack; - wi->stack = wf; + workdir_iterator__seek_frame_start(wi, wf); /* only push new ignores if this is not top level directory */ - if (wi->stack->next != NULL) { + 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]); } + wf->next = wi->stack; + wi->stack = wf; + return workdir_iterator__update_entry(wi); } static int workdir_iterator__current( - git_iterator *self, const git_index_entry **entry) + const git_index_entry **entry, git_iterator *self) { workdir_iterator *wi = (workdir_iterator *)self; - *entry = (wi->entry.path == NULL) ? NULL : &wi->entry; + if (entry) + *entry = (wi->entry.path == NULL) ? NULL : &wi->entry; return 0; } @@ -508,44 +982,83 @@ static int workdir_iterator__at_end(git_iterator *self) return (((workdir_iterator *)self)->entry.path == NULL); } +static int workdir_iterator__advance_into( + const git_index_entry **entry, git_iterator *iter) +{ + int error = 0; + workdir_iterator *wi = (workdir_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 + */ + if (wi->entry.path != NULL && + (wi->entry.mode == GIT_FILEMODE_TREE || + wi->entry.mode == GIT_FILEMODE_COMMIT)) + /* returns GIT_ENOTFOUND if the directory is empty */ + error = workdir_iterator__expand_dir(wi); + + if (!error && entry) + error = workdir_iterator__current(entry, iter); + + return error; +} + static int workdir_iterator__advance( - git_iterator *self, const git_index_entry **entry) + const git_index_entry **entry, git_iterator *self) { - int error; + int error = 0; workdir_iterator *wi = (workdir_iterator *)self; workdir_iterator_frame *wf; 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; - if (wi->entry.path == NULL) - return 0; - - while ((wf = wi->stack) != NULL) { + while (wi->entry.path != NULL) { + wf = wi->stack; next = git_vector_get(&wf->entries, ++wf->index); + if (next != NULL) { - if (strcmp(next->path, DOT_GIT "/") == 0) + /* match git's behavior of ignoring anything named ".git" */ + if (path_is_dotgit(next)) continue; /* else found a good entry */ break; } - /* pop workdir directory stack */ - wi->stack = wf->next; - workdir_iterator__free_frame(wf); - git_ignore__pop_dir(&wi->ignores); - - if (wi->stack == NULL) { + /* 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); } error = workdir_iterator__update_entry(wi); if (!error && entry != NULL) - error = workdir_iterator__current(self, entry); + error = workdir_iterator__current(entry, self); return error; } @@ -560,18 +1073,25 @@ static int workdir_iterator__seek(git_iterator *self, const char *prefix) return 0; } -static int workdir_iterator__reset(git_iterator *self) +static int workdir_iterator__reset( + git_iterator *self, const char *start, const char *end) { workdir_iterator *wi = (workdir_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); } - if (wi->stack) - wi->stack->index = 0; - return 0; + wi->depth = 0; + + if (iterator__reset_range(self, start, end) < 0) + return -1; + + workdir_iterator__seek_frame_start(wi, wi->stack); + + return workdir_iterator__update_entry(wi); } static void workdir_iterator__free(git_iterator *self) @@ -590,7 +1110,9 @@ static void workdir_iterator__free(git_iterator *self) static int workdir_iterator__update_entry(workdir_iterator *wi) { - git_path_with_stat *ps = git_vector_get(&wi->stack->entries, wi->stack->index); + int error = 0; + git_path_with_stat *ps = + git_vector_get(&wi->stack->entries, wi->stack->index); git_buf_truncate(&wi->path, wi->root_len); memset(&wi->entry, 0, sizeof(wi->entry)); @@ -598,62 +1120,62 @@ static int workdir_iterator__update_entry(workdir_iterator *wi) if (!ps) return 0; + /* skip over .git entries */ + if (path_is_dotgit(ps)) + return workdir_iterator__advance(NULL, (git_iterator *)wi); + if (git_buf_put(&wi->path, ps->path, ps->path_len) < 0) return -1; - if (wi->base.end && - git__prefixcmp(wi->path.ptr + wi->root_len, wi->base.end) > 0) + if (iterator__past_end(wi, wi->path.ptr + wi->root_len)) return 0; wi->entry.path = ps->path; - /* skip over .git directory */ - if (strcmp(ps->path, DOT_GIT "/") == 0) - return workdir_iterator__advance((git_iterator *)wi, NULL); + wi->is_ignored = -1; - /* if there is an error processing the entry, treat as ignored */ - wi->is_ignored = 1; - - git_index__init_entry_from_stat(&ps->st, &wi->entry); + git_index_entry__init_from_stat(&wi->entry, &ps->st); /* need different mode here to keep directories during iteration */ wi->entry.mode = git_futils_canonical_mode(ps->st.st_mode); /* if this is a file type we don't handle, treat as ignored */ - if (wi->entry.mode == 0) + if (wi->entry.mode == 0) { + wi->is_ignored = 1; return 0; + } - /* okay, we are far enough along to look up real ignore rule */ - if (git_ignore__lookup(&wi->ignores, wi->entry.path, &wi->is_ignored) < 0) - return 0; /* if error, ignore it and ignore file */ + /* if this isn't a tree, then we're done */ + if (wi->entry.mode != GIT_FILEMODE_TREE) + return 0; /* detect submodules */ - if (S_ISDIR(wi->entry.mode)) { - bool is_submodule = git_path_contains(&wi->path, DOT_GIT); - - /* if there is no .git, still check submodules data */ - if (!is_submodule) { - int res = git_submodule_lookup(NULL, wi->repo, wi->entry.path); - is_submodule = (res == 0); - if (res == GIT_ENOTFOUND) - giterr_clear(); - } - - /* if submodule, mark as GITLINK and remove trailing slash */ - if (is_submodule) { - size_t len = strlen(wi->entry.path); - assert(wi->entry.path[len - 1] == '/'); - wi->entry.path[len - 1] = '\0'; - wi->entry.mode = S_IFGITLINK; - } + error = git_submodule_lookup(NULL, wi->base.repo, wi->entry.path); + if (error == GIT_ENOTFOUND) + giterr_clear(); + + if (error == GIT_EEXISTS) /* if contains .git, treat as untracked submod */ + error = 0; + + /* 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; + return 0; } - return 0; + if (iterator__include_trees(wi)) + return 0; + + return workdir_iterator__advance(NULL, (git_iterator *)wi); } -int git_iterator_for_workdir_range( +int git_iterator_for_workdir( git_iterator **iter, git_repository *repo, + git_iterator_flag_t flags, const char *start, const char *end) { @@ -662,15 +1184,14 @@ int git_iterator_for_workdir_range( assert(iter && repo); - if (git_repository_is_bare(repo)) { - giterr_set(GITERR_INVALID, - "Cannot scan working directory for bare repo"); - return -1; - } + if ((error = git_repository__ensure_not_bare( + repo, "scan working directory")) < 0) + return error; - ITERATOR_BASE_INIT(wi, workdir, WORKDIR); + ITERATOR_BASE_INIT(wi, workdir, WORKDIR, repo); - wi->repo = repo; + if ((error = iterator__update_ignore_case((git_iterator *)wi, flags)) < 0) + goto fail; if (git_buf_sets(&wi->path, git_repository_workdir(repo)) < 0 || git_path_to_dir(&wi->path) < 0 || @@ -679,70 +1200,150 @@ int git_iterator_for_workdir_range( git__free(wi); return -1; } - wi->root_len = wi->path.size; if ((error = workdir_iterator__expand_dir(wi)) < 0) { - if (error == GIT_ENOTFOUND) - error = 0; - else { - git_iterator_free((git_iterator *)wi); - wi = NULL; - } + if (error != GIT_ENOTFOUND) + goto fail; + giterr_clear(); } *iter = (git_iterator *)wi; + return 0; +fail: + git_iterator_free((git_iterator *)wi); return error; } +void git_iterator_free(git_iterator *iter) +{ + if (iter == NULL) + return; + + iter->cb->free(iter); + + git__free(iter->start); + git__free(iter->end); + + memset(iter, 0, sizeof(*iter)); + + git__free(iter); +} + +int git_iterator_set_ignore_case(git_iterator *iter, bool ignore_case) +{ + bool desire_ignore_case = (ignore_case != 0); + + if (iterator__ignore_case(iter) == desire_ignore_case) + return 0; + + if (iter->type == GIT_ITERATOR_TYPE_EMPTY) { + if (desire_ignore_case) + iter->flags |= GIT_ITERATOR_IGNORE_CASE; + else + iter->flags &= ~GIT_ITERATOR_IGNORE_CASE; + } else { + giterr_set(GITERR_INVALID, + "Cannot currently set ignore case on non-empty iterators"); + return -1; + } + + return 0; +} + +git_index *git_iterator_get_index(git_iterator *iter) +{ + if (iter->type == GIT_ITERATOR_TYPE_INDEX) + return ((index_iterator *)iter)->index; + return NULL; +} + int git_iterator_current_tree_entry( - git_iterator *iter, const git_tree_entry **tree_entry) + const git_tree_entry **tree_entry, git_iterator *iter) { - *tree_entry = (iter->type != GIT_ITERATOR_TREE) ? NULL : - tree_iterator__tree_entry((tree_iterator *)iter); + if (iter->type != GIT_ITERATOR_TYPE_TREE) + *tree_entry = NULL; + else { + tree_iterator_frame *tf = ((tree_iterator *)iter)->head; + *tree_entry = (tf->current < tf->n_entries) ? + tf->entries[tf->current]->te : NULL; + } + return 0; } -int git_iterator_current_is_ignored(git_iterator *iter) +int git_iterator_current_parent_tree( + const git_tree **tree_ptr, + git_iterator *iter, + const char *parent_path) { - return (iter->type != GIT_ITERATOR_WORKDIR) ? 0 : - ((workdir_iterator *)iter)->is_ignored; + tree_iterator *ti = (tree_iterator *)iter; + tree_iterator_frame *tf; + const char *scan = parent_path; + const git_tree_entry *te; + + *tree_ptr = NULL; + + if (iter->type != GIT_ITERATOR_TYPE_TREE) + return 0; + + for (tf = ti->root; *scan; ) { + if (!(tf = tf->down) || + tf->current >= tf->n_entries || + !(te = tf->entries[tf->current]->te) || + ti->strncomp(scan, te->filename, te->filename_len) != 0) + return 0; + + scan += te->filename_len; + if (*scan == '/') + scan++; + } + + *tree_ptr = tf->entries[tf->current]->tree; + return 0; } -int git_iterator_advance_into_directory( - git_iterator *iter, const git_index_entry **entry) +bool git_iterator_current_is_ignored(git_iterator *iter) { workdir_iterator *wi = (workdir_iterator *)iter; - if (iter->type == GIT_ITERATOR_WORKDIR && - wi->entry.path && - S_ISDIR(wi->entry.mode) && - !S_ISGITLINK(wi->entry.mode)) - { - if (workdir_iterator__expand_dir(wi) < 0) - /* if error loading or if empty, skip the directory. */ - return workdir_iterator__advance(iter, entry); - } + if (iter->type != GIT_ITERATOR_TYPE_WORKDIR) + return false; + + if (wi->is_ignored != -1) + return (bool)(wi->is_ignored != 0); - return entry ? git_iterator_current(iter, entry) : 0; + if (git_ignore__lookup(&wi->ignores, wi->entry.path, &wi->is_ignored) < 0) + wi->is_ignored = true; + + return (bool)wi->is_ignored; } -int git_iterator_cmp( - git_iterator *iter, const char *path_prefix) +int git_iterator_cmp(git_iterator *iter, const char *path_prefix) { const git_index_entry *entry; /* a "done" iterator is after every prefix */ - if (git_iterator_current(iter, &entry) < 0 || - entry == NULL) + if (git_iterator_current(&entry, iter) < 0 || entry == NULL) return 1; /* a NULL prefix is after any valid iterator */ if (!path_prefix) return -1; - return git__prefixcmp(entry->path, path_prefix); + return iter->prefixcomp(entry->path, path_prefix); } +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) + *path = NULL; + else + *path = &wi->path; + + return 0; +} diff --git a/src/iterator.h b/src/iterator.h index b916a9080..4a4e6a9d8 100644 --- a/src/iterator.h +++ b/src/iterator.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -9,143 +9,197 @@ #include "common.h" #include "git2/index.h" +#include "vector.h" +#include "buffer.h" typedef struct git_iterator git_iterator; typedef enum { - GIT_ITERATOR_EMPTY = 0, - GIT_ITERATOR_TREE = 1, - GIT_ITERATOR_INDEX = 2, - GIT_ITERATOR_WORKDIR = 3 + GIT_ITERATOR_TYPE_EMPTY = 0, + GIT_ITERATOR_TYPE_TREE = 1, + GIT_ITERATOR_TYPE_INDEX = 2, + GIT_ITERATOR_TYPE_WORKDIR = 3, } git_iterator_type_t; +typedef enum { + /** ignore case for entry sort order */ + GIT_ITERATOR_IGNORE_CASE = (1 << 0), + /** force case sensitivity for entry sort order */ + GIT_ITERATOR_DONT_IGNORE_CASE = (1 << 1), + /** return tree items in addition to blob items */ + GIT_ITERATOR_INCLUDE_TREES = (1 << 2), + /** don't flatten trees, requiring advance_into (implies INCLUDE_TREES) */ + GIT_ITERATOR_DONT_AUTOEXPAND = (1 << 3), +} git_iterator_flag_t; + +typedef struct { + int (*current)(const git_index_entry **, git_iterator *); + int (*advance)(const git_index_entry **, git_iterator *); + int (*advance_into)(const git_index_entry **, git_iterator *); + int (*seek)(git_iterator *, const char *prefix); + int (*reset)(git_iterator *, const char *start, const char *end); + int (*at_end)(git_iterator *); + void (*free)(git_iterator *); +} git_iterator_callbacks; + struct git_iterator { git_iterator_type_t type; + git_iterator_callbacks *cb; + git_repository *repo; char *start; char *end; - int (*current)(git_iterator *, const git_index_entry **); - int (*at_end)(git_iterator *); - int (*advance)(git_iterator *, const git_index_entry **); - int (*seek)(git_iterator *, const char *prefix); - int (*reset)(git_iterator *); - void (*free)(git_iterator *); + int (*prefixcomp)(const char *str, const char *prefix); + unsigned int flags; }; -extern int git_iterator_for_nothing(git_iterator **iter); - -extern int git_iterator_for_tree_range( - git_iterator **iter, git_repository *repo, git_tree *tree, - const char *start, const char *end); - -GIT_INLINE(int) git_iterator_for_tree( - git_iterator **iter, git_repository *repo, git_tree *tree) -{ - return git_iterator_for_tree_range(iter, repo, tree, NULL, NULL); -} - -extern int git_iterator_for_index_range( - git_iterator **iter, git_repository *repo, - const char *start, const char *end); - -GIT_INLINE(int) git_iterator_for_index( - git_iterator **iter, git_repository *repo) -{ - return git_iterator_for_index_range(iter, repo, NULL, NULL); -} +extern int git_iterator_for_nothing( + git_iterator **out, + git_iterator_flag_t flags, + const char *start, + const char *end); -extern int git_iterator_for_workdir_range( - git_iterator **iter, git_repository *repo, - const char *start, const char *end); - -GIT_INLINE(int) git_iterator_for_workdir( - git_iterator **iter, git_repository *repo) -{ - return git_iterator_for_workdir_range(iter, repo, NULL, NULL); -} +/* tree 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_tree( + git_iterator **out, + git_tree *tree, + git_iterator_flag_t flags, + const char *start, + const char *end); + +/* index iterators will take the ignore_case value from the index; the + * ignore_case flags are not used + */ +extern int git_iterator_for_index( + git_iterator **out, + git_index *index, + 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_iterator **out, + git_repository *repo, + git_iterator_flag_t flags, + const char *start, + const char *end); +extern void git_iterator_free(git_iterator *iter); -/* Entry is not guaranteed to be fully populated. For a tree iterator, - * we will only populate the mode, oid and path, for example. For a workdir - * iterator, we will not populate the oid. +/* Return a git_index_entry structure for the current value the iterator + * is looking at or NULL if the iterator is at the end. + * + * The entry may noy be fully populated. Tree iterators will only have a + * value mode, OID, and path. Workdir iterators will not have an OID (but + * you can use `git_iterator_current_oid()` to calculate it on demand). * * You do not need to free the entry. It is still "owned" by the iterator. - * Once you call `git_iterator_advance`, then content of the old entry is - * no longer guaranteed to be valid. + * Once you call `git_iterator_advance()` then the old entry is no longer + * guaranteed to be valid - it may be freed or just overwritten in place. */ GIT_INLINE(int) git_iterator_current( - git_iterator *iter, const git_index_entry **entry) + const git_index_entry **entry, git_iterator *iter) { - return iter->current(iter, entry); + return iter->cb->current(entry, iter); } -GIT_INLINE(int) git_iterator_at_end(git_iterator *iter) +/** + * Advance to the next item for the iterator. + * + * If GIT_ITERATOR_INCLUDE_TREES is set, this may be a tree item. If + * GIT_ITERATOR_DONT_AUTOEXPAND is set, calling this again when on a tree + * item will skip over all the items under that tree. + */ +GIT_INLINE(int) git_iterator_advance( + const git_index_entry **entry, git_iterator *iter) { - return iter->at_end(iter); + return iter->cb->advance(entry, iter); } -GIT_INLINE(int) git_iterator_advance( - git_iterator *iter, const git_index_entry **entry) +/** + * Iterate into a tree item (when GIT_ITERATOR_DONT_AUTOEXPAND is set). + * + * git_iterator_advance() steps through all items being iterated over + * (either with or without trees, depending on GIT_ITERATOR_INCLUDE_TREES), + * but if GIT_ITERATOR_DONT_AUTOEXPAND is set, it will skip to the next + * sibling of a tree instead of going to the first child of the tree. In + * that case, use this function to advance to the first child of the tree. + * + * 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. + */ +GIT_INLINE(int) git_iterator_advance_into( + const git_index_entry **entry, git_iterator *iter) { - return iter->advance(iter, entry); + return iter->cb->advance_into(entry, iter); } GIT_INLINE(int) git_iterator_seek( git_iterator *iter, const char *prefix) { - return iter->seek(iter, prefix); + return iter->cb->seek(iter, prefix); } -GIT_INLINE(int) git_iterator_reset(git_iterator *iter) +GIT_INLINE(int) git_iterator_reset( + git_iterator *iter, const char *start, const char *end) { - return iter->reset(iter); + return iter->cb->reset(iter, start, end); } -GIT_INLINE(void) git_iterator_free(git_iterator *iter) +GIT_INLINE(int) git_iterator_at_end(git_iterator *iter) { - if (iter == NULL) - return; - - iter->free(iter); + return iter->cb->at_end(iter); +} - git__free(iter->start); - git__free(iter->end); +GIT_INLINE(git_iterator_type_t) git_iterator_type(git_iterator *iter) +{ + return iter->type; +} - memset(iter, 0, sizeof(*iter)); +GIT_INLINE(git_repository *) git_iterator_owner(git_iterator *iter) +{ + return iter->repo; +} - git__free(iter); +GIT_INLINE(git_iterator_flag_t) git_iterator_flags(git_iterator *iter) +{ + return iter->flags; } -GIT_INLINE(git_iterator_type_t) git_iterator_type(git_iterator *iter) +GIT_INLINE(bool) git_iterator_ignore_case(git_iterator *iter) { - return iter->type; + return ((iter->flags & GIT_ITERATOR_IGNORE_CASE) != 0); } +extern int git_iterator_set_ignore_case(git_iterator *iter, bool ignore_case); + extern int git_iterator_current_tree_entry( - git_iterator *iter, const git_tree_entry **tree_entry); + const git_tree_entry **entry_out, git_iterator *iter); -extern int git_iterator_current_is_ignored(git_iterator *iter); +extern int git_iterator_current_parent_tree( + const git_tree **tree_out, git_iterator *iter, const char *parent_path); -/** - * Iterate into a workdir directory. - * - * Workdir iterators do not automatically descend into directories (so that - * when comparing two iterator entries you can detect a newly created - * directory in the workdir). As a result, you may get S_ISDIR items from - * a workdir iterator. If you wish to iterate over the contents of the - * directories you encounter, then call this function when you encounter - * a directory. - * - * If there are no files in the directory, this will end up acting like a - * regular advance and will skip past the directory, so you should be - * prepared for that case. - * - * On non-workdir iterators or if not pointing at a directory, this is a - * no-op and will not advance the iterator. - */ -extern int git_iterator_advance_into_directory( - git_iterator *iter, const git_index_entry **entry); +extern bool git_iterator_current_is_ignored(git_iterator *iter); extern int git_iterator_cmp( git_iterator *iter, const char *path_prefix); +/** + * Get full path of the current item from a workdir iterator. This will + * return NULL for a non-workdir iterator. The git_buf is still owned by + * the iterator; this is exposed just for efficiency. + */ +extern int git_iterator_current_workdir_path( + git_buf **path, git_iterator *iter); + +/* Return index pointer if index iterator, else NULL */ +extern git_index *git_iterator_get_index(git_iterator *iter); + #endif diff --git a/src/khash.h b/src/khash.h index bd67fe1f7..242204464 100644 --- a/src/khash.h +++ b/src/khash.h @@ -131,7 +131,9 @@ typedef unsigned long long khint64_t; #endif #ifdef _MSC_VER -#define inline __inline +#define kh_inline __inline +#else +#define kh_inline inline #endif typedef khint32_t khint_t; @@ -345,7 +347,7 @@ static const double __ac_HASH_UPPER = 0.77; __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) #define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ - KHASH_INIT2(name, static inline, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) + KHASH_INIT2(name, static kh_inline, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) /* --- BEGIN OF HASH FUNCTIONS --- */ @@ -374,7 +376,7 @@ static const double __ac_HASH_UPPER = 0.77; @param s Pointer to a null terminated string @return The hash value */ -static inline khint_t __ac_X31_hash_string(const char *s) +static kh_inline khint_t __ac_X31_hash_string(const char *s) { khint_t h = (khint_t)*s; if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s; @@ -391,7 +393,7 @@ static inline khint_t __ac_X31_hash_string(const char *s) */ #define kh_str_hash_equal(a, b) (strcmp(a, b) == 0) -static inline khint_t __ac_Wang_hash(khint_t key) +static kh_inline khint_t __ac_Wang_hash(khint_t key) { key += ~(key << 15); key ^= (key >> 10); @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -23,6 +23,10 @@ #define GIT_MAP_TYPE 0xf #define GIT_MAP_FIXED 0x10 +#ifdef __amigaos4__ +#define MAP_FAILED 0 +#endif + typedef struct { /* memory mapped buffer */ void *data; /* data bytes */ size_t len; /* data length */ diff --git a/src/merge.c b/src/merge.c new file mode 100644 index 000000000..e0010d6a4 --- /dev/null +++ b/src/merge.c @@ -0,0 +1,296 @@ +/* + * 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 "repository.h" +#include "revwalk.h" +#include "buffer.h" +#include "merge.h" +#include "refs.h" +#include "git2/repository.h" +#include "git2/merge.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); + + 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; +} + +int git_merge_base_many(git_oid *out, git_repository *repo, const git_oid input_array[], size_t length) +{ + git_revwalk *walk; + git_vector list; + git_commit_list *result = NULL; + int error = -1; + unsigned int i; + git_commit_list_node *commit; + + assert(out && repo && input_array); + + if (length < 2) { + giterr_set(GITERR_INVALID, "At least two commits are required to find an ancestor. Provided 'length' was %u.", length); + return -1; + } + + if (git_vector_init(&list, length - 1, NULL) < 0) + return -1; + + if (git_revwalk_new(&walk, repo) < 0) + goto cleanup; + + for (i = 1; i < length; i++) { + commit = git_revwalk__commit_lookup(walk, &input_array[i]); + if (commit == NULL) + goto cleanup; + + git_vector_insert(&list, commit); + } + + commit = git_revwalk__commit_lookup(walk, &input_array[0]); + if (commit == NULL) + goto cleanup; + + if (git_merge__bases_many(&result, walk, commit, &list) < 0) + goto cleanup; + + if (!result) { + error = GIT_ENOTFOUND; + goto cleanup; + } + + git_oid_cpy(out, &result->item->oid); + + error = 0; + +cleanup: + git_commit_list_free(&result); + git_revwalk_free(walk); + git_vector_free(&list); + return error; +} + +int git_merge_base(git_oid *out, git_repository *repo, const git_oid *one, const git_oid *two) +{ + git_revwalk *walk; + git_vector list; + git_commit_list *result = NULL; + git_commit_list_node *commit; + void *contents[1]; + + if (git_revwalk_new(&walk, repo) < 0) + return -1; + + commit = git_revwalk__commit_lookup(walk, two); + if (commit == NULL) + goto on_error; + + /* This is just one value, so we can do it on the stack */ + memset(&list, 0x0, sizeof(git_vector)); + contents[0] = commit; + list.length = 1; + list.contents = contents; + + commit = git_revwalk__commit_lookup(walk, one); + if (commit == NULL) + goto on_error; + + if (git_merge__bases_many(&result, walk, commit, &list) < 0) + goto on_error; + + if (!result) { + git_revwalk_free(walk); + giterr_clear(); + return GIT_ENOTFOUND; + } + + git_oid_cpy(out, &result->item->oid); + git_commit_list_free(&result); + git_revwalk_free(walk); + + return 0; + +on_error: + git_revwalk_free(walk); + return -1; +} + +static int interesting(git_pqueue *list) +{ + unsigned int i; + /* element 0 isn't used - we need to start at 1 */ + for (i = 1; i < list->size; i++) { + git_commit_list_node *commit = list->d[i]; + if ((commit->flags & STALE) == 0) + return 1; + } + + return 0; +} + +int git_merge__bases_many(git_commit_list **out, git_revwalk *walk, git_commit_list_node *one, git_vector *twos) +{ + int error; + unsigned int i; + git_commit_list_node *two; + git_commit_list *result = NULL, *tmp = NULL; + git_pqueue list; + + /* if the commit is repeated, we have a our merge base already */ + git_vector_foreach(twos, i, two) { + if (one == two) + return git_commit_list_insert(one, out) ? 0 : -1; + } + + if (git_pqueue_init(&list, twos->length * 2, git_commit_list_time_cmp) < 0) + return -1; + + if (git_commit_list_parse(walk, one) < 0) + return -1; + + one->flags |= PARENT1; + if (git_pqueue_insert(&list, one) < 0) + return -1; + + git_vector_foreach(twos, i, two) { + git_commit_list_parse(walk, two); + two->flags |= PARENT2; + if (git_pqueue_insert(&list, two) < 0) + return -1; + } + + /* as long as there are non-STALE commits */ + while (interesting(&list)) { + git_commit_list_node *commit; + int flags; + + commit = git_pqueue_pop(&list); + + flags = commit->flags & (PARENT1 | PARENT2 | STALE); + if (flags == (PARENT1 | PARENT2)) { + if (!(commit->flags & RESULT)) { + commit->flags |= RESULT; + if (git_commit_list_insert(commit, &result) == NULL) + return -1; + } + /* we mark the parents of a merge stale */ + flags |= STALE; + } + + for (i = 0; i < commit->out_degree; i++) { + git_commit_list_node *p = commit->parents[i]; + if ((p->flags & flags) == flags) + continue; + + if ((error = git_commit_list_parse(walk, p)) < 0) + return error; + + p->flags |= flags; + if (git_pqueue_insert(&list, p) < 0) + return -1; + } + } + + git_pqueue_free(&list); + + /* filter out any stale commits in the results */ + tmp = result; + result = NULL; + + while (tmp) { + struct git_commit_list *next = tmp->next; + if (!(tmp->item->flags & STALE)) + if (git_commit_list_insert_by_date(tmp->item, &result) == NULL) + return -1; + + git__free(tmp); + tmp = next; + } + + *out = result; + return 0; +} + +int git_repository_mergehead_foreach(git_repository *repo, + git_repository_mergehead_foreach_cb cb, + void *payload) +{ + git_buf merge_head_path = GIT_BUF_INIT, merge_head_file = GIT_BUF_INIT; + char *buffer, *line; + size_t line_num = 1; + git_oid oid; + int error = 0; + + assert(repo && cb); + + if ((error = git_buf_joinpath(&merge_head_path, repo->path_repository, + GIT_MERGE_HEAD_FILE)) < 0) + return error; + + if ((error = git_futils_readbuffer(&merge_head_file, + git_buf_cstr(&merge_head_path))) < 0) + goto cleanup; + + buffer = merge_head_file.ptr; + + while ((line = git__strsep(&buffer, "\n")) != NULL) { + if (strlen(line) != GIT_OID_HEXSZ) { + giterr_set(GITERR_INVALID, "Unable to parse OID - invalid length"); + error = -1; + goto cleanup; + } + + if ((error = git_oid_fromstr(&oid, line)) < 0) + goto cleanup; + + if (cb(&oid, payload) < 0) { + error = GIT_EUSER; + goto cleanup; + } + + ++line_num; + } + + if (*buffer) { + giterr_set(GITERR_MERGE, "No EOL at line %d", line_num); + error = -1; + goto cleanup; + } + +cleanup: + git_buf_free(&merge_head_path); + git_buf_free(&merge_head_file); + + return error; +} diff --git a/src/merge.h b/src/merge.h new file mode 100644 index 000000000..22c644270 --- /dev/null +++ b/src/merge.h @@ -0,0 +1,22 @@ +/* + * 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_merge_h__ +#define INCLUDE_merge_h__ + +#include "git2/types.h" +#include "git2/merge.h" +#include "commit_list.h" +#include "vector.h" + +#define GIT_MERGE_MSG_FILE "MERGE_MSG" +#define GIT_MERGE_MODE_FILE "MERGE_MODE" + +#define MERGE_CONFIG_FILE_MODE 0666 + +int git_merge__bases_many(git_commit_list **out, git_revwalk *walk, git_commit_list_node *one, git_vector *twos); + +#endif diff --git a/src/message.c b/src/message.c index aa0220fd0..0eff426f2 100644 --- a/src/message.c +++ b/src/message.c @@ -1,12 +1,11 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ #include "message.h" -#include <ctype.h> static size_t line_length_without_trailing_spaces(const char *line, size_t len) { @@ -22,7 +21,7 @@ static size_t line_length_without_trailing_spaces(const char *line, size_t len) /* Greatly inspired from git.git "stripspace" */ /* see https://github.com/git/git/blob/497215d8811ac7b8955693ceaad0899ecd894ed2/builtin/stripspace.c#L4-67 */ -int git_message_prettify(git_buf *message_out, const char *message, int strip_comments) +int git_message__prettify(git_buf *message_out, const char *message, int strip_comments) { const size_t message_len = strlen(message); @@ -59,3 +58,29 @@ int git_message_prettify(git_buf *message_out, const char *message, int strip_co return git_buf_oom(message_out) ? -1 : 0; } + +int git_message_prettify(char *message_out, size_t buffer_size, const char *message, int strip_comments) +{ + git_buf buf = GIT_BUF_INIT; + ssize_t out_size = -1; + + if (message_out && buffer_size) + *message_out = '\0'; + + if (git_message__prettify(&buf, message, strip_comments) < 0) + goto done; + + if (message_out && buf.size + 1 > buffer_size) { /* +1 for NUL byte */ + giterr_set(GITERR_INVALID, "Buffer too short to hold the cleaned message"); + goto done; + } + + if (message_out) + git_buf_copy_cstr(message_out, buffer_size, &buf); + + out_size = buf.size + 1; + +done: + git_buf_free(&buf); + return (int)out_size; +} diff --git a/src/message.h b/src/message.h index ddfa13e18..3c4b8dc45 100644 --- a/src/message.h +++ b/src/message.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -7,8 +7,9 @@ #ifndef INCLUDE_message_h__ #define INCLUDE_message_h__ +#include "git2/message.h" #include "buffer.h" -int git_message_prettify(git_buf *message_out, const char *message, int strip_comments); +int git_message__prettify(git_buf *message_out, const char *message, int strip_comments); #endif /* INCLUDE_message_h__ */ diff --git a/src/mwindow.c b/src/mwindow.c index b59c4d2f7..b35503d46 100644 --- a/src/mwindow.c +++ b/src/mwindow.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -20,17 +20,11 @@ #define DEFAULT_MAPPED_LIMIT \ ((1024 * 1024) * (sizeof(void*) >= 8 ? 8192ULL : 256UL)) -/* - * These are the global options for mmmap limits. - * TODO: allow the user to change these - */ -static struct { - size_t window_size; - size_t mapped_limit; -} _mw_options = { - DEFAULT_WINDOW_SIZE, - DEFAULT_MAPPED_LIMIT, -}; +size_t git_mwindow__window_size = DEFAULT_WINDOW_SIZE; +size_t git_mwindow__mapped_limit = DEFAULT_MAPPED_LIMIT; + +/* Whenever you want to read or modify this, grab git__mwindow_mutex */ +static git_mwindow_ctl mem_ctl; /* * Free all the windows in a sequence, typically because we're done @@ -38,8 +32,14 @@ static struct { */ void git_mwindow_free_all(git_mwindow_file *mwf) { - git_mwindow_ctl *ctl = &GIT_GLOBAL->mem_ctl; - unsigned int i; + git_mwindow_ctl *ctl = &mem_ctl; + size_t i; + + if (git_mutex_lock(&git__mwindow_mutex)) { + giterr_set(GITERR_THREAD, "unable to lock mwindow mutex"); + return; + } + /* * Remove these windows from the global list */ @@ -67,6 +67,8 @@ void git_mwindow_free_all(git_mwindow_file *mwf) mwf->windows = w->next; git__free(w); } + + git_mutex_unlock(&git__mwindow_mutex); } /* @@ -82,14 +84,13 @@ int git_mwindow_contains(git_mwindow *win, git_off_t offset) /* * Find the least-recently-used window in a file */ -void git_mwindow_scan_lru( +static void git_mwindow_scan_lru( git_mwindow_file *mwf, git_mwindow **lru_w, git_mwindow **lru_l) { git_mwindow *w, *w_l; - puts("LRU"); for (w_l = NULL, w = mwf->windows; w; w = w->next) { if (!w->inuse_cnt) { /* @@ -108,12 +109,13 @@ void git_mwindow_scan_lru( /* * Close the least recently used window. You should check to see if - * the file descriptors need closing from time to time. + * the file descriptors need closing from time to time. Called under + * lock from new_window. */ static int git_mwindow_close_lru(git_mwindow_file *mwf) { - git_mwindow_ctl *ctl = &GIT_GLOBAL->mem_ctl; - unsigned int i; + git_mwindow_ctl *ctl = &mem_ctl; + size_t i; git_mwindow *lru_w = NULL, *lru_l = NULL, **list = &mwf->windows; /* FIXME: Does this give us any advantage? */ @@ -147,18 +149,20 @@ static int git_mwindow_close_lru(git_mwindow_file *mwf) return 0; } +/* This gets called under lock from git_mwindow_open */ static git_mwindow *new_window( git_mwindow_file *mwf, git_file fd, git_off_t size, git_off_t offset) { - git_mwindow_ctl *ctl = &GIT_GLOBAL->mem_ctl; - size_t walign = _mw_options.window_size / 2; + git_mwindow_ctl *ctl = &mem_ctl; + size_t walign = git_mwindow__window_size / 2; git_off_t len; git_mwindow *w; w = git__malloc(sizeof(*w)); + if (w == NULL) return NULL; @@ -166,16 +170,16 @@ static git_mwindow *new_window( w->offset = (offset / walign) * walign; len = size - w->offset; - if (len > (git_off_t)_mw_options.window_size) - len = (git_off_t)_mw_options.window_size; + if (len > (git_off_t)git_mwindow__window_size) + len = (git_off_t)git_mwindow__window_size; ctl->mapped += (size_t)len; - while (_mw_options.mapped_limit < ctl->mapped && + while (git_mwindow__mapped_limit < ctl->mapped && git_mwindow_close_lru(mwf) == 0) /* nop */; /* - * We treat _mw_options.mapped_limit as a soft limit. If we can't find a + * We treat `mapped_limit` as a soft limit. If we can't find a * window to close and are above the limit, we still mmap the new * window. */ @@ -208,9 +212,14 @@ unsigned char *git_mwindow_open( size_t extra, unsigned int *left) { - git_mwindow_ctl *ctl = &GIT_GLOBAL->mem_ctl; + git_mwindow_ctl *ctl = &mem_ctl; git_mwindow *w = *cursor; + if (git_mutex_lock(&git__mwindow_mutex)) { + giterr_set(GITERR_THREAD, "unable to lock mwindow mutex"); + return NULL; + } + if (!w || !(git_mwindow_contains(w, offset) && git_mwindow_contains(w, offset + extra))) { if (w) { w->inuse_cnt--; @@ -228,8 +237,10 @@ unsigned char *git_mwindow_open( */ if (!w) { w = new_window(mwf, mwf->fd, mwf->size, offset); - if (w == NULL) + if (w == NULL) { + git_mutex_unlock(&git__mwindow_mutex); return NULL; + } w->next = mwf->windows; mwf->windows = w; } @@ -247,26 +258,62 @@ unsigned char *git_mwindow_open( if (left) *left = (unsigned int)(w->window_map.len - offset); - fflush(stdout); + git_mutex_unlock(&git__mwindow_mutex); return (unsigned char *) w->window_map.data + offset; } int git_mwindow_file_register(git_mwindow_file *mwf) { - git_mwindow_ctl *ctl = &GIT_GLOBAL->mem_ctl; + git_mwindow_ctl *ctl = &mem_ctl; + int ret; + + if (git_mutex_lock(&git__mwindow_mutex)) { + giterr_set(GITERR_THREAD, "unable to lock mwindow mutex"); + return -1; + } if (ctl->windowfiles.length == 0 && - git_vector_init(&ctl->windowfiles, 8, NULL) < 0) + git_vector_init(&ctl->windowfiles, 8, NULL) < 0) { + git_mutex_unlock(&git__mwindow_mutex); return -1; + } - return git_vector_insert(&ctl->windowfiles, mwf); + ret = git_vector_insert(&ctl->windowfiles, mwf); + git_mutex_unlock(&git__mwindow_mutex); + + return ret; +} + +void git_mwindow_file_deregister(git_mwindow_file *mwf) +{ + git_mwindow_ctl *ctl = &mem_ctl; + git_mwindow_file *cur; + size_t i; + + if (git_mutex_lock(&git__mwindow_mutex)) + return; + + git_vector_foreach(&ctl->windowfiles, i, cur) { + if (cur == mwf) { + git_vector_remove(&ctl->windowfiles, i); + git_mutex_unlock(&git__mwindow_mutex); + return; + } + } + git_mutex_unlock(&git__mwindow_mutex); } void git_mwindow_close(git_mwindow **window) { git_mwindow *w = *window; if (w) { + if (git_mutex_lock(&git__mwindow_mutex)) { + giterr_set(GITERR_THREAD, "unable to lock mwindow mutex"); + return; + } + w->inuse_cnt--; + git_mutex_unlock(&git__mwindow_mutex); *window = NULL; } } diff --git a/src/mwindow.h b/src/mwindow.h index 058027251..0018ebbf0 100644 --- a/src/mwindow.h +++ b/src/mwindow.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -38,8 +38,8 @@ typedef struct git_mwindow_ctl { int git_mwindow_contains(git_mwindow *win, git_off_t offset); void git_mwindow_free_all(git_mwindow_file *mwf); unsigned char *git_mwindow_open(git_mwindow_file *mwf, git_mwindow **cursor, git_off_t offset, size_t extra, unsigned int *left); -void git_mwindow_scan_lru(git_mwindow_file *mwf, git_mwindow **lru_w, git_mwindow **lru_l); int git_mwindow_file_register(git_mwindow_file *mwf); +void git_mwindow_file_deregister(git_mwindow_file *mwf); void git_mwindow_close(git_mwindow **w_cursor); #endif diff --git a/src/netops.c b/src/netops.c index 4d461a049..69179dd1c 100644 --- a/src/netops.c +++ b/src/netops.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -10,15 +10,26 @@ # include <sys/select.h> # include <sys/time.h> # include <netdb.h> +# include <netinet/in.h> +# include <arpa/inet.h> #else -# include <winsock2.h> -# include <Ws2tcpip.h> +# include <ws2tcpip.h> # ifdef _MSC_VER -# pragma comment(lib, "Ws2_32.lib") +# pragma comment(lib, "ws2_32") # endif #endif +#ifdef __FreeBSD__ +# include <netinet/in.h> +#endif + +#ifdef GIT_SSL +# include <openssl/ssl.h> +# include <openssl/err.h> +# include <openssl/x509v3.h> +#endif +#include <ctype.h> #include "git2/errors.h" #include "common.h" @@ -29,14 +40,15 @@ #ifdef GIT_WIN32 static void net_set_error(const char *str) { - int size, error = WSAGetLastError(); - LPSTR err_str = NULL; + int error = WSAGetLastError(); + char * win32_error = git_win32_get_error_message(error); - size = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, - 0, error, 0, (LPSTR)&err_str, 0, 0); - - giterr_set(GITERR_NET, "%s: %s", str, err_str); - LocalFree(err_str); + if (win32_error) { + giterr_set(GITERR_NET, "%s: %s", str, win32_error); + git__free(win32_error); + } else { + giterr_set(GITERR_NET, str); + } } #else static void net_set_error(const char *str) @@ -45,21 +57,66 @@ static void net_set_error(const char *str) } #endif -void gitno_buffer_setup(gitno_buffer *buf, char *data, unsigned int len, GIT_SOCKET fd) +#ifdef GIT_SSL +static int ssl_set_error(gitno_ssl *ssl, int error) { - memset(buf, 0x0, sizeof(gitno_buffer)); - memset(data, 0x0, len); - buf->data = data; - buf->len = len; - buf->offset = 0; - buf->fd = fd; + int err; + unsigned long e; + + err = SSL_get_error(ssl->ssl, error); + + assert(err != SSL_ERROR_WANT_READ); + assert(err != SSL_ERROR_WANT_WRITE); + + switch (err) { + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + giterr_set(GITERR_NET, "SSL error: connection failure\n"); + break; + case SSL_ERROR_WANT_X509_LOOKUP: + giterr_set(GITERR_NET, "SSL error: x509 error\n"); + break; + case SSL_ERROR_SYSCALL: + e = ERR_get_error(); + if (e > 0) { + giterr_set(GITERR_NET, "SSL error: %s", + ERR_error_string(e, NULL)); + break; + } else if (error < 0) { + giterr_set(GITERR_OS, "SSL error: syscall failure"); + break; + } + giterr_set(GITERR_NET, "SSL error: received early EOF"); + break; + case SSL_ERROR_SSL: + e = ERR_get_error(); + giterr_set(GITERR_NET, "SSL error: %s", + ERR_error_string(e, NULL)); + break; + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + default: + giterr_set(GITERR_NET, "SSL error: unknown error"); + break; + } + return -1; } +#endif int gitno_recv(gitno_buffer *buf) { + return buf->recv(buf); +} + +#ifdef GIT_SSL +static int gitno__recv_ssl(gitno_buffer *buf) +{ int ret; - ret = p_recv(buf->fd, buf->data + buf->offset, buf->len - buf->offset, 0); + do { + ret = SSL_read(buf->socket->ssl.ssl, buf->data + buf->offset, buf->len - buf->offset); + } while (SSL_get_error(buf->socket->ssl.ssl, ret) == SSL_ERROR_WANT_READ); + if (ret < 0) { net_set_error("Error receiving socket data"); return -1; @@ -68,6 +125,49 @@ int gitno_recv(gitno_buffer *buf) buf->offset += ret; return ret; } +#endif + +static int gitno__recv(gitno_buffer *buf) +{ + int ret; + + ret = p_recv(buf->socket->socket, buf->data + buf->offset, buf->len - buf->offset, 0); + if (ret < 0) { + net_set_error("Error receiving socket data"); + return -1; + } + + buf->offset += ret; + return ret; +} + +void gitno_buffer_setup_callback( + gitno_socket *socket, + gitno_buffer *buf, + char *data, + size_t len, + int (*recv)(gitno_buffer *buf), void *cb_data) +{ + memset(data, 0x0, len); + buf->data = data; + buf->len = len; + buf->offset = 0; + buf->socket = socket; + buf->recv = recv; + buf->cb_data = cb_data; +} + +void gitno_buffer_setup(gitno_socket *socket, gitno_buffer *buf, char *data, size_t len) +{ +#ifdef GIT_SSL + if (socket->ssl.ctx) { + gitno_buffer_setup_callback(socket, buf, data, len, gitno__recv_ssl, NULL); + return; + } +#endif + + gitno_buffer_setup_callback(socket, buf, data, len, gitno__recv, NULL); +} /* Consume up to ptr and move the rest of the buffer to the beginning */ void gitno_consume(gitno_buffer *buf, const char *ptr) @@ -92,24 +192,284 @@ void gitno_consume_n(gitno_buffer *buf, size_t cons) buf->offset -= cons; } -int gitno_connect(GIT_SOCKET *sock, const char *host, const char *port) +#ifdef GIT_SSL + +static int gitno_ssl_teardown(gitno_ssl *ssl) +{ + int ret; + + ret = SSL_shutdown(ssl->ssl); + if (ret < 0) + ret = ssl_set_error(ssl, ret); + else + ret = 0; + + SSL_free(ssl->ssl); + SSL_CTX_free(ssl->ctx); + return ret; +} + +/* Match host names according to RFC 2818 rules */ +static int match_host(const char *pattern, const char *host) +{ + for (;;) { + char c = tolower(*pattern++); + + if (c == '\0') + return *host ? -1 : 0; + + if (c == '*') { + c = *pattern; + /* '*' at the end matches everything left */ + if (c == '\0') + return 0; + + /* + * We've found a pattern, so move towards the next matching + * char. The '.' is handled specially because wildcards aren't + * allowed to cross subdomains. + */ + + while(*host) { + char h = tolower(*host); + if (c == h) + return match_host(pattern, host++); + if (h == '.') + return match_host(pattern, host); + host++; + } + return -1; + } + + if (c != tolower(*host++)) + return -1; + } + + return -1; +} + +static int check_host_name(const char *name, const char *host) +{ + if (!strcasecmp(name, host)) + return 0; + + if (match_host(name, host) < 0) + return -1; + + return 0; +} + +static int verify_server_cert(gitno_ssl *ssl, const char *host) +{ + X509 *cert; + X509_NAME *peer_name; + ASN1_STRING *str; + unsigned char *peer_cn = NULL; + int matched = -1, type = GEN_DNS; + GENERAL_NAMES *alts; + struct in6_addr addr6; + struct in_addr addr4; + void *addr; + int i = -1,j; + + if (SSL_get_verify_result(ssl->ssl) != X509_V_OK) { + giterr_set(GITERR_SSL, "The SSL certificate is invalid"); + return -1; + } + + /* Try to parse the host as an IP address to see if it is */ + if (p_inet_pton(AF_INET, host, &addr4)) { + type = GEN_IPADD; + addr = &addr4; + } else { + if(p_inet_pton(AF_INET6, host, &addr6)) { + type = GEN_IPADD; + addr = &addr6; + } + } + + + cert = SSL_get_peer_certificate(ssl->ssl); + + /* Check the alternative names */ + alts = X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL); + if (alts) { + int num; + + num = sk_GENERAL_NAME_num(alts); + for (i = 0; i < num && matched != 1; i++) { + const GENERAL_NAME *gn = sk_GENERAL_NAME_value(alts, i); + const char *name = (char *) ASN1_STRING_data(gn->d.ia5); + size_t namelen = (size_t) ASN1_STRING_length(gn->d.ia5); + + /* Skip any names of a type we're not looking for */ + if (gn->type != type) + continue; + + if (type == GEN_DNS) { + /* If it contains embedded NULs, don't even try */ + if (memchr(name, '\0', namelen)) + continue; + + if (check_host_name(name, host) < 0) + matched = 0; + else + matched = 1; + } else if (type == GEN_IPADD) { + /* Here name isn't so much a name but a binary representation of the IP */ + matched = !!memcmp(name, addr, namelen); + } + } + } + GENERAL_NAMES_free(alts); + + if (matched == 0) + goto cert_fail; + + if (matched == 1) + return 0; + + /* If no alternative names are available, check the common name */ + peer_name = X509_get_subject_name(cert); + if (peer_name == NULL) + goto on_error; + + if (peer_name) { + /* Get the index of the last CN entry */ + while ((j = X509_NAME_get_index_by_NID(peer_name, NID_commonName, i)) >= 0) + i = j; + } + + if (i < 0) + goto on_error; + + str = X509_NAME_ENTRY_get_data(X509_NAME_get_entry(peer_name, i)); + if (str == NULL) + goto on_error; + + /* Work around a bug in OpenSSL whereby ASN1_STRING_to_UTF8 fails if it's already in utf-8 */ + if (ASN1_STRING_type(str) == V_ASN1_UTF8STRING) { + int size = ASN1_STRING_length(str); + + if (size > 0) { + peer_cn = OPENSSL_malloc(size + 1); + GITERR_CHECK_ALLOC(peer_cn); + memcpy(peer_cn, ASN1_STRING_data(str), size); + peer_cn[size] = '\0'; + } + } else { + int size = ASN1_STRING_to_UTF8(&peer_cn, str); + GITERR_CHECK_ALLOC(peer_cn); + if (memchr(peer_cn, '\0', size)) + goto cert_fail; + } + + if (check_host_name((char *)peer_cn, host) < 0) + goto cert_fail; + + OPENSSL_free(peer_cn); + + return 0; + +on_error: + OPENSSL_free(peer_cn); + return ssl_set_error(ssl, 0); + +cert_fail: + OPENSSL_free(peer_cn); + giterr_set(GITERR_SSL, "Certificate host name check failed"); + return -1; +} + +static int ssl_setup(gitno_socket *socket, const char *host, int flags) +{ + int ret; + + SSL_library_init(); + SSL_load_error_strings(); + socket->ssl.ctx = SSL_CTX_new(SSLv23_method()); + if (socket->ssl.ctx == NULL) + return ssl_set_error(&socket->ssl, 0); + + SSL_CTX_set_mode(socket->ssl.ctx, SSL_MODE_AUTO_RETRY); + SSL_CTX_set_verify(socket->ssl.ctx, SSL_VERIFY_NONE, NULL); + if (!SSL_CTX_set_default_verify_paths(socket->ssl.ctx)) + return ssl_set_error(&socket->ssl, 0); + + socket->ssl.ssl = SSL_new(socket->ssl.ctx); + if (socket->ssl.ssl == NULL) + return ssl_set_error(&socket->ssl, 0); + + if((ret = SSL_set_fd(socket->ssl.ssl, socket->socket)) == 0) + return ssl_set_error(&socket->ssl, ret); + + if ((ret = SSL_connect(socket->ssl.ssl)) <= 0) + return ssl_set_error(&socket->ssl, ret); + + if (GITNO_CONNECT_SSL_NO_CHECK_CERT & flags) + return 0; + + return verify_server_cert(&socket->ssl, host); +} +#endif + +static int gitno__close(GIT_SOCKET s) +{ +#ifdef GIT_WIN32 + if (SOCKET_ERROR == closesocket(s)) + return -1; + + if (0 != WSACleanup()) { + giterr_set(GITERR_OS, "Winsock cleanup failed"); + return -1; + } + + return 0; +#else + return close(s); +#endif +} + +int gitno_connect(gitno_socket *s_out, const char *host, const char *port, int flags) { struct addrinfo *info = NULL, *p; struct addrinfo hints; - int ret; GIT_SOCKET s = INVALID_SOCKET; + int ret; + +#ifdef GIT_WIN32 + /* on win32, the WSA context needs to be initialized + * before any socket calls can be performed */ + WSADATA wsd; + + if (WSAStartup(MAKEWORD(2,2), &wsd) != 0) { + giterr_set(GITERR_OS, "Winsock init failed"); + return -1; + } + + if (LOBYTE(wsd.wVersion) != 2 || HIBYTE(wsd.wVersion) != 2) { + WSACleanup(); + giterr_set(GITERR_OS, "Winsock init failed"); + return -1; + } +#endif + + /* Zero the socket structure provided */ + memset(s_out, 0x0, sizeof(gitno_socket)); memset(&hints, 0x0, sizeof(struct addrinfo)); - hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; + hints.ai_family = AF_UNSPEC; - if ((ret = getaddrinfo(host, port, &hints, &info)) < 0) { - giterr_set(GITERR_NET, "Failed to resolve address for %s: %s", host, gai_strerror(ret)); + if ((ret = p_getaddrinfo(host, port, &hints, &info)) < 0) { + giterr_set(GITERR_NET, + "Failed to resolve address for %s: %s", host, p_gai_strerror(ret)); return -1; } for (p = info; p != NULL; p = p->ai_next) { s = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (s == INVALID_SOCKET) { net_set_error("error creating socket"); break; @@ -119,30 +479,67 @@ int gitno_connect(GIT_SOCKET *sock, const char *host, const char *port) break; /* If we can't connect, try the next one */ - gitno_close(s); + gitno__close(s); s = INVALID_SOCKET; } /* Oops, we couldn't connect to any address */ if (s == INVALID_SOCKET && p == NULL) { giterr_set(GITERR_OS, "Failed to connect to %s", host); + p_freeaddrinfo(info); + return -1; + } + + s_out->socket = s; + p_freeaddrinfo(info); + +#ifdef GIT_SSL + if ((flags & GITNO_CONNECT_SSL) && ssl_setup(s_out, host, flags) < 0) + return -1; +#else + /* SSL is not supported */ + if (flags & GITNO_CONNECT_SSL) { + giterr_set(GITERR_OS, "SSL is not supported by this copy of libgit2."); return -1; } +#endif - freeaddrinfo(info); - *sock = s; return 0; } -int gitno_send(GIT_SOCKET s, const char *msg, size_t len, int flags) +#ifdef GIT_SSL +static int gitno_send_ssl(gitno_ssl *ssl, const char *msg, size_t len, int flags) { int ret; size_t off = 0; + GIT_UNUSED(flags); + while (off < len) { - errno = 0; + ret = SSL_write(ssl->ssl, msg + off, len - off); + if (ret <= 0 && ret != SSL_ERROR_WANT_WRITE) + return ssl_set_error(ssl, ret); + + off += ret; + } + + return off; +} +#endif + +int gitno_send(gitno_socket *socket, const char *msg, size_t len, int flags) +{ + int ret; + size_t off = 0; + +#ifdef GIT_SSL + if (socket->ssl.ctx) + return gitno_send_ssl(&socket->ssl, msg, len, flags); +#endif - ret = p_send(s, msg + off, len - off, flags); + while (off < len) { + errno = 0; + ret = p_send(socket->socket, msg + off, len - off, flags); if (ret < 0) { net_set_error("Error sending data"); return -1; @@ -154,19 +551,17 @@ int gitno_send(GIT_SOCKET s, const char *msg, size_t len, int flags) return (int)off; } - -#ifdef GIT_WIN32 -int gitno_close(GIT_SOCKET s) -{ - return closesocket(s) == SOCKET_ERROR ? -1 : 0; -} -#else -int gitno_close(GIT_SOCKET s) +int gitno_close(gitno_socket *s) { - return close(s); -} +#ifdef GIT_SSL + if (s->ssl.ctx && + gitno_ssl_teardown(&s->ssl) < 0) + return -1; #endif + return gitno__close(s->socket); +} + int gitno_select_in(gitno_buffer *buf, long int sec, long int usec) { fd_set fds; @@ -176,33 +571,60 @@ int gitno_select_in(gitno_buffer *buf, long int sec, long int usec) tv.tv_usec = usec; FD_ZERO(&fds); - FD_SET(buf->fd, &fds); + FD_SET(buf->socket->socket, &fds); /* The select(2) interface is silly */ - return select((int)buf->fd + 1, &fds, NULL, NULL, &tv); + return select((int)buf->socket->socket + 1, &fds, NULL, NULL, &tv); } -int gitno_extract_host_and_port(char **host, char **port, const char *url, const char *default_port) +int gitno_extract_url_parts( + char **host, + char **port, + char **username, + char **password, + const char *url, + const char *default_port) { - char *colon, *slash, *delim; + char *colon, *slash, *at, *end; + const char *start; + + /* + * + * ==> [user[:pass]@]hostname.tld[:port]/resource + */ colon = strchr(url, ':'); slash = strchr(url, '/'); + at = strchr(url, '@'); if (slash == NULL) { giterr_set(GITERR_NET, "Malformed URL: missing /"); return -1; } + start = url; + if (at && at < slash) { + start = at+1; + *username = git__substrdup(url, at - url); + } + + if (colon && colon < at) { + git__free(*username); + *username = git__substrdup(url, colon-url); + *password = git__substrdup(colon+1, at-colon-1); + colon = strchr(at, ':'); + } + if (colon == NULL) { *port = git__strdup(default_port); } else { - *port = git__strndup(colon + 1, slash - colon - 1); + *port = git__substrdup(colon + 1, slash - colon - 1); } GITERR_CHECK_ALLOC(*port); - delim = colon == NULL ? slash : colon; - *host = git__strndup(url, delim - url); + end = colon == NULL ? slash : colon; + + *host = git__substrdup(start, end - start); GITERR_CHECK_ALLOC(*host); return 0; diff --git a/src/netops.h b/src/netops.h index 9d13f3891..d352bf3b6 100644 --- a/src/netops.h +++ b/src/netops.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -8,25 +8,70 @@ #define INCLUDE_netops_h__ #include "posix.h" +#include "common.h" -typedef struct gitno_buffer { +#ifdef GIT_SSL +# include <openssl/ssl.h> +#endif + +struct gitno_ssl { +#ifdef GIT_SSL + SSL_CTX *ctx; + SSL *ssl; +#else + size_t dummy; +#endif +}; + +typedef struct gitno_ssl gitno_ssl; + +/* Represents a socket that may or may not be using SSL */ +struct gitno_socket { + GIT_SOCKET socket; + gitno_ssl ssl; +}; + +typedef struct gitno_socket gitno_socket; + +struct gitno_buffer { char *data; size_t len; size_t offset; - GIT_SOCKET fd; -} gitno_buffer; + gitno_socket *socket; + int (*recv)(struct gitno_buffer *buffer); + void *cb_data; +}; + +typedef struct gitno_buffer gitno_buffer; + +/* Flags to gitno_connect */ +enum { + /* Attempt to create an SSL connection. */ + GITNO_CONNECT_SSL = 1, -void gitno_buffer_setup(gitno_buffer *buf, char *data, unsigned int len, GIT_SOCKET fd); + /* Valid only when GITNO_CONNECT_SSL is also specified. + * Indicates that the server certificate should not be validated. */ + GITNO_CONNECT_SSL_NO_CHECK_CERT = 2, +}; + +void gitno_buffer_setup(gitno_socket *t, gitno_buffer *buf, char *data, size_t len); +void gitno_buffer_setup_callback(gitno_socket *t, gitno_buffer *buf, char *data, size_t len, int (*recv)(gitno_buffer *buf), void *cb_data); int gitno_recv(gitno_buffer *buf); + void gitno_consume(gitno_buffer *buf, const char *ptr); void gitno_consume_n(gitno_buffer *buf, size_t cons); -int gitno_connect(GIT_SOCKET *s, const char *host, const char *port); -int gitno_send(GIT_SOCKET s, const char *msg, size_t len, int flags); -int gitno_close(GIT_SOCKET s); -int gitno_send_chunk_size(int s, size_t len); +int gitno_connect(gitno_socket *socket, const char *host, const char *port, int flags); +int gitno_send(gitno_socket *socket, const char *msg, size_t len, int flags); +int gitno_close(gitno_socket *s); int gitno_select_in(gitno_buffer *buf, long int sec, long int usec); -int gitno_extract_host_and_port(char **host, char **port, const char *url, const char *default_port); +int gitno_extract_url_parts( + char **host, + char **port, + char **username, + char **password, + const char *url, + const char *default_port); #endif diff --git a/src/notes.c b/src/notes.c index 84ad94087..ef48ac88e 100644 --- a/src/notes.c +++ b/src/notes.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -11,56 +11,68 @@ #include "refs.h" #include "config.h" #include "iterator.h" +#include "signature.h" -static int find_subtree(git_tree **subtree, const git_oid *root, - git_repository *repo, const char *target, int *fanout) +static int find_subtree_in_current_level( + git_tree **out, + git_repository *repo, + git_tree *parent, + const char *annotated_object_sha, + int fanout) { - int error; - unsigned int i; - git_tree *tree; + size_t i; const git_tree_entry *entry; - *subtree = NULL; + *out = NULL; - error = git_tree_lookup(&tree, repo, root); - if (error < 0) - return error; + if (parent == NULL) + return GIT_ENOTFOUND; - for (i=0; i<git_tree_entrycount(tree); i++) { - entry = git_tree_entry_byindex(tree, i); + for (i = 0; i < git_tree_entrycount(parent); i++) { + entry = git_tree_entry_byindex(parent, i); if (!git__ishex(git_tree_entry_name(entry))) continue; - /* - * A notes tree follows a strict byte-based progressive fanout - * (i.e. using 2/38, 2/2/36, etc. fanouts, not e.g. 4/36 fanout) - */ - - if (S_ISDIR(git_tree_entry_attributes(entry)) + if (S_ISDIR(git_tree_entry_filemode(entry)) && strlen(git_tree_entry_name(entry)) == 2 - && !strncmp(git_tree_entry_name(entry), target + *fanout, 2)) { + && !strncmp(git_tree_entry_name(entry), annotated_object_sha + fanout, 2)) + return git_tree_lookup(out, repo, git_tree_entry_id(entry)); - /* found matching subtree - unpack and resume lookup */ + /* Not a DIR, so do we have an already existing blob? */ + if (!strcmp(git_tree_entry_name(entry), annotated_object_sha + fanout)) + return GIT_EEXISTS; + } - git_oid subtree_sha; - git_oid_cpy(&subtree_sha, git_tree_entry_id(entry)); - git_tree_free(tree); + return GIT_ENOTFOUND; +} - *fanout += 2; +static int find_subtree_r(git_tree **out, git_tree *root, + git_repository *repo, const char *target, int *fanout) +{ + int error; + git_tree *subtree = NULL; - return find_subtree(subtree, &subtree_sha, repo, - target, fanout); - } + *out = NULL; + + error = find_subtree_in_current_level(&subtree, repo, root, target, *fanout); + if (error == GIT_EEXISTS) { + return git_tree_lookup(out, repo, git_tree_id(root)); } - *subtree = tree; - return 0; + if (error < 0) + return error; + + *fanout += 2; + error = find_subtree_r(out, subtree, repo, target, fanout); + git_tree_free(subtree); + + return error; } static int find_blob(git_oid *blob, git_tree *tree, const char *target) { - unsigned int i; + size_t i; const git_tree_entry *entry; for (i=0; i<git_tree_entrycount(tree); i++) { @@ -76,191 +88,285 @@ static int find_blob(git_oid *blob, git_tree *tree, const char *target) return GIT_ENOTFOUND; } -static int note_write(git_oid *out, git_repository *repo, - git_signature *author, git_signature *committer, - const char *notes_ref, const char *note, - const git_oid *tree_sha, const char *target, - int nparents, git_commit **parents) +static int tree_write( + git_tree **out, + git_repository *repo, + git_tree *source_tree, + const git_oid *object_oid, + const char *treeentry_name, + unsigned int attributes) { - int error, fanout = 0; - git_oid oid; - git_tree *tree = NULL; - git_tree_entry *entry; - git_treebuilder *tb; - - /* check for existing notes tree */ - - if (tree_sha) { - error = find_subtree(&tree, tree_sha, repo, target, &fanout); - if (error < 0) - return error; - - error = find_blob(&oid, tree, target + fanout); - if (error != GIT_ENOTFOUND) { - git_tree_free(tree); - if (!error) { - giterr_set(GITERR_REPOSITORY, "Note for '%s' exists already", target); - error = GIT_EEXISTS; - } - return error; - } - } + int error; + git_treebuilder *tb = NULL; + const git_tree_entry *entry; + git_oid tree_oid; - /* no matching tree entry - add note object to target tree */ + if ((error = git_treebuilder_create(&tb, source_tree)) < 0) + goto cleanup; - error = git_treebuilder_create(&tb, tree); - git_tree_free(tree); + if (object_oid) { + if ((error = git_treebuilder_insert( + &entry, tb, treeentry_name, object_oid, attributes)) < 0) + goto cleanup; + } else { + if ((error = git_treebuilder_remove(tb, treeentry_name)) < 0) + goto cleanup; + } - if (error < 0) - return error; + if ((error = git_treebuilder_write(&tree_oid, repo, tb)) < 0) + goto cleanup; - if (!tree_sha) - /* no notes tree yet - create fanout */ - fanout += 2; + error = git_tree_lookup(out, repo, &tree_oid); - /* create note object */ - error = git_blob_create_frombuffer(&oid, repo, note, strlen(note)); - if (error < 0) { - git_treebuilder_free(tb); - return error; - } +cleanup: + git_treebuilder_free(tb); + return error; +} - error = git_treebuilder_insert(&entry, tb, target + fanout, &oid, 0100644); - if (error < 0) { - /* libgit2 doesn't support object removal (gc) yet */ - /* we leave an orphaned blob object behind - TODO */ +static int manipulate_note_in_tree_r( + git_tree **out, + git_repository *repo, + git_tree *parent, + git_oid *note_oid, + const char *annotated_object_sha, + int fanout, + int (*note_exists_cb)( + git_tree **out, + git_repository *repo, + git_tree *parent, + git_oid *note_oid, + const char *annotated_object_sha, + int fanout, + int current_error), + int (*note_notfound_cb)( + git_tree **out, + git_repository *repo, + git_tree *parent, + git_oid *note_oid, + const char *annotated_object_sha, + int fanout, + int current_error)) +{ + int error; + git_tree *subtree = NULL, *new = NULL; + char subtree_name[3]; - git_treebuilder_free(tb); - return error; - } + error = find_subtree_in_current_level( + &subtree, repo, parent, annotated_object_sha, fanout); - if (out) - git_oid_cpy(out, git_tree_entry_id(entry)); + if (error == GIT_EEXISTS) { + error = note_exists_cb( + out, repo, parent, note_oid, annotated_object_sha, fanout, error); + goto cleanup; + } - error = git_treebuilder_write(&oid, repo, tb); - git_treebuilder_free(tb); + if (error == GIT_ENOTFOUND) { + error = note_notfound_cb( + out, repo, parent, note_oid, annotated_object_sha, fanout, error); + goto cleanup; + } if (error < 0) - return 0; - - if (!tree_sha) { - /* create fanout subtree */ + goto cleanup; - char subtree[3]; - strncpy(subtree, target, 2); - subtree[2] = '\0'; + /* An existing fanout has been found, let's dig deeper */ + error = manipulate_note_in_tree_r( + &new, repo, subtree, note_oid, annotated_object_sha, + fanout + 2, note_exists_cb, note_notfound_cb); - error = git_treebuilder_create(&tb, NULL); - if (error < 0) - return error; + if (error < 0) + goto cleanup; - error = git_treebuilder_insert(NULL, tb, subtree, &oid, 0040000); - if (error < 0) { - git_treebuilder_free(tb); - return error; - } + strncpy(subtree_name, annotated_object_sha + fanout, 2); + subtree_name[2] = '\0'; - error = git_treebuilder_write(&oid, repo, tb); + error = tree_write(out, repo, parent, git_tree_id(new), + subtree_name, GIT_FILEMODE_TREE); - git_treebuilder_free(tb); - if (error < 0) - return error; - } +cleanup: + git_tree_free(new); + git_tree_free(subtree); + return error; +} - /* create new notes commit */ +static int remove_note_in_tree_eexists_cb( + git_tree **out, + git_repository *repo, + git_tree *parent, + git_oid *note_oid, + const char *annotated_object_sha, + int fanout, + int current_error) +{ + GIT_UNUSED(note_oid); + GIT_UNUSED(current_error); - error = git_tree_lookup(&tree, repo, &oid); - if (error < 0) - return error; + return tree_write(out, repo, parent, NULL, annotated_object_sha + fanout, 0); +} - error = git_commit_create(&oid, repo, notes_ref, author, committer, - NULL, GIT_NOTES_DEFAULT_MSG_ADD, - tree, nparents, (const git_commit **) parents); +static int remove_note_in_tree_enotfound_cb( + git_tree **out, + git_repository *repo, + git_tree *parent, + git_oid *note_oid, + const char *annotated_object_sha, + int fanout, + int current_error) +{ + GIT_UNUSED(out); + GIT_UNUSED(repo); + GIT_UNUSED(parent); + GIT_UNUSED(note_oid); + GIT_UNUSED(fanout); + + giterr_set(GITERR_REPOSITORY, "Object '%s' has no note", annotated_object_sha); + return current_error; +} - git_tree_free(tree); +static int insert_note_in_tree_eexists_cb(git_tree **out, + git_repository *repo, + git_tree *parent, + git_oid *note_oid, + const char *annotated_object_sha, + int fanout, + int current_error) +{ + GIT_UNUSED(out); + GIT_UNUSED(repo); + GIT_UNUSED(parent); + GIT_UNUSED(note_oid); + GIT_UNUSED(fanout); + + giterr_set(GITERR_REPOSITORY, "Note for '%s' exists already", annotated_object_sha); + return current_error; +} - return error; +static int insert_note_in_tree_enotfound_cb(git_tree **out, + git_repository *repo, + git_tree *parent, + git_oid *note_oid, + const char *annotated_object_sha, + int fanout, + int current_error) +{ + GIT_UNUSED(current_error); + + /* No existing fanout at this level, insert in place */ + return tree_write( + out, + repo, + parent, + note_oid, + annotated_object_sha + fanout, + GIT_FILEMODE_BLOB); } -static int note_lookup(git_note **out, git_repository *repo, - const git_oid *tree_sha, const char *target) +static int note_write(git_oid *out, + git_repository *repo, + const git_signature *author, + const git_signature *committer, + const char *notes_ref, + const char *note, + git_tree *commit_tree, + const char *target, + git_commit **parents, + int allow_note_overwrite) { - int error, fanout = 0; + int error; git_oid oid; - git_blob *blob; - git_tree *tree; - git_note *note; + git_tree *tree = NULL; - error = find_subtree(&tree, tree_sha, repo, target, &fanout); - if (error < 0) - return error; + // TODO: should we apply filters? + /* create note object */ + if ((error = git_blob_create_frombuffer(&oid, repo, note, strlen(note))) < 0) + goto cleanup; - error = find_blob(&oid, tree, target + fanout); + if ((error = manipulate_note_in_tree_r( + &tree, repo, commit_tree, &oid, target, 0, + allow_note_overwrite ? insert_note_in_tree_enotfound_cb : insert_note_in_tree_eexists_cb, + insert_note_in_tree_enotfound_cb)) < 0) + goto cleanup; + + if (out) + git_oid_cpy(out, &oid); + + error = git_commit_create(&oid, repo, notes_ref, author, committer, + NULL, GIT_NOTES_DEFAULT_MSG_ADD, + tree, *parents == NULL ? 0 : 1, (const git_commit **) parents); +cleanup: git_tree_free(tree); - if (error < 0) - return error; + return error; +} - error = git_blob_lookup(&blob, repo, &oid); - if (error < 0) - return error; +static int note_new(git_note **out, git_oid *note_oid, git_blob *blob) +{ + git_note *note = NULL; - note = git__malloc(sizeof(git_note)); + note = (git_note *)git__malloc(sizeof(git_note)); GITERR_CHECK_ALLOC(note); - git_oid_cpy(¬e->oid, &oid); - note->message = git__strdup(git_blob_rawcontent(blob)); + git_oid_cpy(¬e->oid, note_oid); + note->message = git__strdup((char *)git_blob_rawcontent(blob)); GITERR_CHECK_ALLOC(note->message); *out = note; - git_blob_free(blob); - return error; + return 0; } -static int note_remove(git_repository *repo, - git_signature *author, git_signature *committer, - const char *notes_ref, const git_oid *tree_sha, - const char *target, int nparents, git_commit **parents) +static int note_lookup(git_note **out, git_repository *repo, + git_tree *tree, const char *target) { int error, fanout = 0; git_oid oid; - git_tree *tree; - git_treebuilder *tb; + git_blob *blob = NULL; + git_note *note = NULL; + git_tree *subtree = NULL; - error = find_subtree(&tree, tree_sha, repo, target, &fanout); - if (error < 0) - return error; + if ((error = find_subtree_r(&subtree, tree, repo, target, &fanout)) < 0) + goto cleanup; - error = find_blob(&oid, tree, target + fanout); - if (!error) - error = git_treebuilder_create(&tb, tree); + if ((error = find_blob(&oid, subtree, target + fanout)) < 0) + goto cleanup; - git_tree_free(tree); - if (error < 0) - return error; + if ((error = git_blob_lookup(&blob, repo, &oid)) < 0) + goto cleanup; - error = git_treebuilder_remove(tb, target + fanout); - if (!error) - error = git_treebuilder_write(&oid, repo, tb); + if ((error = note_new(¬e, &oid, blob)) < 0) + goto cleanup; - git_treebuilder_free(tb); - if (error < 0) - return error; + *out = note; - /* create new notes commit */ +cleanup: + git_tree_free(subtree); + git_blob_free(blob); + return error; +} - error = git_tree_lookup(&tree, repo, &oid); - if (error < 0) - return error; +static int note_remove(git_repository *repo, + const git_signature *author, const git_signature *committer, + const char *notes_ref, git_tree *tree, + const char *target, git_commit **parents) +{ + int error; + git_tree *tree_after_removal = NULL; + git_oid oid; - error = git_commit_create(&oid, repo, notes_ref, author, committer, - NULL, GIT_NOTES_DEFAULT_MSG_RM, - tree, nparents, (const git_commit **) parents); + if ((error = manipulate_note_in_tree_r( + &tree_after_removal, repo, tree, NULL, target, 0, + remove_note_in_tree_eexists_cb, remove_note_in_tree_enotfound_cb)) < 0) + goto cleanup; - git_tree_free(tree); + error = git_commit_create(&oid, repo, notes_ref, author, committer, + NULL, GIT_NOTES_DEFAULT_MSG_RM, + tree_after_removal, + *parents == NULL ? 0 : 1, + (const git_commit **) parents); +cleanup: + git_tree_free(tree_after_removal); return error; } @@ -291,134 +397,108 @@ static int normalize_namespace(const char **notes_ref, git_repository *repo) return note_get_default_ref(notes_ref, repo); } -static int retrieve_note_tree_oid(git_oid *tree_oid_out, git_repository *repo, const char *notes_ref) +static int retrieve_note_tree_and_commit( + git_tree **tree_out, + git_commit **commit_out, + git_repository *repo, + const char **notes_ref) { - int error = -1; - git_commit *commit = NULL; + int error; git_oid oid; - if ((error = git_reference_name_to_oid(&oid, repo, notes_ref)) < 0) - goto cleanup; + if ((error = normalize_namespace(notes_ref, repo)) < 0) + return error; - if (git_commit_lookup(&commit, repo, &oid) < 0) - goto cleanup; + if ((error = git_reference_name_to_id(&oid, repo, *notes_ref)) < 0) + return error; - git_oid_cpy(tree_oid_out, git_commit_tree_oid(commit)); + if (git_commit_lookup(commit_out, repo, &oid) < 0) + return error; - error = 0; + if ((error = git_commit_tree(tree_out, *commit_out)) < 0) + return error; -cleanup: - git_commit_free(commit); - return error; + return 0; } int git_note_read(git_note **out, git_repository *repo, const char *notes_ref, const git_oid *oid) { int error; - char *target; - git_oid sha; - - *out = NULL; - - if (normalize_namespace(¬es_ref, repo) < 0) - return -1; - - if ((error = retrieve_note_tree_oid(&sha, repo, notes_ref)) < 0) - return error; + char *target = NULL; + git_tree *tree = NULL; + git_commit *commit = NULL; target = git_oid_allocfmt(oid); GITERR_CHECK_ALLOC(target); - error = note_lookup(out, repo, &sha, target); + if ((error = retrieve_note_tree_and_commit(&tree, &commit, repo, ¬es_ref)) < 0) + goto cleanup; + + error = note_lookup(out, repo, tree, target); +cleanup: git__free(target); + git_tree_free(tree); + git_commit_free(commit); return error; } int git_note_create( - git_oid *out, git_repository *repo, - git_signature *author, git_signature *committer, - const char *notes_ref, const git_oid *oid, - const char *note) + git_oid *out, + git_repository *repo, + const git_signature *author, + const git_signature *committer, + const char *notes_ref, + const git_oid *oid, + const char *note, + int allow_note_overwrite) { - int error, nparents = 0; - char *target; - git_oid sha; + int error; + char *target = NULL; git_commit *commit = NULL; - git_reference *ref; - - if (normalize_namespace(¬es_ref, repo) < 0) - return -1; - - error = git_reference_lookup(&ref, repo, notes_ref); - if (error < 0 && error != GIT_ENOTFOUND) - return error; - - if (!error) { - assert(git_reference_type(ref) == GIT_REF_OID); - - /* lookup existing notes tree oid */ - - git_oid_cpy(&sha, git_reference_oid(ref)); - git_reference_free(ref); - - error = git_commit_lookup(&commit, repo, &sha); - if (error < 0) - return error; - - git_oid_cpy(&sha, git_commit_tree_oid(commit)); - nparents++; - } + git_tree *tree = NULL; target = git_oid_allocfmt(oid); GITERR_CHECK_ALLOC(target); + error = retrieve_note_tree_and_commit(&tree, &commit, repo, ¬es_ref); + + if (error < 0 && error != GIT_ENOTFOUND) + goto cleanup; + error = note_write(out, repo, author, committer, notes_ref, - note, nparents ? &sha : NULL, target, - nparents, &commit); + note, tree, target, &commit, allow_note_overwrite); +cleanup: git__free(target); git_commit_free(commit); + git_tree_free(tree); return error; } int git_note_remove(git_repository *repo, const char *notes_ref, - git_signature *author, git_signature *committer, - const git_oid *oid) + const git_signature *author, const git_signature *committer, + const git_oid *oid) { int error; - char *target; - git_oid sha; - git_commit *commit; - git_reference *ref; - - if (normalize_namespace(¬es_ref, repo) < 0) - return -1; - - error = git_reference_lookup(&ref, repo, notes_ref); - if (error < 0) - return error; - - assert(git_reference_type(ref) == GIT_REF_OID); - - git_oid_cpy(&sha, git_reference_oid(ref)); - git_reference_free(ref); - - error = git_commit_lookup(&commit, repo, &sha); - if (error < 0) - return error; - - git_oid_cpy(&sha, git_commit_tree_oid(commit)); + char *target = NULL; + git_commit *commit = NULL; + git_tree *tree = NULL; 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_remove(repo, author, committer, notes_ref, - &sha, target, 1, &commit); + tree, target, &commit); +cleanup: git__free(target); git_commit_free(commit); + git_tree_free(tree); return error; } @@ -428,13 +508,13 @@ int git_note_default_ref(const char **out, git_repository *repo) return note_get_default_ref(out, repo); } -const char * git_note_message(git_note *note) +const char * git_note_message(const git_note *note) { assert(note); return note->message; } -const git_oid * git_note_oid(git_note *note) +const git_oid * git_note_oid(const git_note *note) { assert(note); return ¬e->oid; @@ -451,17 +531,15 @@ void git_note_free(git_note *note) static int process_entry_path( const char* entry_path, - const git_oid *note_oid, - int (*note_cb)(git_note_data *note_data, void *payload), - void *payload) + git_oid *annotated_object_id) { - int i = 0, j = 0, error = -1, len; + int error = -1; + size_t i = 0, j = 0, len; git_buf buf = GIT_BUF_INIT; - git_note_data note_data; - if (git_buf_puts(&buf, entry_path) < 0) + if ((error = git_buf_puts(&buf, entry_path)) < 0) goto cleanup; - + len = git_buf_len(&buf); while (i < len) { @@ -469,10 +547,9 @@ static int process_entry_path( i++; continue; } - + if (git__fromhex(buf.ptr[i]) < 0) { /* This is not a note entry */ - error = 0; goto cleanup; } @@ -488,16 +565,10 @@ static int process_entry_path( if (j != GIT_OID_HEXSZ) { /* This is not a note entry */ - error = 0; goto cleanup; } - if (git_oid_fromstr(¬e_data.annotated_object_oid, buf.ptr) < 0) - return -1; - - git_oid_cpy(¬e_data.blob_oid, note_oid); - - error = note_cb(¬e_data, payload); + error = git_oid_fromstr(annotated_object_id, buf.ptr); cleanup: git_buf_free(&buf); @@ -505,44 +576,86 @@ cleanup: } int git_note_foreach( - git_repository *repo, - const char *notes_ref, - int (*note_cb)(git_note_data *note_data, void *payload), - void *payload) + git_repository *repo, + const char *notes_ref, + git_note_foreach_cb note_cb, + void *payload) { - int error = -1; - git_oid tree_oid; - git_iterator *iter = NULL; - git_tree *tree = NULL; - const git_index_entry *item; + int error; + git_note_iterator *iter = NULL; + git_oid note_id, annotated_id; - if (normalize_namespace(¬es_ref, repo) < 0) - return -1; + if ((error = git_note_iterator_new(&iter, repo, notes_ref)) < 0) + return error; - if ((error = retrieve_note_tree_oid(&tree_oid, repo, notes_ref)) < 0) - goto cleanup; + while (!(error = git_note_next(¬e_id, &annotated_id, iter))) { + if (note_cb(¬e_id, &annotated_id, payload)) { + error = GIT_EUSER; + break; + } + } - if (git_tree_lookup(&tree, repo, &tree_oid) < 0) - goto cleanup; + if (error == GIT_ITEROVER) + error = 0; - if (git_iterator_for_tree(&iter, repo, tree) < 0) - goto cleanup; + git_note_iterator_free(iter); + return error; +} - if (git_iterator_current(iter, &item) < 0) - goto cleanup; - while (item) { - if (process_entry_path(item->path, &item->oid, note_cb, payload) < 0) - goto cleanup; +void git_note_iterator_free(git_note_iterator *it) +{ + if (it == NULL) + return; - if (git_iterator_advance(iter, &item) < 0) - goto cleanup; - } + git_iterator_free(it); +} - error = 0; + +int git_note_iterator_new( + git_note_iterator **it, + git_repository *repo, + const char *notes_ref) +{ + int error; + git_commit *commit = NULL; + git_tree *tree = NULL; + + error = retrieve_note_tree_and_commit(&tree, &commit, repo, ¬es_ref); + if (error < 0) + goto cleanup; + + if ((error = git_iterator_for_tree(it, tree, 0, NULL, NULL)) < 0) + git_iterator_free(*it); cleanup: - git_iterator_free(iter); git_tree_free(tree); + git_commit_free(commit); + + return error; +} + +int git_note_next( + git_oid* note_id, + git_oid* annotated_id, + git_note_iterator *it) +{ + int error; + const git_index_entry *item; + + if ((error = git_iterator_current(&item, it)) < 0) + goto exit; + + if (item != NULL) { + git_oid_cpy(note_id, &item->oid); + error = process_entry_path(item->path, annotated_id); + + if (error >= 0) + error = git_iterator_advance(NULL, it); + } else { + error = GIT_ITEROVER; + } + +exit: return error; } diff --git a/src/notes.h b/src/notes.h index 219db1ab0..39e18b621 100644 --- a/src/notes.h +++ b/src/notes.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -10,6 +10,7 @@ #include "common.h" #include "git2/oid.h" +#include "git2/types.h" #define GIT_NOTES_DEFAULT_REF "refs/notes/commits" diff --git a/src/object.c b/src/object.c index d3673eda0..80fe51152 100644 --- a/src/object.c +++ b/src/object.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -77,11 +77,63 @@ static int create_object(git_object **object_out, git_otype type) return 0; } +int git_object__from_odb_object( + git_object **object_out, + git_repository *repo, + git_odb_object *odb_obj, + git_otype type) +{ + int error; + 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"); + return GIT_ENOTFOUND; + } + + type = odb_obj->raw.type; + + if ((error = create_object(&object, type)) < 0) + return error; + + /* Initialize parent object */ + git_oid_cpy(&object->cached.oid, &odb_obj->cached.oid); + 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; + + case GIT_OBJ_TAG: + error = git_tag__parse((git_tag *)object, odb_obj); + break; + + case GIT_OBJ_BLOB: + error = git_blob__parse((git_blob *)object, odb_obj); + break; + + default: + break; + } + + if (error < 0) + git_object__free(object); + else + *object_out = git_cache_try_store(&repo->objects, object); + + return error; +} + int git_object_lookup_prefix( git_object **object_out, git_repository *repo, const git_oid *id, - unsigned int len, + size_t len, git_otype type) { git_object *object = NULL; @@ -109,7 +161,7 @@ int git_object_lookup_prefix( if (object != NULL) { if (type != GIT_OBJ_ANY && type != object->type) { git_object_free(object); - giterr_set(GITERR_ODB, "The given type does not match the type in ODB"); + giterr_set(GITERR_INVALID, "The requested type does not match the type in ODB"); return GIT_ENOTFOUND; } @@ -148,51 +200,11 @@ int git_object_lookup_prefix( if (error < 0) return error; - if (type != GIT_OBJ_ANY && type != odb_obj->raw.type) { - git_odb_object_free(odb_obj); - giterr_set(GITERR_ODB, "The given type does not match the type on the ODB"); - return GIT_ENOTFOUND; - } - - type = odb_obj->raw.type; - - if (create_object(&object, type) < 0) - return -1; - - /* Initialize parent object */ - git_oid_cpy(&object->cached.oid, &odb_obj->cached.oid); - 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; - - case GIT_OBJ_TAG: - error = git_tag__parse((git_tag *)object, odb_obj); - break; - - case GIT_OBJ_BLOB: - error = git_blob__parse((git_blob *)object, odb_obj); - break; - - default: - break; - } + error = git_object__from_odb_object(object_out, repo, odb_obj, type); git_odb_object_free(odb_obj); - if (error < 0) { - git_object__free(object); - return -1; - } - - *object_out = git_cache_try_store(&repo->objects, object); - return 0; + return error; } int git_object_lookup(git_object **object_out, git_repository *repo, const git_oid *id, git_otype type) { @@ -292,42 +304,101 @@ size_t git_object__size(git_otype type) return git_objects_table[type].size; } -int git_object__resolve_to_type(git_object **obj, git_otype type) +static int dereference_object(git_object **dereferenced, git_object *obj) { - int error = 0; - git_object *scan, *next; + git_otype type = git_object_type(obj); - if (type == GIT_OBJ_ANY) - return 0; + switch (type) { + case GIT_OBJ_COMMIT: + return git_commit_tree((git_tree **)dereferenced, (git_commit*)obj); - scan = *obj; + case GIT_OBJ_TAG: + return git_tag_target(dereferenced, (git_tag*)obj); - while (!error && scan && git_object_type(scan) != type) { + case GIT_OBJ_BLOB: + return GIT_ENOTFOUND; - switch (git_object_type(scan)) { - case GIT_OBJ_COMMIT: - { - git_tree *tree = NULL; - error = git_commit_tree(&tree, (git_commit *)scan); - next = (git_object *)tree; - break; - } + case GIT_OBJ_TREE: + return GIT_EAMBIGUOUS; - case GIT_OBJ_TAG: - error = git_tag_target(&next, (git_tag *)scan); - break; + default: + return GIT_EINVALIDSPEC; + } +} + +static int peel_error(int error, const git_oid *oid, git_otype type) +{ + const char *type_name; + char hex_oid[GIT_OID_HEXSZ + 1]; + + type_name = git_object_type2string(type); + + git_oid_fmt(hex_oid, oid); + hex_oid[GIT_OID_HEXSZ] = '\0'; + + giterr_set(GITERR_OBJECT, "The git_object of id '%s' can not be " + "successfully peeled into a %s (git_otype=%i).", hex_oid, type_name, type); + + return error; +} + +int git_object_peel( + git_object **peeled, + const git_object *object, + git_otype target_type) +{ + 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); - default: - giterr_set(GITERR_REFERENCE, "Object does not resolve to type"); - error = -1; - next = NULL; - break; + source = (git_object *)object; + + while (!(error = dereference_object(&deref, source))) { + + if (source != object) + git_object_free(source); + + if (git_object_type(deref) == target_type) { + *peeled = deref; + return 0; + } + + if (target_type == GIT_OBJ_ANY && + git_object_type(deref) != git_object_type(object)) + { + *peeled = deref; + return 0; } - git_object_free(scan); - scan = next; + source = deref; + deref = NULL; } - *obj = scan; + if (source != object) + git_object_free(source); + + git_object_free(deref); + + if (error) + error = peel_error(error, git_object_id(object), target_type); + return error; } + +int git_object_dup(git_object **dest, git_object *source) +{ + git_cached_obj_incref(source); + *dest = source; + return 0; +} diff --git a/src/object.h b/src/object.h new file mode 100644 index 000000000..c1e50593c --- /dev/null +++ b/src/object.h @@ -0,0 +1,32 @@ +/* + * 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_object_h__ +#define INCLUDE_object_h__ + +/** Base git object for inheritance */ +struct git_object { + git_cached_obj cached; + git_repository *repo; + git_otype type; +}; + +/* fully free the object; internal method, DO NOT EXPORT */ +void git_object__free(void *object); + +int git_object__from_odb_object( + git_object **object_out, + git_repository *repo, + git_odb_object *odb_obj, + git_otype type); + +int git_object__resolve_to_type(git_object **obj, git_otype type); + +int git_oid__parse(git_oid *oid, const char **buffer_out, const char *buffer_end, const char *header); + +void git_oid__writebuf(git_buf *buf, const char *header, const git_oid *oid); + +#endif @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -12,8 +12,10 @@ #include "hash.h" #include "odb.h" #include "delta-apply.h" +#include "filter.h" #include "git2/odb_backend.h" +#include "git2/oid.h" #define GIT_ALTERNATES_FILE "info/alternates" @@ -21,6 +23,8 @@ #define GIT_LOOSE_PRIORITY 2 #define GIT_PACKED_PRIORITY 1 +#define GIT_ALTERNATES_MAX_DEPTH 5 + typedef struct { git_odb_backend *backend; @@ -28,7 +32,11 @@ typedef struct int is_alternate; } backend_internal; -static int format_object_header(char *hdr, size_t n, size_t obj_len, git_otype obj_type) +size_t git_odb__cache_size = GIT_DEFAULT_CACHE_SIZE; + +static int load_alternates(git_odb *odb, const char *objects_dir, int alternate_depth); + +int git_odb__format_object_header(char *hdr, size_t n, size_t obj_len, git_otype obj_type) { const char *type_str = git_object_type2string(obj_type); int len = p_snprintf(hdr, n, "%s %"PRIuZ, type_str, obj_len); @@ -49,7 +57,7 @@ int git_odb__hashobj(git_oid *id, git_rawobj *obj) if (!obj->data && obj->len != 0) return -1; - hdrlen = format_object_header(header, sizeof(header), obj->len, obj->type); + hdrlen = git_odb__format_object_header(header, sizeof(header), obj->len, obj->type); vec[0].data = header; vec[0].len = hdrlen; @@ -105,6 +113,9 @@ git_otype git_odb_object_type(git_odb_object *object) void git_odb_object_free(git_odb_object *object) { + if (object == NULL) + return; + git_cached_obj_decref((git_cached_obj *)object, &free_odb_object); } @@ -112,31 +123,73 @@ int git_odb__hashfd(git_oid *out, git_file fd, size_t size, git_otype type) { int hdr_len; char hdr[64], buffer[2048]; - git_hash_ctx *ctx; + git_hash_ctx ctx; + ssize_t read_len = 0; + int error = 0; - hdr_len = format_object_header(hdr, sizeof(hdr), size, type); + if (!git_object_typeisloose(type)) { + giterr_set(GITERR_INVALID, "Invalid object type for hash"); + return -1; + } - ctx = git_hash_new_ctx(); + if ((error = git_hash_ctx_init(&ctx)) < 0) + return -1; - git_hash_update(ctx, hdr, hdr_len); + hdr_len = git_odb__format_object_header(hdr, sizeof(hdr), size, type); - while (size > 0) { - ssize_t read_len = read(fd, buffer, sizeof(buffer)); + if ((error = git_hash_update(&ctx, hdr, hdr_len)) < 0) + goto done; - if (read_len < 0) { - git_hash_free_ctx(ctx); - giterr_set(GITERR_OS, "Error reading file"); - return -1; - } + while (size > 0 && (read_len = p_read(fd, buffer, sizeof(buffer))) > 0) { + if ((error = git_hash_update(&ctx, buffer, read_len)) < 0) + goto done; - git_hash_update(ctx, buffer, read_len); size -= read_len; } - git_hash_final(out, ctx); - git_hash_free_ctx(ctx); + /* If p_read returned an error code, the read obviously failed. + * If size is not zero, the file was truncated after we originally + * stat'd it, so we consider this a read failure too */ + if (read_len < 0 || size > 0) { + giterr_set(GITERR_OS, "Error reading file for hashing"); + error = -1; - return 0; + goto done; + return -1; + } + + error = git_hash_final(out, &ctx); + +done: + git_hash_ctx_cleanup(&ctx); + return error; +} + +int git_odb__hashfd_filtered( + git_oid *out, git_file fd, size_t size, git_otype type, git_vector *filters) +{ + int error; + git_buf raw = GIT_BUF_INIT; + git_buf filtered = GIT_BUF_INIT; + + if (!filters || !filters->length) + return git_odb__hashfd(out, fd, size, type); + + /* size of data is used in header, so we have to read the whole file + * into memory to apply filters before beginning to calculate the hash + */ + + if (!(error = git_futils_readbuffer_fd(&raw, fd, size))) + error = git_filters_apply(&filtered, &raw, filters); + + git_buf_free(&raw); + + if (!error) + error = git_odb_hash(out, filtered.ptr, filtered.size, type); + + git_buf_free(&filtered); + + return error; } int git_odb__hashlink(git_oid *out, const char *path) @@ -159,10 +212,11 @@ int git_odb__hashlink(git_oid *out, const char *path) char *link_data; ssize_t read_len; - link_data = git__malloc((size_t)size); + link_data = git__malloc((size_t)(size + 1)); GITERR_CHECK_ALLOC(link_data); - read_len = p_readlink(path, link_data, (size_t)(size + 1)); + read_len = p_readlink(path, link_data, (size_t)size); + link_data[size] = '\0'; if (read_len != (ssize_t)size) { giterr_set(GITERR_OS, "Failed to read symlink data for '%s'", path); return -1; @@ -170,7 +224,7 @@ int git_odb__hashlink(git_oid *out, const char *path) result = git_odb_hash(out, link_data, (size_t)size, GIT_OBJ_BLOB); git__free(link_data); - } else { + } else { int fd = git_futils_open_ro(path); if (fd < 0) return -1; @@ -299,7 +353,7 @@ 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_DEFAULT_CACHE_SIZE, &free_odb_object) < 0 || + if (git_cache_init(&db->cache, git_odb__cache_size, &free_odb_object) < 0 || git_vector_init(&db->backends, 4, backend_sort_cmp) < 0) { git__free(db); @@ -317,6 +371,8 @@ static int add_backend_internal(git_odb *odb, git_odb_backend *backend, int prio assert(odb && backend); + GITERR_CHECK_VERSION(backend, GIT_ODB_BACKEND_VERSION, "git_odb_backend"); + /* Check if the backend is already owned by another ODB */ assert(!backend->odb || backend->odb == odb); @@ -347,7 +403,7 @@ int git_odb_add_alternate(git_odb *odb, git_odb_backend *backend, int priority) return add_backend_internal(odb, backend, priority, 1); } -static int add_default_backends(git_odb *db, const char *objects_dir, int as_alternates) +static int add_default_backends(git_odb *db, const char *objects_dir, int as_alternates, int alternate_depth) { git_odb_backend *loose, *packed; @@ -361,10 +417,10 @@ static int add_default_backends(git_odb *db, const char *objects_dir, int as_alt add_backend_internal(db, packed, GIT_PACKED_PRIORITY, as_alternates) < 0) return -1; - return 0; + return load_alternates(db, objects_dir, alternate_depth); } -static int load_alternates(git_odb *odb, const char *objects_dir) +static int load_alternates(git_odb *odb, const char *objects_dir, int alternate_depth) { git_buf alternates_path = GIT_BUF_INIT; git_buf alternates_buf = GIT_BUF_INIT; @@ -372,6 +428,11 @@ static int load_alternates(git_odb *odb, const char *objects_dir) const char *alternate; int result = 0; + /* Git reports an error, we just ignore anything deeper */ + if (alternate_depth > GIT_ALTERNATES_MAX_DEPTH) { + return 0; + } + if (git_buf_joinpath(&alternates_path, objects_dir, GIT_ALTERNATES_FILE) < 0) return -1; @@ -392,14 +453,18 @@ static int load_alternates(git_odb *odb, const char *objects_dir) if (*alternate == '\0' || *alternate == '#') continue; - /* relative path: build based on the current `objects` folder */ - if (*alternate == '.') { + /* + * Relative path: build based on the current `objects` + * folder. However, relative paths are only allowed in + * the current repository. + */ + if (*alternate == '.' && !alternate_depth) { if ((result = git_buf_joinpath(&alternates_path, objects_dir, alternate)) < 0) break; alternate = git_buf_cstr(&alternates_path); } - if ((result = add_default_backends(odb, alternate, 1)) < 0) + if ((result = add_default_backends(odb, alternate, 1, alternate_depth + 1)) < 0) break; } @@ -409,6 +474,11 @@ static int load_alternates(git_odb *odb, const char *objects_dir) return result; } +int git_odb_add_disk_alternate(git_odb *odb, const char *path) +{ + return add_default_backends(odb, path, 1, 0); +} + int git_odb_open(git_odb **out, const char *objects_dir) { git_odb *db; @@ -420,9 +490,7 @@ int git_odb_open(git_odb **out, const char *objects_dir) if (git_odb_new(&db) < 0) return -1; - if (add_default_backends(db, objects_dir, 0) < 0 || - load_alternates(db, objects_dir) < 0) - { + if (add_default_backends(db, objects_dir, 0, 0) < 0) { git_odb_free(db); return -1; } @@ -433,7 +501,7 @@ int git_odb_open(git_odb **out, const char *objects_dir) static void odb_free(git_odb *db) { - unsigned int i; + size_t i; for (i = 0; i < db->backends.length; ++i) { backend_internal *internal = git_vector_get(&db->backends, i); @@ -461,8 +529,9 @@ void git_odb_free(git_odb *db) int git_odb_exists(git_odb *db, const git_oid *id) { git_odb_object *object; - unsigned int i; + size_t i; bool found = false; + bool refreshed = false; assert(db && id); @@ -471,6 +540,7 @@ int git_odb_exists(git_odb *db, const git_oid *id) return (int)true; } +attempt_lookup: for (i = 0; i < db->backends.length && !found; ++i) { backend_internal *internal = git_vector_get(&db->backends, i); git_odb_backend *b = internal->backend; @@ -479,24 +549,51 @@ int git_odb_exists(git_odb *db, const git_oid *id) found = b->exists(b, id); } + if (!found && !refreshed) { + if (git_odb_refresh(db) < 0) { + giterr_clear(); + return (int)false; + } + + refreshed = true; + goto attempt_lookup; + } + return (int)found; } int git_odb_read_header(size_t *len_p, git_otype *type_p, git_odb *db, const git_oid *id) { - unsigned int i; + int error; + git_odb_object *object; + + error = git_odb__read_header_or_object(&object, len_p, type_p, db, id); + + if (object) + git_odb_object_free(object); + + return error; +} + +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) +{ + size_t i; int error = GIT_ENOTFOUND; git_odb_object *object; - assert(db && id); + 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; - git_odb_object_free(object); + *out = object; return 0; } + *out = NULL; + for (i = 0; i < db->backends.length && error < 0; ++i) { backend_internal *internal = git_vector_get(&db->backends, i); git_odb_backend *b = internal->backend; @@ -517,22 +614,32 @@ int git_odb_read_header(size_t *len_p, git_otype *type_p, git_odb *db, const git *len_p = object->raw.len; *type_p = object->raw.type; - git_odb_object_free(object); + *out = object; + return 0; } int git_odb_read(git_odb_object **out, git_odb *db, const git_oid *id) { - unsigned int i; - int error = GIT_ENOTFOUND; + size_t i; + int error; + bool refreshed = false; git_rawobj raw; 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); if (*out != NULL) return 0; +attempt_lookup: + error = GIT_ENOTFOUND; + for (i = 0; i < db->backends.length && error < 0; ++i) { backend_internal *internal = git_vector_get(&db->backends, i); git_odb_backend *b = internal->backend; @@ -541,9 +648,13 @@ int git_odb_read(git_odb_object **out, git_odb *db, const git_oid *id) error = b->read(&raw.data, &raw.len, &raw.type, b, id); } - /* TODO: If no backends are configured, this returns GIT_ENOTFOUND but - * will never have called giterr_set(). - */ + if (error == GIT_ENOTFOUND && !refreshed) { + if ((error = git_odb_refresh(db)) < 0) + return error; + + refreshed = true; + goto attempt_lookup; + } if (error && error != GIT_PASSTHROUGH) return error; @@ -553,13 +664,14 @@ int git_odb_read(git_odb_object **out, git_odb *db, const git_oid *id) } int git_odb_read_prefix( - git_odb_object **out, git_odb *db, const git_oid *short_id, unsigned int len) + git_odb_object **out, git_odb *db, const git_oid *short_id, size_t len) { - unsigned int i; + size_t i; int error = GIT_ENOTFOUND; git_oid found_full_oid = {{0}}; git_rawobj raw; - bool found = false; + void *data = NULL; + bool found = false, refreshed = false; assert(out && db); @@ -575,11 +687,12 @@ int git_odb_read_prefix( return 0; } +attempt_lookup: for (i = 0; i < db->backends.length; ++i) { backend_internal *internal = git_vector_get(&db->backends, i); git_odb_backend *b = internal->backend; - if (b->read != NULL) { + if (b->read_prefix != NULL) { git_oid full_oid; error = b->read_prefix(&full_oid, &raw.data, &raw.len, &raw.type, b, short_id, len); if (error == GIT_ENOTFOUND || error == GIT_PASSTHROUGH) @@ -588,13 +701,25 @@ int git_odb_read_prefix( if (error) return error; + git__free(data); + data = raw.data; + if (found && git_oid_cmp(&full_oid, &found_full_oid)) return git_odb__error_ambiguous("multiple matches for prefix"); + found_full_oid = full_oid; found = true; } } + if (!found && !refreshed) { + if ((error = git_odb_refresh(db)) < 0) + return error; + + refreshed = true; + goto attempt_lookup; + } + if (!found) return git_odb__error_notfound("no match for prefix", short_id); @@ -602,15 +727,34 @@ int git_odb_read_prefix( return 0; } +int git_odb_foreach(git_odb *db, git_odb_foreach_cb cb, void *payload) +{ + unsigned int i; + backend_internal *internal; + + git_vector_foreach(&db->backends, i, internal) { + git_odb_backend *b = internal->backend; + int error = b->foreach(b, cb, payload); + if (error < 0) + return error; + } + + return 0; +} + int git_odb_write( git_oid *oid, git_odb *db, const void *data, size_t len, git_otype type) { - unsigned int i; + size_t i; int error = GIT_ERROR; git_odb_stream *stream; assert(oid && db); + git_odb_hash(oid, data, len, type); + if (git_odb_exists(db, oid)) + return 0; + for (i = 0; i < db->backends.length && error < 0; ++i) { backend_internal *internal = git_vector_get(&db->backends, i); git_odb_backend *b = internal->backend; @@ -643,7 +787,7 @@ int git_odb_write( int git_odb_open_wstream( git_odb_stream **stream, git_odb *db, size_t size, git_otype type) { - unsigned int i; + size_t i; int error = GIT_ERROR; assert(stream && db); @@ -670,7 +814,7 @@ int git_odb_open_wstream( int git_odb_open_rstream(git_odb_stream **stream, git_odb *db, const git_oid *oid) { - unsigned int i; + size_t i; int error = GIT_ERROR; assert(stream && db); @@ -689,6 +833,56 @@ int git_odb_open_rstream(git_odb_stream **stream, git_odb *db, const git_oid *oi 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; + int error = GIT_ERROR; + + assert(out && db); + + for (i = 0; i < db->backends.length && error < 0; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + /* we don't write in alternates! */ + if (internal->is_alternate) + continue; + + if (b->writepack != NULL) + error = b->writepack(out, b, progress_cb, progress_payload); + } + + if (error == GIT_PASSTHROUGH) + error = 0; + + return error; +} + +void *git_odb_backend_malloc(git_odb_backend *backend, size_t len) +{ + GIT_UNUSED(backend); + return git__malloc(len); +} + +int git_odb_refresh(struct git_odb *db) +{ + size_t i; + assert(db); + + for (i = 0; i < db->backends.length; ++i) { + backend_internal *internal = git_vector_get(&db->backends, i); + git_odb_backend *b = internal->backend; + + if (b->refresh != NULL) { + int error = b->refresh(b); + if (error < 0) + return error; + } + } + + return 0; +} + int git_odb__error_notfound(const char *message, const git_oid *oid) { if (oid != NULL) { @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -46,6 +46,10 @@ struct git_odb { int git_odb__hashobj(git_oid *id, git_rawobj *obj); /* + * Format the object header such as it would appear in the on-disk object + */ +int git_odb__format_object_header(char *hdr, size_t n, size_t obj_len, git_otype obj_type); +/* * Hash an open file descriptor. * This is a performance call when the contents of a fd need to be hashed, * but the fd is already open and we have the size of the contents. @@ -58,12 +62,19 @@ int git_odb__hashobj(git_oid *id, git_rawobj *obj); int git_odb__hashfd(git_oid *out, git_file fd, size_t size, git_otype type); /* - * Hash a `path`, assuming it could be a POSIX symlink: if the path is a symlink, - * then the raw contents of the symlink will be hashed. Otherwise, this will - * fallback to `git_odb__hashfd`. + * Hash an open file descriptor applying an array of filters + * Acts just like git_odb__hashfd with the addition of filters... + */ +int git_odb__hashfd_filtered( + git_oid *out, git_file fd, size_t len, git_otype type, git_vector *filters); + +/* + * Hash a `path`, assuming it could be a POSIX symlink: if the path is a + * symlink, then the raw contents of the symlink will be hashed. Otherwise, + * this will fallback to `git_odb__hashfd`. * - * The hash type for this call is always `GIT_OBJ_BLOB` because symlinks may only - * point to blobs. + * The hash type for this call is always `GIT_OBJ_BLOB` because symlinks may + * only point to blobs. */ int git_odb__hashlink(git_oid *out, const char *path); @@ -77,4 +88,12 @@ int git_odb__error_notfound(const char *message, const git_oid *oid); */ int git_odb__error_ambiguous(const char *message); +/* + * Attempt to read object header or just return whole object if it could + * not be read. + */ +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); + #endif diff --git a/src/odb_loose.c b/src/odb_loose.c index 989b03ab2..68083f7fd 100644 --- a/src/odb_loose.c +++ b/src/odb_loose.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -42,7 +42,7 @@ typedef struct loose_backend { typedef struct { size_t dir_len; unsigned char short_oid[GIT_OID_HEXSZ]; /* hex formatted oid to match */ - unsigned int short_oid_len; + size_t short_oid_len; int found; /* number of matching * objects already found */ unsigned char res_oid[GIT_OID_HEXSZ]; /* hex formatted oid of @@ -502,7 +502,7 @@ static int locate_object_short_oid( git_oid *res_oid, loose_backend *backend, const git_oid *short_oid, - unsigned int len) + size_t len) { char *objects_dir = backend->objects_dir; size_t dir_len = strlen(objects_dir); @@ -629,7 +629,7 @@ static int loose_backend__read_prefix( git_otype *type_p, git_odb_backend *backend, const git_oid *short_oid, - unsigned int len) + size_t len) { int error = 0; @@ -676,6 +676,91 @@ static int loose_backend__exists(git_odb_backend *backend, const git_oid *oid) return !error; } +struct foreach_state { + size_t dir_len; + git_odb_foreach_cb cb; + void *data; + int cb_error; +}; + +GIT_INLINE(int) filename_to_oid(git_oid *oid, const char *ptr) +{ + int v, i = 0; + if (strlen(ptr) != 41) + return -1; + + if (ptr[2] != '/') { + return -1; + } + + v = (git__fromhex(ptr[i]) << 4) | git__fromhex(ptr[i+1]); + if (v < 0) + return -1; + + oid->id[0] = (unsigned char) v; + + ptr += 3; + for (i = 0; i < 38; i += 2) { + v = (git__fromhex(ptr[i]) << 4) | git__fromhex(ptr[i + 1]); + if (v < 0) + return -1; + + oid->id[1 + i/2] = (unsigned char) v; + } + + return 0; +} + +static int foreach_object_dir_cb(void *_state, git_buf *path) +{ + git_oid oid; + struct foreach_state *state = (struct foreach_state *) _state; + + if (filename_to_oid(&oid, path->ptr + state->dir_len) < 0) + return 0; + + if (state->cb(&oid, state->data)) { + state->cb_error = GIT_EUSER; + return -1; + } + + return 0; +} + +static int foreach_cb(void *_state, git_buf *path) +{ + struct foreach_state *state = (struct foreach_state *) _state; + + return git_path_direach(path, foreach_object_dir_cb, state); +} + +static int loose_backend__foreach(git_odb_backend *_backend, git_odb_foreach_cb cb, void *data) +{ + char *objects_dir; + int error; + git_buf buf = GIT_BUF_INIT; + struct foreach_state state; + loose_backend *backend = (loose_backend *) _backend; + + assert(backend && cb); + + objects_dir = backend->objects_dir; + + git_buf_sets(&buf, objects_dir); + git_path_to_dir(&buf); + + memset(&state, 0, sizeof(state)); + state.cb = cb; + state.data = data; + state.dir_len = git_buf_len(&buf); + + error = git_path_direach(&buf, foreach_cb, &state); + + git_buf_free(&buf); + + return state.cb_error ? state.cb_error : error; +} + static int loose_backend__stream_fwrite(git_oid *oid, git_odb_stream *_stream) { loose_writestream *stream = (loose_writestream *)_stream; @@ -785,7 +870,6 @@ static int loose_backend__write(git_oid *oid, git_odb_backend *_backend, const v if (git_buf_joinpath(&final_path, backend->objects_dir, "tmp_object") < 0 || git_filebuf_open(&fbuf, final_path.ptr, - GIT_FILEBUF_HASH_CONTENTS | GIT_FILEBUF_TEMPORARY | (backend->object_zlib_level << GIT_FILEBUF_DEFLATE_SHIFT)) < 0) { @@ -795,7 +879,6 @@ 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); - git_filebuf_hash(oid, &fbuf); if (object_file_name(&final_path, backend->objects_dir, oid) < 0 || git_futils_mkpath2file(final_path.ptr, GIT_OBJECT_DIR_MODE) < 0 || @@ -830,6 +913,7 @@ int git_odb_backend_loose( backend = git__calloc(1, sizeof(loose_backend)); GITERR_CHECK_ALLOC(backend); + backend->parent.version = GIT_ODB_BACKEND_VERSION; backend->objects_dir = git__strdup(objects_dir); GITERR_CHECK_ALLOC(backend->objects_dir); @@ -845,6 +929,7 @@ int git_odb_backend_loose( backend->parent.read_header = &loose_backend__read_header; backend->parent.writestream = &loose_backend__stream; backend->parent.exists = &loose_backend__exists; + backend->parent.foreach = &loose_backend__foreach; backend->parent.free = &loose_backend__free; *backend_out = (git_odb_backend *)backend; diff --git a/src/odb_pack.c b/src/odb_pack.c index 458f288d9..7240a4ac7 100644 --- a/src/odb_pack.c +++ b/src/odb_pack.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -24,7 +24,11 @@ struct pack_backend { git_vector packs; struct git_pack_file *last_found; char *pack_folder; - time_t pack_folder_mtime; +}; + +struct pack_writepack { + struct git_odb_writepack parent; + git_indexer_stream *indexer_stream; }; /** @@ -128,13 +132,9 @@ struct pack_backend { * ***********************************************************/ -static void pack_window_free_all(struct pack_backend *backend, struct git_pack_file *p); -static int pack_window_contains(git_mwindow *win, off_t offset); - static int packfile_sort__cb(const void *a_, const void *b_); static int packfile_load__cb(void *_data, git_buf *path); -static int packfile_refresh_all(struct pack_backend *backend); static int pack_entry_find(struct git_pack_entry *e, struct pack_backend *backend, const git_oid *oid); @@ -149,7 +149,7 @@ static int pack_entry_find_prefix( struct git_pack_entry *e, struct pack_backend *backend, const git_oid *short_oid, - unsigned int len); + size_t len); @@ -159,23 +159,6 @@ static int pack_entry_find_prefix( * ***********************************************************/ -GIT_INLINE(void) pack_window_free_all(struct pack_backend *backend, struct git_pack_file *p) -{ - GIT_UNUSED(backend); - git_mwindow_free_all(&p->mwf); -} - -GIT_INLINE(int) pack_window_contains(git_mwindow *win, off_t offset) -{ - /* We must promise at least 20 bytes (one hash) after the - * offset is available from this window, otherwise the offset - * is not actually in this window and a different window (which - * has that one hash excess) must be used. This is to support - * the object header and delta base parsing routines below. - */ - return git_mwindow_contains(win, offset + 20); -} - static int packfile_sort__cb(const void *a_, const void *b_) { const struct git_pack_file *a = a_; @@ -212,7 +195,7 @@ static int packfile_load__cb(void *_data, git_buf *path) struct pack_backend *backend = (struct pack_backend *)_data; struct git_pack_file *pack; int error; - unsigned int i; + size_t i; if (git__suffixcmp(path->ptr, ".idx") != 0) return 0; /* not an index */ @@ -233,79 +216,61 @@ static int packfile_load__cb(void *_data, git_buf *path) return git_vector_insert(&backend->packs, pack); } -static int packfile_refresh_all(struct pack_backend *backend) +static int pack_entry_find_inner( + struct git_pack_entry *e, + struct pack_backend *backend, + const git_oid *oid, + struct git_pack_file *last_found) { - int error; - struct stat st; + size_t i; - if (backend->pack_folder == NULL) + if (last_found && + git_pack_entry_find(e, last_found, oid, GIT_OID_HEXSZ) == 0) return 0; - if (p_stat(backend->pack_folder, &st) < 0 || !S_ISDIR(st.st_mode)) - return git_odb__error_notfound("failed to refresh packfiles", NULL); - - if (st.st_mtime != backend->pack_folder_mtime) { - git_buf path = GIT_BUF_INIT; - git_buf_sets(&path, backend->pack_folder); - - /* reload all packs */ - error = git_path_direach(&path, packfile_load__cb, (void *)backend); - - git_buf_free(&path); + for (i = 0; i < backend->packs.length; ++i) { + struct git_pack_file *p; - if (error < 0) - return error; + p = git_vector_get(&backend->packs, i); + if (p == last_found) + continue; - git_vector_sort(&backend->packs); - backend->pack_folder_mtime = st.st_mtime; + if (git_pack_entry_find(e, p, oid, GIT_OID_HEXSZ) == 0) { + backend->last_found = p; + return 0; + } } - return 0; + return -1; } static int pack_entry_find(struct git_pack_entry *e, struct pack_backend *backend, const git_oid *oid) { - int error; - unsigned int i; - - if ((error = packfile_refresh_all(backend)) < 0) - return error; + struct git_pack_file *last_found = backend->last_found; if (backend->last_found && git_pack_entry_find(e, backend->last_found, oid, GIT_OID_HEXSZ) == 0) return 0; - for (i = 0; i < backend->packs.length; ++i) { - struct git_pack_file *p; - - p = git_vector_get(&backend->packs, i); - if (p == backend->last_found) - continue; - - if (git_pack_entry_find(e, p, oid, GIT_OID_HEXSZ) == 0) { - backend->last_found = p; - return 0; - } - } + if (!pack_entry_find_inner(e, backend, oid, last_found)) + return 0; return git_odb__error_notfound("failed to find pack entry", oid); } -static int pack_entry_find_prefix( - struct git_pack_entry *e, - struct pack_backend *backend, - const git_oid *short_oid, - unsigned int len) +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) { int error; - unsigned int i; + size_t i; unsigned found = 0; - if ((error = packfile_refresh_all(backend)) < 0) - return error; - - if (backend->last_found) { - error = git_pack_entry_find(e, backend->last_found, short_oid, len); + if (last_found) { + error = git_pack_entry_find(e, last_found, short_oid, len); if (error == GIT_EAMBIGUOUS) return error; if (!error) @@ -316,7 +281,7 @@ static int pack_entry_find_prefix( struct git_pack_file *p; p = git_vector_get(&backend->packs, i); - if (p == backend->last_found) + if (p == last_found) continue; error = git_pack_entry_find(e, p, short_oid, len); @@ -329,6 +294,18 @@ static int pack_entry_find_prefix( } } + 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) @@ -345,20 +322,47 @@ static int pack_entry_find_prefix( * Implement the git_odb_backend API calls * ***********************************************************/ +static int pack_backend__refresh(git_odb_backend *_backend) +{ + struct pack_backend *backend = (struct pack_backend *)_backend; -/* -int pack_backend__read_header(git_rawobj *obj, git_odb_backend *backend, const git_oid *oid) + int error; + struct stat st; + git_buf path = GIT_BUF_INIT; + + if (backend->pack_folder == NULL) + return 0; + + if (p_stat(backend->pack_folder, &st) < 0 || !S_ISDIR(st.st_mode)) + return git_odb__error_notfound("failed to refresh packfiles", NULL); + + git_buf_sets(&path, backend->pack_folder); + + /* reload all packs */ + error = git_path_direach(&path, packfile_load__cb, (void *)backend); + + git_buf_free(&path); + + if (error < 0) + return error; + + git_vector_sort(&backend->packs); + return 0; +} + + +static int pack_backend__read_header(size_t *len_p, git_otype *type_p, struct git_odb_backend *backend, const git_oid *oid) { - pack_location location; + struct git_pack_entry e; + int error; - assert(obj && backend && oid); + assert(len_p && type_p && backend && oid); - if (locate_packfile(&location, (struct pack_backend *)backend, oid) < 0) - return GIT_ENOTFOUND; + if ((error = pack_entry_find(&e, (struct pack_backend *)backend, oid)) < 0) + return error; - return read_header_packed(obj, &location); + return git_packfile_resolve_header(len_p, type_p, e.p, e.offset); } -*/ static int pack_backend__read(void **buffer_p, size_t *len_p, git_otype *type_p, git_odb_backend *backend, const git_oid *oid) { @@ -384,7 +388,7 @@ static int pack_backend__read_prefix( git_otype *type_p, git_odb_backend *backend, const git_oid *short_oid, - unsigned int len) + size_t len) { int error = 0; @@ -420,18 +424,101 @@ static int pack_backend__exists(git_odb_backend *backend, const git_oid *oid) return pack_entry_find(&e, (struct pack_backend *)backend, oid) == 0; } -static void pack_backend__free(git_odb_backend *_backend) +static int pack_backend__foreach(git_odb_backend *_backend, git_odb_foreach_cb cb, void *data) { + int error; + struct git_pack_file *p; struct pack_backend *backend; unsigned int i; + assert(_backend && cb); + backend = (struct pack_backend *)_backend; + + /* Make sure we know about the packfiles */ + if ((error = pack_backend__refresh(_backend)) < 0) + return error; + + git_vector_foreach(&backend->packs, i, p) { + if ((error = git_pack_foreach_entry(p, cb, data)) < 0) + return error; + } + + return 0; +} + +static int pack_backend__writepack_add(struct git_odb_writepack *_writepack, const void *data, size_t size, git_transfer_progress *stats) +{ + struct pack_writepack *writepack = (struct pack_writepack *)_writepack; + + assert(writepack); + + return git_indexer_stream_add(writepack->indexer_stream, data, size, stats); +} + +static int pack_backend__writepack_commit(struct git_odb_writepack *_writepack, git_transfer_progress *stats) +{ + struct pack_writepack *writepack = (struct pack_writepack *)_writepack; + + assert(writepack); + + return git_indexer_stream_finalize(writepack->indexer_stream, stats); +} + +static void pack_backend__writepack_free(struct git_odb_writepack *_writepack) +{ + struct pack_writepack *writepack = (struct pack_writepack *)_writepack; + + assert(writepack); + + git_indexer_stream_free(writepack->indexer_stream); + git__free(writepack); +} + +static int pack_backend__writepack(struct git_odb_writepack **out, + git_odb_backend *_backend, + git_transfer_progress_callback progress_cb, + void *progress_payload) +{ + struct pack_backend *backend; + struct pack_writepack *writepack; + + assert(out && _backend); + + *out = NULL; + + backend = (struct pack_backend *)_backend; + + writepack = git__calloc(1, sizeof(struct pack_writepack)); + GITERR_CHECK_ALLOC(writepack); + + if (git_indexer_stream_new(&writepack->indexer_stream, + backend->pack_folder, progress_cb, progress_payload) < 0) { + git__free(writepack); + return -1; + } + + writepack->parent.backend = _backend; + writepack->parent.add = pack_backend__writepack_add; + writepack->parent.commit = pack_backend__writepack_commit; + writepack->parent.free = pack_backend__writepack_free; + + *out = (git_odb_writepack *)writepack; + + return 0; +} + +static void pack_backend__free(git_odb_backend *_backend) +{ + struct pack_backend *backend; + size_t i; + assert(_backend); backend = (struct pack_backend *)_backend; for (i = 0; i < backend->packs.length; ++i) { struct git_pack_file *p = git_vector_get(&backend->packs, i); - packfile_free(p); + git_packfile_free(p); } git_vector_free(&backend->packs); @@ -439,6 +526,43 @@ 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) +{ + struct pack_backend *backend = NULL; + struct git_pack_file *packfile = NULL; + + if (git_packfile_check(&packfile, idx) < 0) + 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.free = &pack_backend__free; + + *backend_out = (git_odb_backend *)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) { struct pack_backend *backend = NULL; @@ -446,6 +570,7 @@ int git_odb_backend_pack(git_odb_backend **backend_out, const char *objects_dir) 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, 8, packfile_sort__cb) < 0 || git_buf_joinpath(&path, objects_dir, "pack") < 0) @@ -455,14 +580,21 @@ int git_odb_backend_pack(git_odb_backend **backend_out, const char *objects_dir) } if (git_path_isdir(git_buf_cstr(&path)) == true) { + int error; + backend->pack_folder = git_buf_detach(&path); - backend->pack_folder_mtime = 0; + 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 = NULL; + 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; diff --git a/src/offmap.h b/src/offmap.h new file mode 100644 index 000000000..cd46fd687 --- /dev/null +++ b/src/offmap.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2012 the libgit2 contributors + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_offmap_h__ +#define INCLUDE_offmap_h__ + +#include "common.h" +#include "git2/types.h" + +#define kmalloc git__malloc +#define kcalloc git__calloc +#define krealloc git__realloc +#define kfree git__free +#include "khash.h" + +__KHASH_TYPE(off, git_off_t, void *); +typedef khash_t(off) git_offmap; + +#define GIT__USE_OFFMAP \ + __KHASH_IMPL(off, static kh_inline, git_off_t, void *, 1, kh_int64_hash_func, kh_int64_hash_equal); + +#define git_offmap_alloc() kh_init(off) +#define git_offmap_free(h) kh_destroy(off, h), h = NULL +#define git_offmap_clear(h) kh_clear(off, h) + +#define git_offmap_num_entries(h) kh_size(h) + +#define git_offmap_lookup_index(h, k) kh_get(off, h, k) +#define git_offmap_valid_index(h, idx) (idx != kh_end(h)) + +#define git_offmap_exists(h, k) (kh_get(off, h, k) != kh_end(h)) + +#define git_offmap_value_at(h, idx) kh_val(h, idx) +#define git_offmap_set_value_at(h, idx, v) kh_val(h, idx) = v +#define git_offmap_delete_at(h, idx) kh_del(off, h, idx) + +#define git_offmap_insert(h, key, val, rval) do { \ + khiter_t __pos = kh_put(off, h, key, &rval); \ + if (rval >= 0) { \ + if (rval == 0) kh_key(h, __pos) = key; \ + kh_val(h, __pos) = val; \ + } } while (0) + +#define git_offmap_insert2(h, key, val, oldv, rval) do { \ + khiter_t __pos = kh_put(off, h, key, &rval); \ + if (rval >= 0) { \ + if (rval == 0) { \ + oldv = kh_val(h, __pos); \ + kh_key(h, __pos) = key; \ + } else { oldv = NULL; } \ + kh_val(h, __pos) = val; \ + } } while (0) + +#define git_offmap_delete(h, key) do { \ + khiter_t __pos = git_offmap_lookup_index(h, key); \ + if (git_offmap_valid_index(h, __pos)) \ + git_offmap_delete_at(h, __pos); } while (0) + +#define git_offmap_foreach kh_foreach +#define git_offmap_foreach_value kh_foreach_value + +#endif @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -24,11 +24,8 @@ int git_oid_fromstrn(git_oid *out, const char *str, size_t length) size_t p; int v; - if (length < 4) - return oid_error_invalid("input too short"); - if (length > GIT_OID_HEXSZ) - length = GIT_OID_HEXSZ; + return oid_error_invalid("too long"); for (p = 0; p < length - 1; p += 2) { v = (git__fromhex(str[p + 0]) << 4) @@ -54,6 +51,11 @@ int git_oid_fromstrn(git_oid *out, const char *str, size_t length) return 0; } +int git_oid_fromstrp(git_oid *out, const char *str) +{ + return git_oid_fromstrn(out, str, strlen(str)); +} + int git_oid_fromstr(git_oid *out, const char *str) { return git_oid_fromstrn(out, str, GIT_OID_HEXSZ); @@ -98,11 +100,14 @@ char *git_oid_tostr(char *out, size_t n, const git_oid *oid) { char str[GIT_OID_HEXSZ]; - if (!out || n == 0 || !oid) + 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) @@ -161,12 +166,7 @@ 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 memcmp(a->id, b->id, sizeof(a->id)); -} - -int git_oid_ncmp(const git_oid *oid_a, const git_oid *oid_b, unsigned int len) +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; @@ -301,7 +301,7 @@ void git_oid_shorten_free(git_oid_shorten *os) * - Each normal node points to 16 children (one for each possible * character in the oid). This is *not* stored in an array of * pointers, because in a 64-bit arch this would be sucking - * 16*sizeof(void*) = 128 bytes of memory per node, which is fucking + * 16*sizeof(void*) = 128 bytes of memory per node, which is * insane. What we do is store Node Indexes, and use these indexes * to look up each node in the om->index array. These indexes are * signed shorts, so this limits the amount of unique OIDs that diff --git a/src/oidmap.h b/src/oidmap.h index 858268c92..40274cd19 100644 --- a/src/oidmap.h +++ b/src/oidmap.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -28,13 +28,8 @@ GIT_INLINE(khint_t) hash_git_oid(const git_oid *oid) return h; } -GIT_INLINE(int) hash_git_oid_equal(const git_oid *a, const git_oid *b) -{ - return (memcmp(a->id, b->id, sizeof(a->id)) == 0); -} - #define GIT__USE_OIDMAP \ - __KHASH_IMPL(oid, static inline, const git_oid *, void *, 1, hash_git_oid, hash_git_oid_equal) + __KHASH_IMPL(oid, static kh_inline, const git_oid *, void *, 1, hash_git_oid, 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 new file mode 100644 index 000000000..459201f58 --- /dev/null +++ b/src/pack-objects.c @@ -0,0 +1,1342 @@ +/* + * 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 "pack-objects.h" + +#include "compress.h" +#include "delta.h" +#include "iterator.h" +#include "netops.h" +#include "pack.h" +#include "thread-utils.h" +#include "tree.h" + +#include "git2/pack.h" +#include "git2/commit.h" +#include "git2/tag.h" +#include "git2/indexer.h" +#include "git2/config.h" + +struct unpacked { + git_pobject *object; + void *data; + struct git_delta_index *index; + unsigned int depth; +}; + +struct tree_walk_context { + git_packbuilder *pb; + git_buf buf; +}; + +#ifdef GIT_THREADS + +#define GIT_PACKBUILDER__MUTEX_OP(pb, mtx, op) do { \ + int result = git_mutex_##op(&(pb)->mtx); \ + assert(!result); \ + GIT_UNUSED(result); \ + } while (0) + +#else + +#define GIT_PACKBUILDER__MUTEX_OP(pb,mtx,op) GIT_UNUSED(pb) + +#endif /* GIT_THREADS */ + +#define git_packbuilder__cache_lock(pb) GIT_PACKBUILDER__MUTEX_OP(pb, cache_mutex, lock) +#define git_packbuilder__cache_unlock(pb) GIT_PACKBUILDER__MUTEX_OP(pb, cache_mutex, unlock) +#define git_packbuilder__progress_lock(pb) GIT_PACKBUILDER__MUTEX_OP(pb, progress_mutex, lock) +#define git_packbuilder__progress_unlock(pb) GIT_PACKBUILDER__MUTEX_OP(pb, progress_mutex, unlock) + +static unsigned name_hash(const char *name) +{ + unsigned c, hash = 0; + + if (!name) + return 0; + + /* + * This effectively just creates a sortable number from the + * last sixteen non-whitespace characters. Last characters + * count "most", so things that end in ".c" sort together. + */ + while ((c = *name++) != 0) { + if (git__isspace(c)) + continue; + hash = (hash >> 2) + (c << 24); + } + return hash; +} + +static int packbuilder_config(git_packbuilder *pb) +{ + git_config *config; + int ret; + int64_t val; + + if (git_repository_config__weakptr(&config, pb->repo) < 0) + return -1; + +#define config_get(KEY,DST,DFLT) do { \ + ret = git_config_get_int64(&val, config, KEY); \ + if (!ret) (DST) = val; \ + else if (ret == GIT_ENOTFOUND) (DST) = (DFLT); \ + else if (ret < 0) return -1; } while (0) + + config_get("pack.deltaCacheSize", pb->max_delta_cache_size, + GIT_PACK_DELTA_CACHE_SIZE); + config_get("pack.deltaCacheLimit", pb->cache_max_small_delta_size, + GIT_PACK_DELTA_CACHE_LIMIT); + config_get("pack.deltaCacheSize", pb->big_file_threshold, + GIT_PACK_BIG_FILE_THRESHOLD); + config_get("pack.windowMemory", pb->window_memory_limit, 0); + +#undef config_get + + return 0; +} + +int git_packbuilder_new(git_packbuilder **out, git_repository *repo) +{ + git_packbuilder *pb; + + *out = NULL; + + pb = git__calloc(1, sizeof(*pb)); + GITERR_CHECK_ALLOC(pb); + + pb->object_ix = git_oidmap_alloc(); + + if (!pb->object_ix) + goto on_error; + + pb->repo = repo; + pb->nr_threads = 1; /* do not spawn any thread by default */ + + if (git_hash_ctx_init(&pb->ctx) < 0 || + git_repository_odb(&pb->odb, repo) < 0 || + packbuilder_config(pb) < 0) + goto on_error; + +#ifdef GIT_THREADS + + if (git_mutex_init(&pb->cache_mutex) || + git_mutex_init(&pb->progress_mutex) || + git_cond_init(&pb->progress_cond)) + goto on_error; + +#endif + + *out = pb; + return 0; + +on_error: + git_packbuilder_free(pb); + return -1; +} + +unsigned int git_packbuilder_set_threads(git_packbuilder *pb, unsigned int n) +{ + assert(pb); + +#ifdef GIT_THREADS + pb->nr_threads = n; +#else + GIT_UNUSED(n); + assert(1 == pb->nr_threads); +#endif + + return pb->nr_threads; +} + +static void rehash(git_packbuilder *pb) +{ + git_pobject *po; + khiter_t pos; + unsigned int i; + int ret; + + kh_clear(oid, pb->object_ix); + for (i = 0, po = pb->object_list; i < pb->nr_objects; i++, po++) { + pos = kh_put(oid, pb->object_ix, &po->id, &ret); + kh_value(pb->object_ix, pos) = po; + } +} + +int git_packbuilder_insert(git_packbuilder *pb, const git_oid *oid, + const char *name) +{ + git_pobject *po; + khiter_t pos; + int ret; + + assert(pb && oid); + + /* If the object already exists in the hash table, then we don't + * have any work to do */ + pos = kh_get(oid, pb->object_ix, oid); + if (pos != kh_end(pb->object_ix)) + return 0; + + if (pb->nr_objects >= pb->nr_alloc) { + pb->nr_alloc = (pb->nr_alloc + 1024) * 3 / 2; + pb->object_list = git__realloc(pb->object_list, + pb->nr_alloc * sizeof(*po)); + GITERR_CHECK_ALLOC(pb->object_list); + rehash(pb); + } + + po = pb->object_list + pb->nr_objects; + memset(po, 0x0, sizeof(*po)); + + if (git_odb_read_header(&po->size, &po->type, pb->odb, oid) < 0) + return -1; + + pb->nr_objects++; + git_oid_cpy(&po->id, oid); + po->hash = name_hash(name); + + pos = kh_put(oid, pb->object_ix, &po->id, &ret); + assert(ret != 0); + kh_value(pb->object_ix, pos) = po; + + pb->done = false; + return 0; +} + +/* + * The per-object header is a pretty dense thing, which is + * - first byte: low four bits are "size", + * then three bits of "type", + * with the high bit being "size continues". + * - each byte afterwards: low seven bits are size continuation, + * with the high bit being "size continues" + */ +static int gen_pack_object_header( + unsigned char *hdr, + unsigned long size, + git_otype type) +{ + unsigned char *hdr_base; + unsigned char c; + + assert(type >= GIT_OBJ_COMMIT && type <= GIT_OBJ_REF_DELTA); + + /* TODO: add support for chunked objects; see git.git 6c0d19b1 */ + + c = (unsigned char)((type << 4) | (size & 15)); + size >>= 4; + hdr_base = hdr; + + while (size) { + *hdr++ = c | 0x80; + c = size & 0x7f; + size >>= 7; + } + *hdr++ = c; + + return (int)(hdr - hdr_base); +} + +static int get_delta(void **out, git_odb *odb, git_pobject *po) +{ + git_odb_object *src = NULL, *trg = NULL; + unsigned long delta_size; + void *delta_buf; + + *out = NULL; + + if (git_odb_read(&src, odb, &po->delta->id) < 0 || + git_odb_read(&trg, odb, &po->id) < 0) + goto on_error; + + delta_buf = git_delta( + git_odb_object_data(src), (unsigned long)git_odb_object_size(src), + git_odb_object_data(trg), (unsigned long)git_odb_object_size(trg), + &delta_size, 0); + + if (!delta_buf || delta_size != po->delta_size) { + giterr_set(GITERR_INVALID, "Delta size changed"); + goto on_error; + } + + *out = delta_buf; + + git_odb_object_free(src); + git_odb_object_free(trg); + return 0; + +on_error: + git_odb_object_free(src); + git_odb_object_free(trg); + return -1; +} + +static int write_object(git_buf *buf, git_packbuilder *pb, git_pobject *po) +{ + git_odb_object *obj = NULL; + git_buf zbuf = GIT_BUF_INIT; + git_otype type; + unsigned char hdr[10]; + unsigned int hdr_len; + unsigned long size; + void *data; + + if (po->delta) { + if (po->delta_data) + data = po->delta_data; + else if (get_delta(&data, pb->odb, po) < 0) + goto on_error; + size = po->delta_size; + type = GIT_OBJ_REF_DELTA; + } else { + if (git_odb_read(&obj, pb->odb, &po->id)) + goto on_error; + + data = (void *)git_odb_object_data(obj); + size = (unsigned long)git_odb_object_size(obj); + type = git_odb_object_type(obj); + } + + /* Write header */ + hdr_len = gen_pack_object_header(hdr, size, type); + + if (git_buf_put(buf, (char *)hdr, hdr_len) < 0) + goto on_error; + + if (git_hash_update(&pb->ctx, hdr, hdr_len) < 0) + goto on_error; + + if (type == GIT_OBJ_REF_DELTA) { + if (git_buf_put(buf, (char *)po->delta->id.id, GIT_OID_RAWSZ) < 0 || + git_hash_update(&pb->ctx, po->delta->id.id, GIT_OID_RAWSZ) < 0) + goto on_error; + } + + /* Write data */ + if (po->z_delta_size) + size = po->z_delta_size; + else if (git__compress(&zbuf, data, size) < 0) + goto on_error; + else { + if (po->delta) + git__free(data); + data = zbuf.ptr; + size = (unsigned long)zbuf.size; + } + + if (git_buf_put(buf, data, size) < 0 || + git_hash_update(&pb->ctx, data, size) < 0) + goto on_error; + + if (po->delta_data) + git__free(po->delta_data); + + git_odb_object_free(obj); + git_buf_free(&zbuf); + + pb->nr_written++; + return 0; + +on_error: + git_odb_object_free(obj); + git_buf_free(&zbuf); + return -1; +} + +enum write_one_status { + WRITE_ONE_SKIP = -1, /* already written */ + WRITE_ONE_BREAK = 0, /* writing this will bust the limit; not written */ + WRITE_ONE_WRITTEN = 1, /* normal */ + WRITE_ONE_RECURSIVE = 2 /* already scheduled to be written */ +}; + +static int write_one(git_buf *buf, git_packbuilder *pb, git_pobject *po, + enum write_one_status *status) +{ + if (po->recursing) { + *status = WRITE_ONE_RECURSIVE; + return 0; + } else if (po->written) { + *status = WRITE_ONE_SKIP; + return 0; + } + + if (po->delta) { + po->recursing = 1; + if (write_one(buf, pb, po->delta, status) < 0) + return -1; + switch (*status) { + case WRITE_ONE_RECURSIVE: + /* we cannot depend on this one */ + po->delta = NULL; + break; + default: + break; + } + } + + po->written = 1; + po->recursing = 0; + return write_object(buf, pb, po); +} + +GIT_INLINE(void) add_to_write_order(git_pobject **wo, unsigned int *endp, + git_pobject *po) +{ + if (po->filled) + return; + wo[(*endp)++] = po; + po->filled = 1; +} + +static void add_descendants_to_write_order(git_pobject **wo, unsigned int *endp, + git_pobject *po) +{ + int add_to_order = 1; + while (po) { + if (add_to_order) { + git_pobject *s; + /* add this node... */ + add_to_write_order(wo, endp, po); + /* all its siblings... */ + for (s = po->delta_sibling; s; s = s->delta_sibling) { + add_to_write_order(wo, endp, s); + } + } + /* drop down a level to add left subtree nodes if possible */ + if (po->delta_child) { + add_to_order = 1; + po = po->delta_child; + } else { + add_to_order = 0; + /* our sibling might have some children, it is next */ + if (po->delta_sibling) { + po = po->delta_sibling; + continue; + } + /* go back to our parent node */ + po = po->delta; + while (po && !po->delta_sibling) { + /* we're on the right side of a subtree, keep + * going up until we can go right again */ + po = po->delta; + } + if (!po) { + /* done- we hit our original root node */ + return; + } + /* pass it off to sibling at this level */ + po = po->delta_sibling; + } + }; +} + +static void add_family_to_write_order(git_pobject **wo, unsigned int *endp, + git_pobject *po) +{ + git_pobject *root; + + for (root = po; root->delta; root = root->delta) + ; /* nothing */ + add_descendants_to_write_order(wo, endp, root); +} + +static int cb_tag_foreach(const char *name, git_oid *oid, void *data) +{ + git_packbuilder *pb = data; + git_pobject *po; + khiter_t pos; + + GIT_UNUSED(name); + + pos = kh_get(oid, pb->object_ix, oid); + if (pos == kh_end(pb->object_ix)) + return 0; + + po = kh_value(pb->object_ix, pos); + po->tagged = 1; + + /* TODO: peel objects */ + + return 0; +} + +static git_pobject **compute_write_order(git_packbuilder *pb) +{ + unsigned int i, wo_end, last_untagged; + + git_pobject **wo = git__malloc(sizeof(*wo) * pb->nr_objects); + + for (i = 0; i < pb->nr_objects; i++) { + git_pobject *po = pb->object_list + i; + po->tagged = 0; + po->filled = 0; + po->delta_child = NULL; + po->delta_sibling = NULL; + } + + /* + * Fully connect delta_child/delta_sibling network. + * Make sure delta_sibling is sorted in the original + * recency order. + */ + for (i = pb->nr_objects; i > 0;) { + git_pobject *po = &pb->object_list[--i]; + if (!po->delta) + continue; + /* Mark me as the first child */ + po->delta_sibling = po->delta->delta_child; + po->delta->delta_child = po; + } + + /* + * Mark objects that are at the tip of tags. + */ + if (git_tag_foreach(pb->repo, &cb_tag_foreach, pb) < 0) + return NULL; + + /* + * Give the objects in the original recency order until + * we see a tagged tip. + */ + for (i = wo_end = 0; i < pb->nr_objects; i++) { + git_pobject *po = pb->object_list + i; + if (po->tagged) + break; + add_to_write_order(wo, &wo_end, po); + } + last_untagged = i; + + /* + * Then fill all the tagged tips. + */ + for (; i < pb->nr_objects; i++) { + git_pobject *po = pb->object_list + i; + if (po->tagged) + add_to_write_order(wo, &wo_end, po); + } + + /* + * And then all remaining commits and tags. + */ + for (i = last_untagged; i < pb->nr_objects; i++) { + git_pobject *po = pb->object_list + i; + if (po->type != GIT_OBJ_COMMIT && + po->type != GIT_OBJ_TAG) + continue; + add_to_write_order(wo, &wo_end, po); + } + + /* + * And then all the trees. + */ + for (i = last_untagged; i < pb->nr_objects; i++) { + git_pobject *po = pb->object_list + i; + if (po->type != GIT_OBJ_TREE) + continue; + add_to_write_order(wo, &wo_end, po); + } + + /* + * Finally all the rest in really tight order + */ + for (i = last_untagged; i < pb->nr_objects; i++) { + git_pobject *po = pb->object_list + i; + if (!po->filled) + add_family_to_write_order(wo, &wo_end, po); + } + + if (wo_end != pb->nr_objects) { + giterr_set(GITERR_INVALID, "invalid write order"); + return NULL; + } + + return wo; +} + +static int write_pack(git_packbuilder *pb, + int (*cb)(void *buf, size_t size, void *data), + void *data) +{ + git_pobject **write_order; + git_pobject *po; + git_buf buf = GIT_BUF_INIT; + enum write_one_status status; + struct git_pack_header ph; + unsigned int i = 0; + + write_order = compute_write_order(pb); + if (write_order == NULL) + goto on_error; + + /* Write pack header */ + ph.hdr_signature = htonl(PACK_SIGNATURE); + ph.hdr_version = htonl(PACK_VERSION); + ph.hdr_entries = htonl(pb->nr_objects); + + if (cb(&ph, sizeof(ph), data) < 0) + goto on_error; + + if (git_hash_update(&pb->ctx, &ph, sizeof(ph)) < 0) + goto on_error; + + pb->nr_remaining = pb->nr_objects; + do { + pb->nr_written = 0; + for ( ; i < pb->nr_objects; ++i) { + po = write_order[i]; + if (write_one(&buf, pb, po, &status) < 0) + goto on_error; + if (cb(buf.ptr, buf.size, data) < 0) + goto on_error; + git_buf_clear(&buf); + } + + pb->nr_remaining -= pb->nr_written; + } while (pb->nr_remaining && i < pb->nr_objects); + + git__free(write_order); + git_buf_free(&buf); + + if (git_hash_final(&pb->pack_oid, &pb->ctx) < 0) + goto on_error; + + return cb(pb->pack_oid.id, GIT_OID_RAWSZ, data); + +on_error: + git__free(write_order); + git_buf_free(&buf); + return -1; +} + +static int write_pack_buf(void *buf, size_t size, void *data) +{ + git_buf *b = (git_buf *)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; + const git_pobject *b = (git_pobject *)_b; + + if (a->type > b->type) + return -1; + if (a->type < b->type) + return 1; + if (a->hash > b->hash) + return -1; + if (a->hash < b->hash) + return 1; + /* + * TODO + * + if (a->preferred_base > b->preferred_base) + return -1; + if (a->preferred_base < b->preferred_base) + return 1; + */ + if (a->size > b->size) + return -1; + if (a->size < b->size) + return 1; + return a < b ? -1 : (a > b); /* newest first */ +} + +static int delta_cacheable(git_packbuilder *pb, unsigned long src_size, + unsigned long trg_size, unsigned long delta_size) +{ + if (pb->max_delta_cache_size && + pb->delta_cache_size + delta_size > pb->max_delta_cache_size) + return 0; + + if (delta_size < pb->cache_max_small_delta_size) + return 1; + + /* cache delta, if objects are large enough compared to delta size */ + if ((src_size >> 20) + (trg_size >> 21) > (delta_size >> 10)) + return 1; + + return 0; +} + +static int try_delta(git_packbuilder *pb, struct unpacked *trg, + struct unpacked *src, unsigned int max_depth, + unsigned long *mem_usage, int *ret) +{ + git_pobject *trg_object = trg->object; + git_pobject *src_object = src->object; + git_odb_object *obj; + unsigned long trg_size, src_size, delta_size, + sizediff, max_size, sz; + unsigned int ref_depth; + void *delta_buf; + + /* Don't bother doing diffs between different types */ + if (trg_object->type != src_object->type) { + *ret = -1; + return 0; + } + + *ret = 0; + + /* TODO: support reuse-delta */ + + /* Let's not bust the allowed depth. */ + if (src->depth >= max_depth) + return 0; + + /* Now some size filtering heuristics. */ + trg_size = (unsigned long)trg_object->size; + if (!trg_object->delta) { + max_size = trg_size/2 - 20; + ref_depth = 1; + } else { + max_size = trg_object->delta_size; + ref_depth = trg->depth; + } + + max_size = (uint64_t)max_size * (max_depth - src->depth) / + (max_depth - ref_depth + 1); + if (max_size == 0) + return 0; + + src_size = (unsigned long)src_object->size; + sizediff = src_size < trg_size ? trg_size - src_size : 0; + if (sizediff >= max_size) + return 0; + if (trg_size < src_size / 32) + return 0; + + /* Load data if not already done */ + if (!trg->data) { + if (git_odb_read(&obj, pb->odb, &trg_object->id) < 0) + return -1; + + sz = (unsigned long)git_odb_object_size(obj); + trg->data = git__malloc(sz); + GITERR_CHECK_ALLOC(trg->data); + memcpy(trg->data, git_odb_object_data(obj), sz); + + git_odb_object_free(obj); + + if (sz != trg_size) { + giterr_set(GITERR_INVALID, + "Inconsistent target object length"); + return -1; + } + + *mem_usage += sz; + } + if (!src->data) { + if (git_odb_read(&obj, pb->odb, &src_object->id) < 0) + return -1; + + sz = (unsigned long)git_odb_object_size(obj); + src->data = git__malloc(sz); + GITERR_CHECK_ALLOC(src->data); + memcpy(src->data, git_odb_object_data(obj), sz); + + git_odb_object_free(obj); + + if (sz != src_size) { + giterr_set(GITERR_INVALID, + "Inconsistent source object length"); + return -1; + } + + *mem_usage += sz; + } + if (!src->index) { + src->index = git_delta_create_index(src->data, src_size); + if (!src->index) + return 0; /* suboptimal pack - out of memory */ + + *mem_usage += git_delta_sizeof_index(src->index); + } + + delta_buf = git_delta_create(src->index, trg->data, trg_size, + &delta_size, max_size); + if (!delta_buf) + return 0; + + if (trg_object->delta) { + /* Prefer only shallower same-sized deltas. */ + if (delta_size == trg_object->delta_size && + src->depth + 1 >= trg->depth) { + git__free(delta_buf); + return 0; + } + } + + git_packbuilder__cache_lock(pb); + if (trg_object->delta_data) { + git__free(trg_object->delta_data); + pb->delta_cache_size -= trg_object->delta_size; + trg_object->delta_data = NULL; + } + if (delta_cacheable(pb, src_size, trg_size, delta_size)) { + pb->delta_cache_size += delta_size; + git_packbuilder__cache_unlock(pb); + + trg_object->delta_data = git__realloc(delta_buf, delta_size); + GITERR_CHECK_ALLOC(trg_object->delta_data); + } else { + /* create delta when writing the pack */ + git_packbuilder__cache_unlock(pb); + git__free(delta_buf); + } + + trg_object->delta = src_object; + trg_object->delta_size = delta_size; + trg->depth = src->depth + 1; + + *ret = 1; + return 0; +} + +static unsigned int check_delta_limit(git_pobject *me, unsigned int n) +{ + git_pobject *child = me->delta_child; + unsigned int m = n; + + while (child) { + unsigned int c = check_delta_limit(child, n + 1); + if (m < c) + m = c; + child = child->delta_sibling; + } + return m; +} + +static unsigned long free_unpacked(struct unpacked *n) +{ + unsigned long freed_mem = git_delta_sizeof_index(n->index); + git_delta_free_index(n->index); + n->index = NULL; + if (n->data) { + freed_mem += (unsigned long)n->object->size; + git__free(n->data); + n->data = NULL; + } + n->object = NULL; + n->depth = 0; + return freed_mem; +} + +static int find_deltas(git_packbuilder *pb, git_pobject **list, + unsigned int *list_size, unsigned int window, + unsigned int depth) +{ + git_pobject *po; + git_buf zbuf = GIT_BUF_INIT; + struct unpacked *array; + uint32_t idx = 0, count = 0; + unsigned long mem_usage = 0; + unsigned int i; + int error = -1; + + array = git__calloc(window, sizeof(struct unpacked)); + GITERR_CHECK_ALLOC(array); + + for (;;) { + struct unpacked *n = array + idx; + unsigned int max_depth; + int j, best_base = -1; + + git_packbuilder__progress_lock(pb); + if (!*list_size) { + git_packbuilder__progress_unlock(pb); + break; + } + + po = *list++; + (*list_size)--; + git_packbuilder__progress_unlock(pb); + + mem_usage -= free_unpacked(n); + n->object = po; + + while (pb->window_memory_limit && + mem_usage > pb->window_memory_limit && + count > 1) { + uint32_t tail = (idx + window - count) % window; + mem_usage -= free_unpacked(array + tail); + count--; + } + + /* + * If the current object is at pack edge, take the depth the + * objects that depend on the current object into account + * otherwise they would become too deep. + */ + max_depth = depth; + if (po->delta_child) { + max_depth -= check_delta_limit(po, 0); + if (max_depth <= 0) + goto next; + } + + j = window; + while (--j > 0) { + int ret; + uint32_t other_idx = idx + j; + struct unpacked *m; + + if (other_idx >= window) + other_idx -= window; + + m = array + other_idx; + if (!m->object) + break; + + if (try_delta(pb, n, m, max_depth, &mem_usage, &ret) < 0) + goto on_error; + if (ret < 0) + break; + else if (ret > 0) + best_base = other_idx; + } + + /* + * If we decided to cache the delta data, then it is best + * to compress it right away. First because we have to do + * it anyway, and doing it here while we're threaded will + * save a lot of time in the non threaded write phase, + * as well as allow for caching more deltas within + * the same cache size limit. + * ... + * But only if not writing to stdout, since in that case + * the network is most likely throttling writes anyway, + * and therefore it is best to go to the write phase ASAP + * instead, as we can afford spending more time compressing + * between writes at that moment. + */ + if (po->delta_data) { + if (git__compress(&zbuf, po->delta_data, po->delta_size) < 0) + goto on_error; + + git__free(po->delta_data); + po->delta_data = git__malloc(zbuf.size); + GITERR_CHECK_ALLOC(po->delta_data); + + memcpy(po->delta_data, zbuf.ptr, zbuf.size); + po->z_delta_size = (unsigned long)zbuf.size; + git_buf_clear(&zbuf); + + git_packbuilder__cache_lock(pb); + pb->delta_cache_size -= po->delta_size; + pb->delta_cache_size += po->z_delta_size; + git_packbuilder__cache_unlock(pb); + } + + /* + * If we made n a delta, and if n is already at max + * depth, leaving it in the window is pointless. we + * should evict it first. + */ + if (po->delta && max_depth <= n->depth) + continue; + + /* + * Move the best delta base up in the window, after the + * currently deltified object, to keep it longer. It will + * be the first base object to be attempted next. + */ + if (po->delta) { + struct unpacked swap = array[best_base]; + int dist = (window + idx - best_base) % window; + int dst = best_base; + while (dist--) { + int src = (dst + 1) % window; + array[dst] = array[src]; + dst = src; + } + array[dst] = swap; + } + + next: + idx++; + if (count + 1 < window) + count++; + if (idx >= window) + idx = 0; + } + error = 0; + +on_error: + for (i = 0; i < window; ++i) { + git__free(array[i].index); + git__free(array[i].data); + } + git__free(array); + git_buf_free(&zbuf); + + return error; +} + +#ifdef GIT_THREADS + +struct thread_params { + git_thread thread; + git_packbuilder *pb; + + git_pobject **list; + + git_cond cond; + git_mutex mutex; + + unsigned int list_size; + unsigned int remaining; + + int window; + int depth; + int working; + int data_ready; +}; + +static void *threaded_find_deltas(void *arg) +{ + struct thread_params *me = arg; + + while (me->remaining) { + if (find_deltas(me->pb, me->list, &me->remaining, + me->window, me->depth) < 0) { + ; /* TODO */ + } + + git_packbuilder__progress_lock(me->pb); + me->working = 0; + git_cond_signal(&me->pb->progress_cond); + git_packbuilder__progress_unlock(me->pb); + + if (git_mutex_lock(&me->mutex)) { + giterr_set(GITERR_THREAD, "unable to lock packfile condition mutex"); + return NULL; + } + + while (!me->data_ready) + git_cond_wait(&me->cond, &me->mutex); + + /* + * We must not set ->data_ready before we wait on the + * condition because the main thread may have set it to 1 + * before we get here. In order to be sure that new + * work is available if we see 1 in ->data_ready, it + * was initialized to 0 before this thread was spawned + * and we reset it to 0 right away. + */ + me->data_ready = 0; + git_mutex_unlock(&me->mutex); + } + /* leave ->working 1 so that this doesn't get more work assigned */ + return NULL; +} + +static int ll_find_deltas(git_packbuilder *pb, git_pobject **list, + unsigned int list_size, unsigned int window, + unsigned int depth) +{ + struct thread_params *p; + int i, ret, active_threads = 0; + + if (!pb->nr_threads) + pb->nr_threads = git_online_cpus(); + + if (pb->nr_threads <= 1) { + find_deltas(pb, list, &list_size, window, depth); + return 0; + } + + p = git__malloc(pb->nr_threads * sizeof(*p)); + GITERR_CHECK_ALLOC(p); + + /* Partition the work among the threads */ + for (i = 0; i < pb->nr_threads; ++i) { + unsigned sub_size = list_size / (pb->nr_threads - i); + + /* don't use too small segments or no deltas will be found */ + if (sub_size < 2*window && i+1 < pb->nr_threads) + sub_size = 0; + + p[i].pb = pb; + p[i].window = window; + p[i].depth = depth; + p[i].working = 1; + p[i].data_ready = 0; + + /* try to split chunks on "path" boundaries */ + while (sub_size && sub_size < list_size && + list[sub_size]->hash && + list[sub_size]->hash == list[sub_size-1]->hash) + sub_size++; + + p[i].list = list; + p[i].list_size = sub_size; + p[i].remaining = sub_size; + + list += sub_size; + list_size -= sub_size; + } + + /* Start work threads */ + for (i = 0; i < pb->nr_threads; ++i) { + if (!p[i].list_size) + continue; + + git_mutex_init(&p[i].mutex); + git_cond_init(&p[i].cond); + + ret = git_thread_create(&p[i].thread, NULL, + threaded_find_deltas, &p[i]); + if (ret) { + giterr_set(GITERR_THREAD, "unable to create thread"); + return -1; + } + active_threads++; + } + + /* + * Now let's wait for work completion. Each time a thread is done + * with its work, we steal half of the remaining work from the + * thread with the largest number of unprocessed objects and give + * it to that newly idle thread. This ensure good load balancing + * until the remaining object list segments are simply too short + * to be worth splitting anymore. + */ + while (active_threads) { + struct thread_params *target = NULL; + struct thread_params *victim = NULL; + unsigned sub_size = 0; + + /* Start by locating a thread that has transitioned its + * 'working' flag from 1 -> 0. This indicates that it is + * ready to receive more work using our work-stealing + * algorithm. */ + git_packbuilder__progress_lock(pb); + for (;;) { + for (i = 0; !target && i < pb->nr_threads; i++) + if (!p[i].working) + target = &p[i]; + if (target) + break; + git_cond_wait(&pb->progress_cond, &pb->progress_mutex); + } + + /* At this point we hold the progress lock and have located + * a thread to receive more work. We still need to locate a + * thread from which to steal work (the victim). */ + for (i = 0; i < pb->nr_threads; i++) + if (p[i].remaining > 2*window && + (!victim || victim->remaining < p[i].remaining)) + victim = &p[i]; + + if (victim) { + sub_size = victim->remaining / 2; + list = victim->list + victim->list_size - sub_size; + while (sub_size && list[0]->hash && + list[0]->hash == list[-1]->hash) { + list++; + sub_size--; + } + if (!sub_size) { + /* + * It is possible for some "paths" to have + * so many objects that no hash boundary + * might be found. Let's just steal the + * exact half in that case. + */ + sub_size = victim->remaining / 2; + list -= sub_size; + } + target->list = list; + victim->list_size -= sub_size; + victim->remaining -= sub_size; + } + target->list_size = sub_size; + target->remaining = sub_size; + target->working = 1; + git_packbuilder__progress_unlock(pb); + + if (git_mutex_lock(&target->mutex)) { + giterr_set(GITERR_THREAD, "unable to lock packfile condition mutex"); + git__free(p); + return -1; + } + + target->data_ready = 1; + git_cond_signal(&target->cond); + git_mutex_unlock(&target->mutex); + + if (!sub_size) { + git_thread_join(target->thread, NULL); + git_cond_free(&target->cond); + git_mutex_free(&target->mutex); + active_threads--; + } + } + + git__free(p); + return 0; +} + +#else +#define ll_find_deltas(pb, l, ls, w, d) find_deltas(pb, l, &ls, w, d) +#endif + +static int prepare_pack(git_packbuilder *pb) +{ + git_pobject **delta_list; + unsigned int i, n = 0; + + if (pb->nr_objects == 0 || pb->done) + return 0; /* nothing to do */ + + delta_list = git__malloc(pb->nr_objects * sizeof(*delta_list)); + GITERR_CHECK_ALLOC(delta_list); + + for (i = 0; i < pb->nr_objects; ++i) { + git_pobject *po = pb->object_list + i; + + /* Make sure the item is within our size limits */ + if (po->size < 50 || po->size > pb->big_file_threshold) + continue; + + delta_list[n++] = po; + } + + if (n > 1) { + git__tsort((void **)delta_list, n, type_size_sort); + if (ll_find_deltas(pb, delta_list, n, + GIT_PACK_WINDOW + 1, + GIT_PACK_DEPTH) < 0) { + git__free(delta_list); + return -1; + } + } + + pb->done = true; + git__free(delta_list); + return 0; +} + +#define PREPARE_PACK if (prepare_pack(pb) < 0) { return -1; } + +int git_packbuilder_foreach(git_packbuilder *pb, int (*cb)(void *buf, size_t size, void *payload), void *payload) +{ + PREPARE_PACK; + return write_pack(pb, cb, payload); +} + +int git_packbuilder_write_buf(git_buf *buf, git_packbuilder *pb) +{ + PREPARE_PACK; + return write_pack(pb, &write_pack_buf, buf); +} + +int git_packbuilder_write(git_packbuilder *pb, const char *path) +{ + PREPARE_PACK; + return write_pack_file(pb, path); +} + +#undef PREPARE_PACK + +static int cb_tree_walk(const char *root, const git_tree_entry *entry, void *payload) +{ + struct tree_walk_context *ctx = payload; + + /* A commit inside a tree represents a submodule commit and should be skipped. */ + if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) + return 0; + + if (git_buf_sets(&ctx->buf, root) < 0 || + git_buf_puts(&ctx->buf, git_tree_entry_name(entry)) < 0) + return -1; + + return git_packbuilder_insert(ctx->pb, + git_tree_entry_id(entry), + git_buf_cstr(&ctx->buf)); +} + +int git_packbuilder_insert_tree(git_packbuilder *pb, const git_oid *oid) +{ + git_tree *tree; + struct tree_walk_context context = { pb, GIT_BUF_INIT }; + + if (git_tree_lookup(&tree, pb->repo, oid) < 0 || + git_packbuilder_insert(pb, oid, NULL) < 0) + return -1; + + if (git_tree_walk(tree, GIT_TREEWALK_PRE, cb_tree_walk, &context) < 0) { + git_tree_free(tree); + git_buf_free(&context.buf); + return -1; + } + + git_tree_free(tree); + git_buf_free(&context.buf); + return 0; +} + +uint32_t git_packbuilder_object_count(git_packbuilder *pb) +{ + return pb->nr_objects; +} + +uint32_t git_packbuilder_written(git_packbuilder *pb) +{ + return pb->nr_written; +} + +void git_packbuilder_free(git_packbuilder *pb) +{ + if (pb == NULL) + return; + +#ifdef GIT_THREADS + + git_mutex_free(&pb->cache_mutex); + git_mutex_free(&pb->progress_mutex); + git_cond_free(&pb->progress_cond); + +#endif + + if (pb->odb) + git_odb_free(pb->odb); + + if (pb->object_ix) + git_oidmap_free(pb->object_ix); + + if (pb->object_list) + git__free(pb->object_list); + + git_hash_ctx_cleanup(&pb->ctx); + + git__free(pb); +} diff --git a/src/pack-objects.h b/src/pack-objects.h new file mode 100644 index 000000000..8e7ba7f78 --- /dev/null +++ b/src/pack-objects.h @@ -0,0 +1,87 @@ +/* + * 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_pack_objects_h__ +#define INCLUDE_pack_objects_h__ + +#include "common.h" + +#include "buffer.h" +#include "hash.h" +#include "oidmap.h" +#include "netops.h" + +#include "git2/oid.h" + +#define GIT_PACK_WINDOW 10 /* number of objects to possibly delta against */ +#define GIT_PACK_DEPTH 50 /* max delta depth */ +#define GIT_PACK_DELTA_CACHE_SIZE (256 * 1024 * 1024) +#define GIT_PACK_DELTA_CACHE_LIMIT 1000 +#define GIT_PACK_BIG_FILE_THRESHOLD (512 * 1024 * 1024) + +typedef struct git_pobject { + git_oid id; + git_otype type; + git_off_t offset; + + size_t size; + + unsigned int hash; /* name hint hash */ + + struct git_pobject *delta; /* delta base object */ + struct git_pobject *delta_child; /* deltified objects who bases me */ + struct git_pobject *delta_sibling; /* other deltified objects + * who uses the same base as + * me */ + + void *delta_data; + unsigned long delta_size; + unsigned long z_delta_size; + + int written:1, + recursing:1, + tagged:1, + filled:1; +} git_pobject; + +struct git_packbuilder { + git_repository *repo; /* associated repository */ + git_odb *odb; /* associated object database */ + + git_hash_ctx ctx; + + uint32_t nr_objects, + nr_alloc, + nr_written, + nr_remaining; + + git_pobject *object_list; + + git_oidmap *object_ix; + + git_oid pack_oid; /* hash of written pack */ + + /* synchronization objects */ + git_mutex cache_mutex; + git_mutex progress_mutex; + git_cond progress_cond; + + /* configs */ + uint64_t delta_cache_size; + uint64_t max_delta_cache_size; + uint64_t cache_max_small_delta_size; + uint64_t big_file_threshold; + uint64_t window_memory_limit; + + int nr_threads; /* nr of threads to use */ + + bool done; +}; + +int git_packbuilder_write_buf(git_buf *buf, git_packbuilder *pb); + +#endif /* INCLUDE_pack_objects_h__ */ diff --git a/src/pack.c b/src/pack.c index 0db1069de..75ac98186 100644 --- a/src/pack.c +++ b/src/pack.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -38,7 +38,7 @@ static int pack_entry_find_offset( git_oid *found_oid, struct git_pack_file *p, const git_oid *short_oid, - unsigned int len); + size_t len); static int packfile_error(const char *message) { @@ -46,6 +46,134 @@ static int packfile_error(const char *message) return -1; } +/******************** + * Delta base cache + ********************/ + +static git_pack_cache_entry *new_cache_object(git_rawobj *source) +{ + git_pack_cache_entry *e = git__calloc(1, sizeof(git_pack_cache_entry)); + if (!e) + return NULL; + + memcpy(&e->raw, source, sizeof(git_rawobj)); + + return e; +} + +static void free_cache_object(void *o) +{ + git_pack_cache_entry *e = (git_pack_cache_entry *)o; + + if (e != NULL) { + assert(e->refcount.val == 0); + git__free(e->raw.data); + git__free(e); + } +} + +static void cache_free(git_pack_cache *cache) +{ + khiter_t k; + + if (cache->entries) { + for (k = kh_begin(cache->entries); k != kh_end(cache->entries); k++) { + if (kh_exist(cache->entries, k)) + free_cache_object(kh_value(cache->entries, k)); + } + + git_offmap_free(cache->entries); + git_mutex_free(&cache->lock); + } +} + +static int cache_init(git_pack_cache *cache) +{ + memset(cache, 0, sizeof(git_pack_cache)); + cache->entries = git_offmap_alloc(); + GITERR_CHECK_ALLOC(cache->entries); + cache->memory_limit = GIT_PACK_CACHE_MEMORY_LIMIT; + git_mutex_init(&cache->lock); + + return 0; +} + +static git_pack_cache_entry *cache_get(git_pack_cache *cache, git_off_t offset) +{ + khiter_t k; + git_pack_cache_entry *entry = NULL; + + if (git_mutex_lock(&cache->lock) < 0) + return NULL; + + k = kh_get(off, cache->entries, offset); + if (k != kh_end(cache->entries)) { /* found it */ + entry = kh_value(cache->entries, k); + git_atomic_inc(&entry->refcount); + entry->last_usage = cache->use_ctr++; + } + git_mutex_unlock(&cache->lock); + + return entry; +} + +/* Run with the cache lock held */ +static void free_lowest_entry(git_pack_cache *cache) +{ + git_pack_cache_entry *entry; + khiter_t k; + + for (k = kh_begin(cache->entries); k != kh_end(cache->entries); k++) { + if (!kh_exist(cache->entries, k)) + continue; + + entry = kh_value(cache->entries, k); + + if (entry && entry->refcount.val == 0) { + cache->memory_used -= entry->raw.len; + kh_del(off, cache->entries, k); + free_cache_object(entry); + } + } +} + +static int cache_add(git_pack_cache *cache, git_rawobj *base, git_off_t offset) +{ + git_pack_cache_entry *entry; + int error, exists = 0; + khiter_t k; + + if (base->len > GIT_PACK_CACHE_SIZE_LIMIT) + return -1; + + entry = new_cache_object(base); + if (entry) { + if (git_mutex_lock(&cache->lock) < 0) { + giterr_set(GITERR_OS, "failed to lock cache"); + return -1; + } + /* Add it to the cache if nobody else has */ + exists = kh_get(off, cache->entries, offset) != kh_end(cache->entries); + if (!exists) { + while (cache->memory_used + base->len > cache->memory_limit) + free_lowest_entry(cache); + + k = kh_put(off, cache->entries, offset, &error); + assert(error != 0); + kh_value(cache->entries, k) = entry; + cache->memory_used += entry->raw.len; + } + git_mutex_unlock(&cache->lock); + /* Somebody beat us to adding it into the cache */ + if (exists) { + git__free(entry); + return -1; + } + } + + return 0; +} + /*********************************************************** * * PACK INDEX METHODS @@ -54,6 +182,10 @@ static int packfile_error(const char *message) static void pack_index_free(struct git_pack_file *p) { + if (p->oids) { + git__free(p->oids); + p->oids = NULL; + } if (p->index_map.data) { git_futils_mmap_free(&p->index_map); p->index_map.data = NULL; @@ -262,7 +394,7 @@ int git_packfile_unpack_header( 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; @@ -273,6 +405,56 @@ int git_packfile_unpack_header( return 0; } +int git_packfile_resolve_header( + size_t *size_p, + git_otype *type_p, + struct git_pack_file *p, + git_off_t offset) +{ + git_mwindow *w_curs = NULL; + git_off_t curpos = offset; + size_t size; + git_otype type; + git_off_t base_offset; + int error; + + error = git_packfile_unpack_header(&size, &type, &p->mwf, &w_curs, &curpos); + git_mwindow_close(&w_curs); + if (error < 0) + return error; + + if (type == GIT_OBJ_OFS_DELTA || type == GIT_OBJ_REF_DELTA) { + size_t base_size; + git_rawobj delta; + base_offset = get_delta_base(p, &w_curs, &curpos, type, offset); + git_mwindow_close(&w_curs); + error = packfile_unpack_compressed(&delta, p, &w_curs, &curpos, size, type); + git_mwindow_close(&w_curs); + if (error < 0) + return error; + error = git__delta_read_header(delta.data, delta.len, &base_size, size_p); + git__free(delta.data); + if (error < 0) + return error; + } else + *size_p = size; + + while (type == GIT_OBJ_OFS_DELTA || type == GIT_OBJ_REF_DELTA) { + curpos = base_offset; + error = git_packfile_unpack_header(&size, &type, &p->mwf, &w_curs, &curpos); + git_mwindow_close(&w_curs); + if (error < 0) + return error; + if (type != GIT_OBJ_OFS_DELTA && type != GIT_OBJ_REF_DELTA) + break; + base_offset = get_delta_base(p, &w_curs, &curpos, type, base_offset); + git_mwindow_close(&w_curs); + } + *type_p = type; + + return error; +} + static int packfile_unpack_delta( git_rawobj *obj, struct git_pack_file *p, @@ -282,9 +464,10 @@ static int packfile_unpack_delta( git_otype delta_type, git_off_t obj_offset) { - git_off_t base_offset; + git_off_t base_offset, base_key; git_rawobj base, delta; - int error; + git_pack_cache_entry *cached = NULL; + int error, found_base = 0; base_offset = get_delta_base(p, w_curs, curpos, delta_type, obj_offset); git_mwindow_close(w_curs); @@ -293,32 +476,49 @@ static int packfile_unpack_delta( if (base_offset < 0) /* must actually be an error code */ return (int)base_offset; - error = git_packfile_unpack(&base, p, &base_offset); + if (!p->bases.entries && (cache_init(&p->bases) < 0)) + return -1; - /* - * TODO: git.git tries to load the base from other packfiles - * or loose objects. - * - * We'll need to do this in order to support thin packs. - */ - if (error < 0) - return error; + base_key = base_offset; /* git_packfile_unpack modifies base_offset */ + if ((cached = cache_get(&p->bases, base_offset)) != NULL) { + memcpy(&base, &cached->raw, sizeof(git_rawobj)); + found_base = 1; + } + + if (!cached) { /* have to inflate it */ + error = git_packfile_unpack(&base, p, &base_offset); + + /* + * TODO: git.git tries to load the base from other packfiles + * or loose objects. + * + * We'll need to do this in order to support thin packs. + */ + if (error < 0) + return error; + } error = packfile_unpack_compressed(&delta, p, w_curs, curpos, delta_size, delta_type); git_mwindow_close(w_curs); + if (error < 0) { - git__free(base.data); + if (!found_base) + git__free(base.data); return error; } obj->type = base.type; error = git__delta_apply(obj, base.data, base.len, delta.data, delta.len); + if (error < 0) + goto on_error; - git__free(base.data); - git__free(delta.data); + if (found_base) + git_atomic_dec(&cached->refcount); + else if (cache_add(&p->bases, &base, base_key) < 0) + git__free(base.data); - /* TODO: we might want to cache this shit. eventually */ - //add_delta_base_cache(p, base_offset, base, base_size, *type); +on_error: + git__free(delta.data); return error; /* error set by git__delta_apply */ } @@ -387,6 +587,72 @@ static void use_git_free(void *opaq, void *ptr) git__free(ptr); } +int git_packfile_stream_open(git_packfile_stream *obj, struct git_pack_file *p, git_off_t curpos) +{ + int st; + + memset(obj, 0, sizeof(git_packfile_stream)); + obj->curpos = curpos; + obj->p = p; + obj->zstream.zalloc = use_git_alloc; + obj->zstream.zfree = use_git_free; + obj->zstream.next_in = Z_NULL; + obj->zstream.next_out = Z_NULL; + st = inflateInit(&obj->zstream); + if (st != Z_OK) { + git__free(obj); + giterr_set(GITERR_ZLIB, "Failed to inflate packfile"); + return -1; + } + + return 0; +} + +ssize_t git_packfile_stream_read(git_packfile_stream *obj, void *buffer, size_t len) +{ + unsigned char *in; + size_t written; + int st; + + if (obj->done) + return 0; + + in = pack_window_open(obj->p, &obj->mw, obj->curpos, &obj->zstream.avail_in); + if (in == NULL) + return GIT_EBUFS; + + obj->zstream.next_out = buffer; + obj->zstream.avail_out = (unsigned int)len; + obj->zstream.next_in = in; + + st = inflate(&obj->zstream, Z_SYNC_FLUSH); + git_mwindow_close(&obj->mw); + + obj->curpos += obj->zstream.next_in - in; + written = len - obj->zstream.avail_out; + + if (st != Z_OK && st != Z_STREAM_END) { + giterr_set(GITERR_ZLIB, "Failed to inflate packfile"); + return -1; + } + + if (st == Z_STREAM_END) + obj->done = 1; + + + /* If we didn't write anything out but we're not done, we need more data */ + if (!written && st != Z_STREAM_END) + return GIT_EBUFS; + + return written; + +} + +void git_packfile_stream_free(git_packfile_stream *obj) +{ + inflateEnd(&obj->zstream); +} + int packfile_unpack_compressed( git_rawobj *obj, struct git_pack_file *p, @@ -494,14 +760,14 @@ git_off_t get_delta_base( } else if (type == GIT_OBJ_REF_DELTA) { /* If we have the cooperative cache, search in it first */ if (p->has_cache) { - int pos; - struct git_pack_entry key; + khiter_t k; + git_oid oid; - git_oid_fromraw(&key.sha1, base_info); - pos = git_vector_bsearch(&p->cache, &key); - if (pos >= 0) { + git_oid_fromraw(&oid, base_info); + k = kh_get(oid, p->idx_cache, &oid); + if (k != kh_end(p->idx_cache)) { *curpos += 20; - return ((struct git_pack_entry *)git_vector_get(&p->cache, pos))->offset; + return ((struct git_pack_entry *)kh_value(p->idx_cache, k))->offset; } } /* The base entry _must_ be in the same pack */ @@ -529,12 +795,14 @@ static struct git_pack_file *packfile_alloc(size_t extra) } -void packfile_free(struct git_pack_file *p) +void git_packfile_free(struct git_pack_file *p) { assert(p); - /* clear_delta_base_cache(); */ + cache_free(&p->bases); + git_mwindow_free_all(&p->mwf); + git_mwindow_file_deregister(&p->mwf); if (p->mwf.fd != -1) p_close(p->mwf.fd); @@ -559,8 +827,10 @@ static int packfile_open(struct git_pack_file *p) /* TODO: open with noatime */ p->mwf.fd = git_futils_open_ro(p->pack_name); - if (p->mwf.fd < 0) - return p->mwf.fd; + if (p->mwf.fd < 0) { + p->mwf.fd = -1; + return -1; + } if (p_fstat(p->mwf.fd, &st) < 0 || git_mwindow_file_register(&p->mwf) < 0) @@ -685,12 +955,76 @@ static git_off_t nth_packed_object_offset(const struct git_pack_file *p, uint32_ } } +static int git__memcmp4(const void *a, const void *b) { + return memcmp(a, b, 4); +} + +int git_pack_foreach_entry( + struct git_pack_file *p, + git_odb_foreach_cb cb, + void *data) +{ + const unsigned char *index = p->index_map.data, *current; + uint32_t i; + + if (index == NULL) { + int error; + + if ((error = pack_index_open(p)) < 0) + return error; + + assert(p->index_map.data); + + index = p->index_map.data; + } + + if (p->index_version > 1) { + index += 8; + } + + index += 4 * 256; + + if (p->oids == NULL) { + git_vector offsets, oids; + int error; + + if ((error = git_vector_init(&oids, p->num_objects, NULL))) + return error; + + if ((error = git_vector_init(&offsets, p->num_objects, git__memcmp4))) + return error; + + if (p->index_version > 1) { + const unsigned char *off = index + 24 * p->num_objects; + for (i = 0; i < p->num_objects; i++) + git_vector_insert(&offsets, (void*)&off[4 * i]); + git_vector_sort(&offsets); + git_vector_foreach(&offsets, i, current) + git_vector_insert(&oids, (void*)&index[5 * (current - off)]); + } else { + for (i = 0; i < p->num_objects; i++) + git_vector_insert(&offsets, (void*)&index[24 * i]); + git_vector_sort(&offsets); + git_vector_foreach(&offsets, i, current) + git_vector_insert(&oids, (void*)¤t[4]); + } + git_vector_free(&offsets); + p->oids = (git_oid **)oids.contents; + } + + for (i = 0; i < p->num_objects; i++) + if (cb(p->oids[i], data)) + return GIT_EUSER; + + return 0; +} + static int pack_entry_find_offset( git_off_t *offset_out, git_oid *found_oid, struct git_pack_file *p, const git_oid *short_oid, - unsigned int len) + size_t len) { const uint32_t *level1_ofs = p->index_map.data; const unsigned char *index = p->index_map.data; @@ -783,7 +1117,7 @@ int git_pack_entry_find( struct git_pack_entry *e, struct git_pack_file *p, const git_oid *short_oid, - unsigned int len) + size_t len) { git_off_t offset; git_oid found_oid; diff --git a/src/pack.h b/src/pack.h index cd7a4d2e1..8d7e33dfe 100644 --- a/src/pack.h +++ b/src/pack.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -8,12 +8,15 @@ #ifndef INCLUDE_pack_h__ #define INCLUDE_pack_h__ +#include <zlib.h> + #include "git2/oid.h" #include "common.h" #include "map.h" #include "mwindow.h" #include "odb.h" +#include "oidmap.h" #define GIT_PACK_FILE_MODE 0444 @@ -51,6 +54,28 @@ struct git_pack_idx_header { uint32_t idx_version; }; +typedef struct git_pack_cache_entry { + size_t last_usage; /* enough? */ + git_atomic refcount; + git_rawobj raw; +} git_pack_cache_entry; + +#include "offmap.h" + +GIT__USE_OFFMAP; +GIT__USE_OIDMAP; + +#define GIT_PACK_CACHE_MEMORY_LIMIT 16 * 1024 * 1024 +#define GIT_PACK_CACHE_SIZE_LIMIT 1024 * 1024 /* don't bother caching anything over 1MB */ + +typedef struct { + size_t memory_used; + size_t memory_limit; + size_t use_ctr; + git_mutex lock; + git_offmap *entries; +} git_pack_cache; + struct git_pack_file { git_mwindow_file mwf; git_map index_map; @@ -63,7 +88,10 @@ struct git_pack_file { git_time_t mtime; unsigned pack_local:1, pack_keep:1, has_cache:1; git_oid sha1; - git_vector cache; + git_oidmap *idx_cache; + git_oid **oids; + + git_pack_cache bases; /* delta base cache */ /* something like ".git/objects/pack/xxxxx.pack" */ char pack_name[GIT_FLEX_ARRAY]; /* more */ @@ -75,6 +103,14 @@ struct git_pack_entry { struct git_pack_file *p; }; +typedef struct git_packfile_stream { + git_off_t curpos; + int done; + z_stream zstream; + struct git_pack_file *p; + git_mwindow *mw; +} git_packfile_stream; + int git_packfile_unpack_header( size_t *size_p, git_otype *type_p, @@ -82,6 +118,12 @@ int git_packfile_unpack_header( git_mwindow **w_curs, git_off_t *curpos); +int git_packfile_resolve_header( + size_t *size_p, + git_otype *type_p, + struct git_pack_file *p, + git_off_t offset); + int git_packfile_unpack(git_rawobj *obj, struct git_pack_file *p, git_off_t *obj_offset); int packfile_unpack_compressed( git_rawobj *obj, @@ -91,16 +133,24 @@ int packfile_unpack_compressed( size_t size, git_otype type); +int git_packfile_stream_open(git_packfile_stream *obj, struct git_pack_file *p, git_off_t curpos); +ssize_t git_packfile_stream_read(git_packfile_stream *obj, void *buffer, size_t len); +void git_packfile_stream_free(git_packfile_stream *obj); + git_off_t get_delta_base(struct git_pack_file *p, git_mwindow **w_curs, git_off_t *curpos, git_otype type, git_off_t delta_obj_offset); -void packfile_free(struct git_pack_file *p); +void git_packfile_free(struct git_pack_file *p); int git_packfile_check(struct git_pack_file **pack_out, const char *path); int git_pack_entry_find( struct git_pack_entry *e, struct git_pack_file *p, const git_oid *short_oid, - unsigned int len); + size_t len); +int git_pack_foreach_entry( + struct git_pack_file *p, + git_odb_foreach_cb cb, + void *data); #endif diff --git a/src/path.c b/src/path.c index 84edf6d89..6437979d5 100644 --- a/src/path.c +++ b/src/path.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -17,9 +17,55 @@ #include <stdio.h> #include <ctype.h> +#define LOOKS_LIKE_DRIVE_PREFIX(S) (git__isalpha((S)[0]) && (S)[1] == ':') + +#ifdef GIT_WIN32 +static bool looks_like_network_computer_name(const char *path, int pos) +{ + if (pos < 3) + return false; + + if (path[0] != '/' || path[1] != '/') + return false; + + while (pos-- > 2) { + if (path[pos] == '/') + return false; + } + + return true; +} +#endif + /* * Based on the Android implementation, BSD licensed. - * Check http://android.git.kernel.org/ + * http://android.git.kernel.org/ + * + * Copyright (C) 2008 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. */ int git_path_basename_r(git_buf *buffer, const char *path) { @@ -105,10 +151,19 @@ int git_path_dirname_r(git_buf *buffer, const char *path) /* Mimic unix behavior where '/.git' returns '/': 'C:/.git' will return 'C:/' here */ - if (len == 2 && isalpha(path[0]) && path[1] == ':') { + if (len == 2 && LOOKS_LIKE_DRIVE_PREFIX(path)) { len = 3; goto Exit; } + + /* Similarly checks if we're dealing with a network computer name + '//computername/.git' will return '//computername/' */ + + if (looks_like_network_computer_name(path, len)) { + len++; + goto Exit; + } + #endif Exit: @@ -145,6 +200,20 @@ char *git_path_basename(const char *path) return basename; } +size_t git_path_basename_offset(git_buf *buffer) +{ + ssize_t slash; + + if (!buffer || buffer->size <= 0) + return 0; + + slash = git_buf_rfind_next(buffer, '/'); + + if (slash >= 0 && buffer->ptr[slash] == '/') + return (size_t)(slash + 1); + + return 0; +} const char *git_path_topdir(const char *path) { @@ -168,11 +237,11 @@ int git_path_root(const char *path) { int offset = 0; -#ifdef GIT_WIN32 /* Does the root of the path look like a windows drive ? */ - if (isalpha(path[0]) && (path[1] == ':')) + if (LOOKS_LIKE_DRIVE_PREFIX(path)) offset += 2; +#ifdef GIT_WIN32 /* Are we dealing with a windows network path? */ else if ((path[0] == '/' && path[1] == '/') || (path[0] == '\\' && path[1] == '\\')) @@ -191,6 +260,31 @@ int git_path_root(const char *path) return -1; /* Not a real error - signals that path is not rooted */ } +int git_path_join_unrooted( + git_buf *path_out, const char *path, const char *base, ssize_t *root_at) +{ + int error, root; + + assert(path && path_out); + + root = git_path_root(path); + + if (base != NULL && root < 0) { + error = git_buf_joinpath(path_out, base, path); + + if (root_at) + *root_at = (ssize_t)strlen(base); + } + else { + error = git_buf_sets(path_out, path); + + if (root_at) + *root_at = (root < 0) ? 0 : (ssize_t)root; + } + + return error; +} + int git_path_prettify(git_buf *path_out, const char *path, const char *base) { char buf[GIT_PATH_MAX]; @@ -210,7 +304,7 @@ int git_path_prettify(git_buf *path_out, const char *path, const char *base) giterr_set(GITERR_OS, "Failed to resolve path '%s'", path); git_buf_clear(path_out); - + return error; } @@ -306,7 +400,7 @@ int git_path_fromurl(git_buf *local_path_out, const char *file_url) if (offset >= len || file_url[offset] == '/') return error_invalid_local_file_uri(file_url); -#ifndef _MSC_VER +#ifndef GIT_WIN32 offset--; /* A *nix absolute path starts with a forward slash */ #endif @@ -341,9 +435,10 @@ int git_path_walk_up( iter.asize = path->asize; while (scan >= stop) { - if ((error = cb(data, &iter)) < 0) - break; + error = cb(data, &iter); iter.ptr[scan] = oldc; + if (error < 0) + break; scan = git_buf_rfind_next(&iter, '/'); if (scan >= 0) { scan++; @@ -385,6 +480,68 @@ bool git_path_isfile(const char *path) return S_ISREG(st.st_mode) != 0; } +#ifdef GIT_WIN32 + +bool git_path_is_empty_dir(const char *path) +{ + git_buf pathbuf = GIT_BUF_INIT; + HANDLE hFind = INVALID_HANDLE_VALUE; + wchar_t wbuf[GIT_WIN_PATH]; + WIN32_FIND_DATAW ffd; + bool retval = true; + + if (!git_path_isdir(path)) return false; + + git_buf_printf(&pathbuf, "%s\\*", path); + git__utf8_to_16(wbuf, GIT_WIN_PATH, git_buf_cstr(&pathbuf)); + + hFind = FindFirstFileW(wbuf, &ffd); + if (INVALID_HANDLE_VALUE == hFind) { + giterr_set(GITERR_OS, "Couldn't open '%s'", path); + return false; + } + + do { + if (!git_path_is_dot_or_dotdotW(ffd.cFileName)) { + retval = false; + } + } while (FindNextFileW(hFind, &ffd) != 0); + + FindClose(hFind); + git_buf_free(&pathbuf); + return retval; +} + +#else + +bool git_path_is_empty_dir(const char *path) +{ + DIR *dir = NULL; + struct dirent *e; + bool retval = true; + + if (!git_path_isdir(path)) return false; + + dir = opendir(path); + if (!dir) { + giterr_set(GITERR_OS, "Couldn't open '%s'", path); + return false; + } + + while ((e = readdir(dir)) != NULL) { + if (!git_path_is_dot_or_dotdot(e->d_name)) { + giterr_set(GITERR_INVALID, + "'%s' exists and is not an empty directory", path); + retval = false; + break; + } + } + closedir(dir); + + return retval; +} +#endif + int git_path_lstat(const char *path, struct stat *st) { int err = 0; @@ -407,7 +564,7 @@ static bool _check_dir_contents( size_t sub_size = strlen(sub); /* leave base valid even if we could not make space for subdir */ - if (git_buf_try_grow(dir, dir_size + sub_size + 2) < 0) + if (git_buf_try_grow(dir, dir_size + sub_size + 2, false) < 0) return false; /* save excursion */ @@ -437,12 +594,7 @@ bool git_path_contains_file(git_buf *base, const char *file) int git_path_find_dir(git_buf *dir, const char *path, const char *base) { - int error; - - if (base != NULL && git_path_root(path) < 0) - error = git_buf_joinpath(dir, base, path); - else - error = git_buf_sets(dir, path); + int error = git_path_join_unrooted(dir, path, base, NULL); if (!error) { char buf[GIT_PATH_MAX]; @@ -460,31 +612,94 @@ int git_path_find_dir(git_buf *dir, const char *path, const char *base) return error; } +int git_path_resolve_relative(git_buf *path, size_t ceiling) +{ + char *base, *to, *from, *next; + size_t len; + + if (!path || git_buf_oom(path)) + return -1; + + if (ceiling > path->size) + ceiling = path->size; + + /* recognize drive prefixes, etc. that should not be backed over */ + if (ceiling == 0) + ceiling = git_path_root(path->ptr) + 1; + + /* recognize URL prefixes that should not be backed over */ + if (ceiling == 0) { + for (next = path->ptr; *next && git__isalpha(*next); ++next); + if (next[0] == ':' && next[1] == '/' && next[2] == '/') + ceiling = (next + 3) - path->ptr; + } + + base = to = from = path->ptr + ceiling; + + while (*from) { + for (next = from; *next && *next != '/'; ++next); + + len = next - from; + + if (len == 1 && from[0] == '.') + /* do nothing with singleton dot */; + + else if (len == 2 && from[0] == '.' && from[1] == '.') { + while (to > base && to[-1] == '/') to--; + while (to > base && to[-1] != '/') to--; + } + + else { + if (*next == '/') + len++; + + if (to != from) + memmove(to, from, len); + + to += len; + } + + from += len; + + while (*from == '/') from++; + } + + *to = '\0'; + + path->size = to - path->ptr; + + return 0; +} + +int git_path_apply_relative(git_buf *target, const char *relpath) +{ + git_buf_joinpath(target, git_buf_cstr(target), relpath); + return git_path_resolve_relative(target, 0); +} + int git_path_cmp( const char *name1, size_t len1, int isdir1, - const char *name2, size_t len2, int isdir2) + const char *name2, size_t len2, int isdir2, + int (*compare)(const char *, const char *, size_t)) { + unsigned char c1, c2; size_t len = len1 < len2 ? len1 : len2; int cmp; - cmp = memcmp(name1, name2, len); + cmp = compare(name1, name2, len); if (cmp) return cmp; - if (len1 < len2) - return (!isdir1 && !isdir2) ? -1 : - (isdir1 ? '/' - name2[len1] : name2[len1] - '/'); - if (len1 > len2) - return (!isdir1 && !isdir2) ? 1 : - (isdir2 ? name1[len2] - '/' : '/' - name1[len2]); - return 0; -} -/* Taken from git.git */ -GIT_INLINE(int) is_dot_or_dotdot(const char *name) -{ - return (name[0] == '.' && - (name[1] == '\0' || - (name[1] == '.' && name[2] == '\0'))); + c1 = name1[len]; + c2 = name2[len]; + + if (c1 == '\0' && isdir1) + c1 = '/'; + + if (c2 == '\0' && isdir2) + c2 = '/'; + + return (c1 < c2) ? -1 : (c1 > c2) ? 1 : 0; } int git_path_direach( @@ -506,7 +721,7 @@ int git_path_direach( return -1; } -#ifdef __sun +#if defined(__sun) || defined(__GNU__) de_buf = git__malloc(sizeof(struct dirent) + FILENAME_MAX + 1); #else de_buf = git__malloc(sizeof(struct dirent)); @@ -515,7 +730,7 @@ int git_path_direach( while (p_readdir_r(dir, de_buf, &de) == 0 && de != NULL) { int result; - if (is_dot_or_dotdot(de->d_name)) + if (git_path_is_dot_or_dotdot(de->d_name)) continue; if (git_buf_puts(path, de->d_name) < 0) { @@ -560,7 +775,7 @@ int git_path_dirload( return -1; } -#ifdef __sun +#if defined(__sun) || defined(__GNU__) de_buf = git__malloc(sizeof(struct dirent) + FILENAME_MAX + 1); #else de_buf = git__malloc(sizeof(struct dirent)); @@ -574,7 +789,7 @@ int git_path_dirload( char *entry_path; size_t entry_len; - if (is_dot_or_dotdot(de->d_name)) + if (git_path_is_dot_or_dotdot(de->d_name)) continue; entry_len = strlen(de->d_name); @@ -609,18 +824,30 @@ int git_path_dirload( int git_path_with_stat_cmp(const void *a, const void *b) { const git_path_with_stat *psa = a, *psb = b; - return git__strcmp_cb(psa->path, psb->path); + return strcmp(psa->path, psb->path); +} + +int git_path_with_stat_cmp_icase(const void *a, const void *b) +{ + const git_path_with_stat *psa = a, *psb = b; + return strcasecmp(psa->path, psb->path); } int git_path_dirload_with_stat( const char *path, size_t prefix_len, + bool ignore_case, + const char *start_stat, + const char *end_stat, git_vector *contents) { int error; unsigned int i; git_path_with_stat *ps; git_buf full = GIT_BUF_INIT; + int (*strncomp)(const char *a, const char *b, size_t sz); + size_t start_len = start_stat ? strlen(start_stat) : 0; + size_t end_len = end_stat ? strlen(end_stat) : 0, cmp_len; if (git_buf_set(&full, path, prefix_len) < 0) return -1; @@ -632,24 +859,46 @@ int git_path_dirload_with_stat( return error; } + strncomp = ignore_case ? git__strncasecmp : git__strncmp; + + /* stat struct at start of git_path_with_stat, so shift path text */ git_vector_foreach(contents, i, ps) { size_t path_len = strlen((char *)ps); - memmove(ps->path, ps, path_len + 1); ps->path_len = path_len; + } + + git_vector_foreach(contents, i, ps) { + /* skip if before start_stat or after end_stat */ + cmp_len = min(start_len, ps->path_len); + if (cmp_len && strncomp(ps->path, start_stat, cmp_len) < 0) + continue; + cmp_len = min(end_len, ps->path_len); + if (cmp_len && strncomp(ps->path, end_stat, cmp_len) > 0) + continue; + + git_buf_truncate(&full, prefix_len); if ((error = git_buf_joinpath(&full, full.ptr, ps->path)) < 0 || (error = git_path_lstat(full.ptr, &ps->st)) < 0) break; - git_buf_truncate(&full, prefix_len); - if (S_ISDIR(ps->st.st_mode)) { - ps->path[path_len] = '/'; - ps->path[path_len + 1] = '\0'; + if ((error = git_buf_joinpath(&full, full.ptr, ".git")) < 0) + break; + + if (p_access(full.ptr, F_OK) == 0) { + ps->st.st_mode = GIT_FILEMODE_COMMIT; + } else { + ps->path[ps->path_len++] = '/'; + ps->path[ps->path_len] = '\0'; + } } } + /* sort now that directory suffix is added */ + git_vector_sort(contents); + git_buf_free(&full); return error; diff --git a/src/path.h b/src/path.h index fd76805e5..ead4fa338 100644 --- a/src/path.h +++ b/src/path.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -58,6 +58,11 @@ extern int git_path_dirname_r(git_buf *buffer, const char *path); extern char *git_path_basename(const char *path); extern int git_path_basename_r(git_buf *buffer, const char *path); +/* Return the offset of the start of the basename. Unlike the other + * basename functions, this returns 0 if the path is empty. + */ +extern size_t git_path_basename_offset(git_buf *buffer); + extern const char *git_path_topdir(const char *path); /** @@ -80,7 +85,24 @@ extern int git_path_to_dir(git_buf *path); */ extern void git_path_string_to_dir(char* path, size_t size); +/** + * Taken from git.git; returns nonzero if the given path is "." or "..". + */ +GIT_INLINE(int) git_path_is_dot_or_dotdot(const char *name) +{ + return (name[0] == '.' && + (name[1] == '\0' || + (name[1] == '.' && name[2] == '\0'))); +} + #ifdef GIT_WIN32 +GIT_INLINE(int) git_path_is_dot_or_dotdotW(const wchar_t *name) +{ + return (name[0] == L'.' && + (name[1] == L'\0' || + (name[1] == L'.' && name[2] == L'\0'))); +} + /** * Convert backslashes in path to forward slashes. */ @@ -130,6 +152,11 @@ extern bool git_path_isdir(const char *path); extern bool git_path_isfile(const char *path); /** + * Check if the given path is a directory, and is empty. + */ +extern bool git_path_is_empty_dir(const char *path); + +/** * Stat a file and/or link and set error if needed. */ extern int git_path_lstat(const char *path, struct stat *st); @@ -164,6 +191,15 @@ extern bool git_path_contains_dir(git_buf *parent, const char *subdir); extern bool git_path_contains_file(git_buf *dir, const char *file); /** + * Prepend base to unrooted path or just copy path over. + * + * This will optionally return the index into the path where the "root" + * is, either the end of the base directory prefix or the path root. + */ +extern int git_path_join_unrooted( + git_buf *path_out, const char *path, const char *base, ssize_t *root_at); + +/** * Clean up path, prepending base if it is not already rooted. */ extern int git_path_prettify(git_buf *path_out, const char *path, const char *base); @@ -186,6 +222,29 @@ extern int git_path_prettify_dir(git_buf *path_out, const char *path, const char extern int git_path_find_dir(git_buf *dir, const char *path, const char *base); /** + * Resolve relative references within a path. + * + * This eliminates "./" and "../" relative references inside a path, + * as well as condensing multiple slashes into single ones. It will + * not touch the path before the "ceiling" length. + * + * Additionally, this will recognize an "c:/" drive prefix or a "xyz://" URL + * prefix and not touch that part of the path. + */ +extern int git_path_resolve_relative(git_buf *path, size_t ceiling); + +/** + * Apply a relative path to base path. + * + * Note that the base path could be a filename or a URL and this + * should still work. The relative path is walked segment by segment + * with three rules: series of slashes will be condensed to a single + * slash, "." will be eaten with no change, and ".." will remove a + * segment from the base path. + */ +extern int git_path_apply_relative(git_buf *target, const char *relpath); + +/** * Walk each directory entry, except '.' and '..', calling fn(state). * * @param pathbuf buffer the function reads the initial directory @@ -194,6 +253,7 @@ extern int git_path_find_dir(git_buf *dir, const char *path, const char *base); * the input state and the second arg is pathbuf. The function * may modify the pathbuf, but only by appending new text. * @param state to pass to fn as the first arg. + * @return 0 on success, GIT_EUSER on non-zero callback, or error code */ extern int git_path_direach( git_buf *pathbuf, @@ -201,11 +261,12 @@ extern int git_path_direach( void *state); /** - * Sort function to order two paths. + * Sort function to order two paths */ extern int git_path_cmp( const char *name1, size_t len1, int isdir1, - const char *name2, size_t len2, int isdir2); + const char *name2, size_t len2, int isdir2, + int (*compare)(const char *, const char *, size_t)); /** * Invoke callback up path directory by directory until the ceiling is @@ -261,18 +322,33 @@ typedef struct { } git_path_with_stat; extern int git_path_with_stat_cmp(const void *a, const void *b); +extern int git_path_with_stat_cmp_icase(const void *a, const void *b); /** * Load all directory entries along with stat info into a vector. * - * This is just like git_path_dirload except that each entry in the - * vector is a git_path_with_stat structure that contains both the - * path and the stat info, plus directories will have a / suffixed - * to their path name. + * This adds four things on top of plain `git_path_dirload`: + * + * 1. Each entry in the vector is a `git_path_with_stat` struct that + * contains both the path and the stat info + * 2. The entries will be sorted alphabetically + * 3. Entries that are directories will be suffixed with a '/' + * 4. Optionally, you can be a start and end prefix and only elements + * after the start and before the end (inclusively) will be stat'ed. + * + * @param path The directory to read from + * @param prefix_len The trailing part of path to prefix to entry paths + * @param ignore_case How to sort and compare paths with start/end limits + * @param start_stat As optimization, only stat values after this prefix + * @param end_stat As optimization, only stat values before this prefix + * @param contents Vector to fill with git_path_with_stat structures */ extern int git_path_dirload_with_stat( const char *path, size_t prefix_len, + bool ignore_case, + const char *start_stat, + const char *end_stat, git_vector *contents); #endif diff --git a/src/pathspec.c b/src/pathspec.c new file mode 100644 index 000000000..d4eb12582 --- /dev/null +++ b/src/pathspec.c @@ -0,0 +1,168 @@ +/* + * 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 "pathspec.h" +#include "buf_text.h" +#include "attr_file.h" + +/* what is the common non-wildcard prefix for all items in the pathspec */ +char *git_pathspec_prefix(const git_strarray *pathspec) +{ + git_buf prefix = GIT_BUF_INIT; + const char *scan; + + if (!pathspec || !pathspec->count || + git_buf_text_common_prefix(&prefix, pathspec) < 0) + return NULL; + + /* diff prefix will only be leading non-wildcards */ + for (scan = prefix.ptr; *scan; ++scan) { + if (git__iswildcard(*scan) && + (scan == prefix.ptr || (*(scan - 1) != '\\'))) + break; + } + git_buf_truncate(&prefix, scan - prefix.ptr); + + if (prefix.size <= 0) { + git_buf_free(&prefix); + return NULL; + } + + git_buf_text_unescape(&prefix); + + return git_buf_detach(&prefix); +} + +/* is there anything in the spec that needs to be filtered on */ +bool git_pathspec_is_empty(const git_strarray *pathspec) +{ + size_t i; + + if (pathspec == NULL) + return true; + + for (i = 0; i < pathspec->count; ++i) { + const char *str = pathspec->strings[i]; + + if (str && str[0]) + return false; + } + + return true; +} + +/* build a vector of fnmatch patterns to evaluate efficiently */ +int git_pathspec_init( + git_vector *vspec, const git_strarray *strspec, git_pool *strpool) +{ + size_t i; + + memset(vspec, 0, sizeof(*vspec)); + + if (git_pathspec_is_empty(strspec)) + return 0; + + if (git_vector_init(vspec, strspec->count, NULL) < 0) + return -1; + + for (i = 0; i < strspec->count; ++i) { + int ret; + const char *pattern = strspec->strings[i]; + git_attr_fnmatch *match = git__calloc(1, sizeof(git_attr_fnmatch)); + if (!match) + return -1; + + match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE; + + ret = git_attr_fnmatch__parse(match, strpool, NULL, &pattern); + if (ret == GIT_ENOTFOUND) { + git__free(match); + continue; + } else if (ret < 0) + return ret; + + if (git_vector_insert(vspec, match) < 0) + return -1; + } + + return 0; +} + +/* free data from the pathspec vector */ +void git_pathspec_free(git_vector *vspec) +{ + git_attr_fnmatch *match; + unsigned int i; + + git_vector_foreach(vspec, i, match) { + git__free(match); + vspec->contents[i] = NULL; + } + + git_vector_free(vspec); +} + +/* match a path against the vectorized pathspec */ +bool git_pathspec_match_path( + git_vector *vspec, + const char *path, + bool disable_fnmatch, + bool casefold, + const char **matched_pathspec) +{ + 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); + + if (matched_pathspec) + *matched_pathspec = NULL; + + if (!vspec || !vspec->length) + return true; + + if (disable_fnmatch) + fnmatch_flags = -1; + else if (casefold) + fnmatch_flags = FNM_CASEFOLD; + + if (casefold) { + use_strcmp = git__strcasecmp; + use_strncmp = git__strncasecmp; + } else { + use_strcmp = git__strcmp; + use_strncmp = git__strncmp; + } + + 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 false; +} + diff --git a/src/pathspec.h b/src/pathspec.h new file mode 100644 index 000000000..43a94baad --- /dev/null +++ b/src/pathspec.h @@ -0,0 +1,40 @@ +/* + * 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_pathspec_h__ +#define INCLUDE_pathspec_h__ + +#include "common.h" +#include "buffer.h" +#include "vector.h" +#include "pool.h" + +/* what is the common non-wildcard prefix for all items in the pathspec */ +extern char *git_pathspec_prefix(const git_strarray *pathspec); + +/* is there anything in the spec that needs to be filtered on */ +extern bool git_pathspec_is_empty(const git_strarray *pathspec); + +/* build a vector of fnmatch patterns to evaluate efficiently */ +extern int git_pathspec_init( + git_vector *vspec, const git_strarray *strspec, git_pool *strpool); + +/* free data from the pathspec vector */ +extern void git_pathspec_free(git_vector *vspec); + +/* + * 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, + const char *path, + bool disable_fnmatch, + bool casefold, + const char **matched_pathspec); + +#endif diff --git a/src/pkt.h b/src/pkt.h deleted file mode 100644 index 75442c833..000000000 --- a/src/pkt.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2009-2012 the libgit2 contributors - * - * This file is part of libgit2, distributed under the GNU GPL v2 with - * a Linking Exception. For full terms see the included COPYING file. - */ - -#ifndef INCLUDE_pkt_h__ -#define INCLUDE_pkt_h__ - -#include "common.h" -#include "transport.h" -#include "buffer.h" -#include "posix.h" -#include "git2/net.h" - -enum git_pkt_type { - GIT_PKT_CMD, - GIT_PKT_FLUSH, - GIT_PKT_REF, - GIT_PKT_HAVE, - GIT_PKT_ACK, - GIT_PKT_NAK, - GIT_PKT_PACK, - GIT_PKT_COMMENT, - GIT_PKT_ERR, -}; - -/* Used for multi-ack */ -enum git_ack_status { - GIT_ACK_NONE, - GIT_ACK_CONTINUE, - GIT_ACK_COMMON, - GIT_ACK_READY -}; - -/* This would be a flush pkt */ -typedef struct { - enum git_pkt_type type; -} git_pkt; - -struct git_pkt_cmd { - enum git_pkt_type type; - char *cmd; - char *path; - char *host; -}; - -/* This is a pkt-line with some info in it */ -typedef struct { - enum git_pkt_type type; - git_remote_head head; - char *capabilities; -} git_pkt_ref; - -/* Useful later */ -typedef struct { - enum git_pkt_type type; - git_oid oid; - enum git_ack_status status; -} git_pkt_ack; - -typedef struct { - enum git_pkt_type type; - char comment[GIT_FLEX_ARRAY]; -} git_pkt_comment; - -typedef struct { - enum git_pkt_type type; - char error[GIT_FLEX_ARRAY]; -} git_pkt_err; - -int git_pkt_parse_line(git_pkt **head, const char *line, const char **out, size_t len); -int git_pkt_buffer_flush(git_buf *buf); -int git_pkt_send_flush(GIT_SOCKET s); -int git_pkt_buffer_done(git_buf *buf); -int git_pkt_buffer_wants(const git_vector *refs, git_transport_caps *caps, git_buf *buf); -int git_pkt_buffer_have(git_oid *oid, git_buf *buf); -void git_pkt_free(git_pkt *pkt); - -#endif diff --git a/src/pool.c b/src/pool.c index 641292d06..b3cd49665 100644 --- a/src/pool.c +++ b/src/pool.c @@ -10,6 +10,10 @@ struct git_pool_page { char data[GIT_FLEX_ARRAY]; }; +struct pool_freelist { + struct pool_freelist *next; +}; + #define GIT_POOL_MIN_USABLE 4 #define GIT_POOL_MIN_PAGESZ 2 * sizeof(void*) @@ -150,7 +154,7 @@ void *git_pool_malloc(git_pool *pool, uint32_t items) pool->has_multi_item_alloc = 1; else if (pool->free_list != NULL) { ptr = pool->free_list; - pool->free_list = *((void **)pool->free_list); + pool->free_list = ((struct pool_freelist *)pool->free_list)->next; return ptr; } @@ -206,6 +210,11 @@ char *git_pool_strdup(git_pool *pool, const char *str) return git_pool_strndup(pool, str, strlen(str)); } +char *git_pool_strdup_safe(git_pool *pool, const char *str) +{ + return str ? git_pool_strdup(pool, str) : NULL; +} + char *git_pool_strcat(git_pool *pool, const char *a, const char *b) { void *ptr; @@ -230,10 +239,31 @@ char *git_pool_strcat(git_pool *pool, const char *a, const char *b) void git_pool_free(git_pool *pool, void *ptr) { - assert(pool && ptr && pool->item_size >= sizeof(void*)); + struct pool_freelist *item = ptr; + + assert(pool && pool->item_size >= sizeof(void*)); + + if (item) { + item->next = pool->free_list; + pool->free_list = item; + } +} + +void git_pool_free_array(git_pool *pool, size_t count, void **ptrs) +{ + struct pool_freelist **items = (struct pool_freelist **)ptrs; + size_t i; + + assert(pool && ptrs && pool->item_size >= sizeof(void*)); + + if (!count) + return; + + for (i = count - 1; i > 0; --i) + items[i]->next = items[i - 1]; - *((void **)ptr) = pool->free_list; - pool->free_list = ptr; + items[i]->next = pool->free_list; + pool->free_list = items[count - 1]; } uint32_t git_pool__open_pages(git_pool *pool) @@ -275,6 +305,8 @@ uint32_t git_pool__system_page_size(void) SYSTEM_INFO info; GetSystemInfo(&info); size = (uint32_t)info.dwPageSize; +#elif defined(__amigaos4__) + size = (uint32_t)4096; /* 4K as there is no global value we can query */ #else size = (uint32_t)sysconf(_SC_PAGE_SIZE); #endif diff --git a/src/pool.h b/src/pool.h index 54a2861ed..5ac9b764f 100644 --- a/src/pool.h +++ b/src/pool.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -76,6 +76,17 @@ extern void git_pool_swap(git_pool *a, git_pool *b); extern void *git_pool_malloc(git_pool *pool, uint32_t items); /** + * Allocate space and zero it out. + */ +GIT_INLINE(void *) git_pool_mallocz(git_pool *pool, uint32_t items) +{ + void *ptr = git_pool_malloc(pool, items); + if (ptr) + memset(ptr, 0, (size_t)items * (size_t)pool->item_size); + return ptr; +} + +/** * Allocate space and duplicate string data into it. * * This is allowed only for pools with item_size == sizeof(char) @@ -90,6 +101,13 @@ extern char *git_pool_strndup(git_pool *pool, const char *str, size_t n); extern char *git_pool_strdup(git_pool *pool, const char *str); /** + * Allocate space and duplicate a string into it, NULL is no error. + * + * This is allowed only for pools with item_size == sizeof(char) + */ +extern char *git_pool_strdup_safe(git_pool *pool, const char *str); + +/** * Allocate space for the concatenation of two strings. * * This is allowed only for pools with item_size == sizeof(char) @@ -108,6 +126,13 @@ extern char *git_pool_strcat(git_pool *pool, const char *a, const char *b); */ extern void git_pool_free(git_pool *pool, void *ptr); +/** + * Push an array of pool allocated blocks efficiently onto the free list. + * + * This has the same constraints as `git_pool_free()` above. + */ +extern void git_pool_free_array(git_pool *pool, size_t count, void **ptrs); + /* * Misc utilities */ diff --git a/src/posix.c b/src/posix.c index a9a6af984..5d526d33c 100644 --- a/src/posix.c +++ b/src/posix.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -12,12 +12,98 @@ #ifndef GIT_WIN32 +#ifdef NO_ADDRINFO + +int p_getaddrinfo( + const char *host, + const char *port, + struct addrinfo *hints, + struct addrinfo **info) +{ + struct addrinfo *ainfo, *ai; + int p = 0; + + GIT_UNUSED(hints); + + if ((ainfo = malloc(sizeof(struct addrinfo))) == NULL) + return -1; + + if ((ainfo->ai_hostent = gethostbyname(host)) == NULL) { + free(ainfo); + return -2; + } + + ainfo->ai_servent = getservbyname(port, 0); + + if (ainfo->ai_servent) + ainfo->ai_port = ainfo->ai_servent->s_port; + else + ainfo->ai_port = atol(port); + + memcpy(&ainfo->ai_addr_in.sin_addr, + ainfo->ai_hostent->h_addr_list[0], + ainfo->ai_hostent->h_length); + + ainfo->ai_protocol = 0; + ainfo->ai_socktype = hints->ai_socktype; + ainfo->ai_family = ainfo->ai_hostent->h_addrtype; + ainfo->ai_addr_in.sin_family = ainfo->ai_family; + ainfo->ai_addr_in.sin_port = ainfo->ai_port; + ainfo->ai_addr = (struct addrinfo *)&ainfo->ai_addr_in; + ainfo->ai_addrlen = sizeof(struct sockaddr_in); + + *info = ainfo; + + if (ainfo->ai_hostent->h_addr_list[1] == NULL) { + ainfo->ai_next = NULL; + return 0; + } + + ai = ainfo; + + for (p = 1; ainfo->ai_hostent->h_addr_list[p] != NULL; p++) { + ai->ai_next = malloc(sizeof(struct addrinfo)); + memcpy(&ai->ai_next, ainfo, sizeof(struct addrinfo)); + memcpy(&ai->ai_next->ai_addr_in.sin_addr, + ainfo->ai_hostent->h_addr_list[p], + ainfo->ai_hostent->h_length); + ai->ai_next->ai_addr = (struct addrinfo *)&ai->ai_next->ai_addr_in; + ai = ai->ai_next; + } + + ai->ai_next = NULL; + return 0; +} + +void p_freeaddrinfo(struct addrinfo *info) +{ + struct addrinfo *p, *next; + + p = info; + + while(p != NULL) { + next = p->ai_next; + free(p); + p = next; + } +} + +const char *p_gai_strerror(int ret) +{ + switch(ret) { + case -1: return "Out of memory"; break; + case -2: return "Address lookup failed"; break; + default: return "Unknown error"; break; + } +} + +#endif /* NO_ADDRINFO */ + int p_open(const char *path, int flags, ...) { mode_t mode = 0; - if (flags & O_CREAT) - { + if (flags & O_CREAT) { va_list arg_list; va_start(arg_list, flags); @@ -63,11 +149,12 @@ int p_rename(const char *from, const char *to) return -1; } -#endif +#endif /* GIT_WIN32 */ int p_read(git_file fd, void *buf, size_t cnt) { char *b = buf; + while (cnt) { ssize_t r; #ifdef GIT_WIN32 @@ -92,6 +179,7 @@ int p_read(git_file fd, void *buf, size_t cnt) int p_write(git_file fd, const void *buf, size_t cnt) { const char *b = buf; + while (cnt) { ssize_t r; #ifdef GIT_WIN32 @@ -114,3 +202,5 @@ int p_write(git_file fd, const void *buf, size_t cnt) } return 0; } + + diff --git a/src/posix.h b/src/posix.h index d020d94ac..719c8a04c 100644 --- a/src/posix.h +++ b/src/posix.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -10,9 +10,17 @@ #include "common.h" #include <fcntl.h> #include <time.h> +#include "fnmatch.h" +#ifndef S_IFGITLINK #define S_IFGITLINK 0160000 #define S_ISGITLINK(m) (((m) & S_IFMT) == S_IFGITLINK) +#endif + +/* if S_ISGID is not defined, then don't try to set it */ +#ifndef S_ISGID +#define S_ISGID 0 +#endif #if !defined(O_BINARY) #define O_BINARY 0 @@ -24,14 +32,13 @@ typedef int git_file; * Standard POSIX Methods * * All the methods starting with the `p_` prefix are - * direct ports of the standard POSIX methods. + * direct ports of the standard POSIX methods. * * Some of the methods are slightly wrapped to provide * saner defaults. Some of these methods are emulated * in Windows platforns. * * Use your manpages to check the docs on these. - * Straightforward */ extern int p_read(git_file fd, void *buf, size_t cnt); @@ -59,9 +66,18 @@ extern int p_rename(const char *from, const char *to); typedef int GIT_SOCKET; #define INVALID_SOCKET -1 +#define p_localtime_r localtime_r +#define p_gmtime_r gmtime_r +#define p_gettimeofday gettimeofday + #else typedef SOCKET GIT_SOCKET; +struct timezone; +extern struct tm * p_localtime_r (const time_t *timer, struct tm *result); +extern struct tm * p_gmtime_r (const time_t *timer, struct tm *result); +extern int p_gettimeofday(struct timeval *tv, struct timezone *tz); + #endif @@ -74,6 +90,41 @@ typedef SOCKET GIT_SOCKET; # include "unix/posix.h" #endif -#define p_readdir_r(d,e,r) readdir_r(d,e,r) +#ifdef NO_READDIR_R +# include <dirent.h> +GIT_INLINE(int) p_readdir_r(DIR *dirp, struct dirent *entry, struct dirent **result) +{ + GIT_UNUSED(entry); + *result = readdir(dirp); + return 0; +} +#else /* NO_READDIR_R */ +# define p_readdir_r(d,e,r) readdir_r(d,e,r) +#endif + +#ifdef NO_ADDRINFO +# include <netdb.h> +struct addrinfo { + struct hostent *ai_hostent; + struct servent *ai_servent; + struct sockaddr_in ai_addr_in; + struct sockaddr *ai_addr; + size_t ai_addrlen; + int ai_family; + int ai_socktype; + int ai_protocol; + long ai_port; + struct addrinfo *ai_next; +}; + +extern int p_getaddrinfo(const char *host, const char *port, + struct addrinfo *hints, struct addrinfo **info); +extern void p_freeaddrinfo(struct addrinfo *info); +extern const char *p_gai_strerror(int ret); +#else +# define p_getaddrinfo(a, b, c, d) getaddrinfo(a, b, c, d) +# define p_freeaddrinfo(a) freeaddrinfo(a) +# define p_gai_strerror(c) gai_strerror(c) +#endif /* NO_ADDRINFO */ #endif diff --git a/src/ppc/sha1.c b/src/ppc/sha1.c deleted file mode 100644 index 803b81d0a..000000000 --- a/src/ppc/sha1.c +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2009-2012 the libgit2 contributors - * - * This file is part of libgit2, distributed under the GNU GPL v2 with - * a Linking Exception. For full terms see the included COPYING file. - */ -#include <stdio.h> -#include <string.h> -#include "sha1.h" - -extern void ppc_sha1_core(uint32_t *hash, const unsigned char *p, - unsigned int nblocks); - -int ppc_SHA1_Init(ppc_SHA_CTX *c) -{ - c->hash[0] = 0x67452301; - c->hash[1] = 0xEFCDAB89; - c->hash[2] = 0x98BADCFE; - c->hash[3] = 0x10325476; - c->hash[4] = 0xC3D2E1F0; - c->len = 0; - c->cnt = 0; - return 0; -} - -int ppc_SHA1_Update(ppc_SHA_CTX *c, const void *ptr, unsigned long n) -{ - unsigned long nb; - const unsigned char *p = ptr; - - c->len += (uint64_t) n << 3; - while (n != 0) { - if (c->cnt || n < 64) { - nb = 64 - c->cnt; - if (nb > n) - nb = n; - memcpy(&c->buf.b[c->cnt], p, nb); - if ((c->cnt += nb) == 64) { - ppc_sha1_core(c->hash, c->buf.b, 1); - c->cnt = 0; - } - } else { - nb = n >> 6; - ppc_sha1_core(c->hash, p, nb); - nb <<= 6; - } - n -= nb; - p += nb; - } - return 0; -} - -int ppc_SHA1_Final(unsigned char *hash, ppc_SHA_CTX *c) -{ - unsigned int cnt = c->cnt; - - c->buf.b[cnt++] = 0x80; - if (cnt > 56) { - if (cnt < 64) - memset(&c->buf.b[cnt], 0, 64 - cnt); - ppc_sha1_core(c->hash, c->buf.b, 1); - cnt = 0; - } - if (cnt < 56) - memset(&c->buf.b[cnt], 0, 56 - cnt); - c->buf.l[7] = c->len; - ppc_sha1_core(c->hash, c->buf.b, 1); - memcpy(hash, c->hash, 20); - return 0; -} diff --git a/src/ppc/sha1.h b/src/ppc/sha1.h deleted file mode 100644 index aca4e5dda..000000000 --- a/src/ppc/sha1.h +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2009-2012 the libgit2 contributors - * - * This file is part of libgit2, distributed under the GNU GPL v2 with - * a Linking Exception. For full terms see the included COPYING file. - */ -#include <stdint.h> - -typedef struct { - uint32_t hash[5]; - uint32_t cnt; - uint64_t len; - union { - unsigned char b[64]; - uint64_t l[8]; - } buf; -} ppc_SHA_CTX; - -int ppc_SHA1_Init(ppc_SHA_CTX *c); -int ppc_SHA1_Update(ppc_SHA_CTX *c, const void *p, unsigned long n); -int ppc_SHA1_Final(unsigned char *hash, ppc_SHA_CTX *c); - -#define SHA_CTX ppc_SHA_CTX -#define SHA1_Init ppc_SHA1_Init -#define SHA1_Update ppc_SHA1_Update -#define SHA1_Final ppc_SHA1_Final diff --git a/src/ppc/sha1ppc.S b/src/ppc/sha1ppc.S deleted file mode 100644 index 1711eef6e..000000000 --- a/src/ppc/sha1ppc.S +++ /dev/null @@ -1,224 +0,0 @@ -/* - * SHA-1 implementation for PowerPC. - * - * Copyright (C) 2005 Paul Mackerras <paulus@samba.org> - */ - -/* - * PowerPC calling convention: - * %r0 - volatile temp - * %r1 - stack pointer. - * %r2 - reserved - * %r3-%r12 - Incoming arguments & return values; volatile. - * %r13-%r31 - Callee-save registers - * %lr - Return address, volatile - * %ctr - volatile - * - * Register usage in this routine: - * %r0 - temp - * %r3 - argument (pointer to 5 words of SHA state) - * %r4 - argument (pointer to data to hash) - * %r5 - Constant K in SHA round (initially number of blocks to hash) - * %r6-%r10 - Working copies of SHA variables A..E (actually E..A order) - * %r11-%r26 - Data being hashed W[]. - * %r27-%r31 - Previous copies of A..E, for final add back. - * %ctr - loop count - */ - - -/* - * We roll the registers for A, B, C, D, E around on each - * iteration; E on iteration t is D on iteration t+1, and so on. - * We use registers 6 - 10 for this. (Registers 27 - 31 hold - * the previous values.) - */ -#define RA(t) (((t)+4)%5+6) -#define RB(t) (((t)+3)%5+6) -#define RC(t) (((t)+2)%5+6) -#define RD(t) (((t)+1)%5+6) -#define RE(t) (((t)+0)%5+6) - -/* We use registers 11 - 26 for the W values */ -#define W(t) ((t)%16+11) - -/* Register 5 is used for the constant k */ - -/* - * The basic SHA-1 round function is: - * E += ROTL(A,5) + F(B,C,D) + W[i] + K; B = ROTL(B,30) - * Then the variables are renamed: (A,B,C,D,E) = (E,A,B,C,D). - * - * Every 20 rounds, the function F() and the constant K changes: - * - 20 rounds of f0(b,c,d) = "bit wise b ? c : d" = (^b & d) + (b & c) - * - 20 rounds of f1(b,c,d) = b^c^d = (b^d)^c - * - 20 rounds of f2(b,c,d) = majority(b,c,d) = (b&d) + ((b^d)&c) - * - 20 more rounds of f1(b,c,d) - * - * These are all scheduled for near-optimal performance on a G4. - * The G4 is a 3-issue out-of-order machine with 3 ALUs, but it can only - * *consider* starting the oldest 3 instructions per cycle. So to get - * maximum performance out of it, you have to treat it as an in-order - * machine. Which means interleaving the computation round t with the - * computation of W[t+4]. - * - * The first 16 rounds use W values loaded directly from memory, while the - * remaining 64 use values computed from those first 16. We preload - * 4 values before starting, so there are three kinds of rounds: - * - The first 12 (all f0) also load the W values from memory. - * - The next 64 compute W(i+4) in parallel. 8*f0, 20*f1, 20*f2, 16*f1. - * - The last 4 (all f1) do not do anything with W. - * - * Therefore, we have 6 different round functions: - * STEPD0_LOAD(t,s) - Perform round t and load W(s). s < 16 - * STEPD0_UPDATE(t,s) - Perform round t and compute W(s). s >= 16. - * STEPD1_UPDATE(t,s) - * STEPD2_UPDATE(t,s) - * STEPD1(t) - Perform round t with no load or update. - * - * The G5 is more fully out-of-order, and can find the parallelism - * by itself. The big limit is that it has a 2-cycle ALU latency, so - * even though it's 2-way, the code has to be scheduled as if it's - * 4-way, which can be a limit. To help it, we try to schedule the - * read of RA(t) as late as possible so it doesn't stall waiting for - * the previous round's RE(t-1), and we try to rotate RB(t) as early - * as possible while reading RC(t) (= RB(t-1)) as late as possible. - */ - -/* the initial loads. */ -#define LOADW(s) \ - lwz W(s),(s)*4(%r4) - -/* - * Perform a step with F0, and load W(s). Uses W(s) as a temporary - * before loading it. - * This is actually 10 instructions, which is an awkward fit. - * It can execute grouped as listed, or delayed one instruction. - * (If delayed two instructions, there is a stall before the start of the - * second line.) Thus, two iterations take 7 cycles, 3.5 cycles per round. - */ -#define STEPD0_LOAD(t,s) \ -add RE(t),RE(t),W(t); andc %r0,RD(t),RB(t); and W(s),RC(t),RB(t); \ -add RE(t),RE(t),%r0; rotlwi %r0,RA(t),5; rotlwi RB(t),RB(t),30; \ -add RE(t),RE(t),W(s); add %r0,%r0,%r5; lwz W(s),(s)*4(%r4); \ -add RE(t),RE(t),%r0 - -/* - * This is likewise awkward, 13 instructions. However, it can also - * execute starting with 2 out of 3 possible moduli, so it does 2 rounds - * in 9 cycles, 4.5 cycles/round. - */ -#define STEPD0_UPDATE(t,s,loadk...) \ -add RE(t),RE(t),W(t); andc %r0,RD(t),RB(t); xor W(s),W((s)-16),W((s)-3); \ -add RE(t),RE(t),%r0; and %r0,RC(t),RB(t); xor W(s),W(s),W((s)-8); \ -add RE(t),RE(t),%r0; rotlwi %r0,RA(t),5; xor W(s),W(s),W((s)-14); \ -add RE(t),RE(t),%r5; loadk; rotlwi RB(t),RB(t),30; rotlwi W(s),W(s),1; \ -add RE(t),RE(t),%r0 - -/* Nicely optimal. Conveniently, also the most common. */ -#define STEPD1_UPDATE(t,s,loadk...) \ -add RE(t),RE(t),W(t); xor %r0,RD(t),RB(t); xor W(s),W((s)-16),W((s)-3); \ -add RE(t),RE(t),%r5; loadk; xor %r0,%r0,RC(t); xor W(s),W(s),W((s)-8); \ -add RE(t),RE(t),%r0; rotlwi %r0,RA(t),5; xor W(s),W(s),W((s)-14); \ -add RE(t),RE(t),%r0; rotlwi RB(t),RB(t),30; rotlwi W(s),W(s),1 - -/* - * The naked version, no UPDATE, for the last 4 rounds. 3 cycles per. - * We could use W(s) as a temp register, but we don't need it. - */ -#define STEPD1(t) \ - add RE(t),RE(t),W(t); xor %r0,RD(t),RB(t); \ -rotlwi RB(t),RB(t),30; add RE(t),RE(t),%r5; xor %r0,%r0,RC(t); \ -add RE(t),RE(t),%r0; rotlwi %r0,RA(t),5; /* spare slot */ \ -add RE(t),RE(t),%r0 - -/* - * 14 instructions, 5 cycles per. The majority function is a bit - * awkward to compute. This can execute with a 1-instruction delay, - * but it causes a 2-instruction delay, which triggers a stall. - */ -#define STEPD2_UPDATE(t,s,loadk...) \ -add RE(t),RE(t),W(t); and %r0,RD(t),RB(t); xor W(s),W((s)-16),W((s)-3); \ -add RE(t),RE(t),%r0; xor %r0,RD(t),RB(t); xor W(s),W(s),W((s)-8); \ -add RE(t),RE(t),%r5; loadk; and %r0,%r0,RC(t); xor W(s),W(s),W((s)-14); \ -add RE(t),RE(t),%r0; rotlwi %r0,RA(t),5; rotlwi W(s),W(s),1; \ -add RE(t),RE(t),%r0; rotlwi RB(t),RB(t),30 - -#define STEP0_LOAD4(t,s) \ - STEPD0_LOAD(t,s); \ - STEPD0_LOAD((t+1),(s)+1); \ - STEPD0_LOAD((t)+2,(s)+2); \ - STEPD0_LOAD((t)+3,(s)+3) - -#define STEPUP4(fn, t, s, loadk...) \ - STEP##fn##_UPDATE(t,s,); \ - STEP##fn##_UPDATE((t)+1,(s)+1,); \ - STEP##fn##_UPDATE((t)+2,(s)+2,); \ - STEP##fn##_UPDATE((t)+3,(s)+3,loadk) - -#define STEPUP20(fn, t, s, loadk...) \ - STEPUP4(fn, t, s,); \ - STEPUP4(fn, (t)+4, (s)+4,); \ - STEPUP4(fn, (t)+8, (s)+8,); \ - STEPUP4(fn, (t)+12, (s)+12,); \ - STEPUP4(fn, (t)+16, (s)+16, loadk) - - .globl ppc_sha1_core -ppc_sha1_core: - stwu %r1,-80(%r1) - stmw %r13,4(%r1) - - /* Load up A - E */ - lmw %r27,0(%r3) - - mtctr %r5 - -1: - LOADW(0) - lis %r5,0x5a82 - mr RE(0),%r31 - LOADW(1) - mr RD(0),%r30 - mr RC(0),%r29 - LOADW(2) - ori %r5,%r5,0x7999 /* K0-19 */ - mr RB(0),%r28 - LOADW(3) - mr RA(0),%r27 - - STEP0_LOAD4(0, 4) - STEP0_LOAD4(4, 8) - STEP0_LOAD4(8, 12) - STEPUP4(D0, 12, 16,) - STEPUP4(D0, 16, 20, lis %r5,0x6ed9) - - ori %r5,%r5,0xeba1 /* K20-39 */ - STEPUP20(D1, 20, 24, lis %r5,0x8f1b) - - ori %r5,%r5,0xbcdc /* K40-59 */ - STEPUP20(D2, 40, 44, lis %r5,0xca62) - - ori %r5,%r5,0xc1d6 /* K60-79 */ - STEPUP4(D1, 60, 64,) - STEPUP4(D1, 64, 68,) - STEPUP4(D1, 68, 72,) - STEPUP4(D1, 72, 76,) - addi %r4,%r4,64 - STEPD1(76) - STEPD1(77) - STEPD1(78) - STEPD1(79) - - /* Add results to original values */ - add %r31,%r31,RE(0) - add %r30,%r30,RD(0) - add %r29,%r29,RC(0) - add %r28,%r28,RB(0) - add %r27,%r27,RA(0) - - bdnz 1b - - /* Save final hash, restore registers, and return */ - stmw %r27,0(%r3) - lmw %r13,4(%r1) - addi %r1,%r1,80 - blr diff --git a/src/pqueue.c b/src/pqueue.c index cb59c13ec..7819ed41e 100644 --- a/src/pqueue.c +++ b/src/pqueue.c @@ -1,8 +1,30 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. + * + * This file is based on a modified version of the priority queue found + * in the Apache project and libpqueue library. + * + * https://github.com/vy/libpqueue + * + * Original file notice: + * + * Copyright 2010 Volkan Yazici <volkan.yazici@gmail.com> + * Copyright 2006-2010 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ #include "common.h" diff --git a/src/pqueue.h b/src/pqueue.h index a3e1edd1d..ed7139285 100644 --- a/src/pqueue.h +++ b/src/pqueue.h @@ -1,8 +1,30 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. + * + * This file is based on a modified version of the priority queue found + * in the Apache project and libpqueue library. + * + * https://github.com/vy/libpqueue + * + * Original file notice: + * + * Copyright 2010 Volkan Yazici <volkan.yazici@gmail.com> + * Copyright 2006-2010 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ #ifndef INCLUDE_pqueue_h__ diff --git a/src/protocol.c b/src/protocol.c deleted file mode 100644 index 6b3861796..000000000 --- a/src/protocol.c +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2009-2012 the libgit2 contributors - * - * This file is part of libgit2, distributed under the GNU GPL v2 with - * a Linking Exception. For full terms see the included COPYING file. - */ -#include "common.h" -#include "protocol.h" -#include "pkt.h" -#include "buffer.h" - -int git_protocol_store_refs(git_protocol *p, const char *data, size_t len) -{ - git_buf *buf = &p->buf; - git_vector *refs = p->refs; - int error; - const char *line_end, *ptr; - - if (len == 0) { /* EOF */ - if (git_buf_len(buf) != 0) { - giterr_set(GITERR_NET, "Unexpected EOF"); - return p->error = -1; - } else { - return 0; - } - } - - git_buf_put(buf, data, len); - ptr = buf->ptr; - while (1) { - git_pkt *pkt; - - if (git_buf_len(buf) == 0) - return 0; - - error = git_pkt_parse_line(&pkt, ptr, &line_end, git_buf_len(buf)); - if (error == GIT_EBUFS) - return 0; /* Ask for more */ - if (error < 0) - return p->error = -1; - - git_buf_consume(buf, line_end); - - if (pkt->type == GIT_PKT_ERR) { - giterr_set(GITERR_NET, "Remote error: %s", ((git_pkt_err *)pkt)->error); - git__free(pkt); - return -1; - } - - if (git_vector_insert(refs, pkt) < 0) - return p->error = -1; - - if (pkt->type == GIT_PKT_FLUSH) - p->flush = 1; - } - - return 0; -} diff --git a/src/protocol.h b/src/protocol.h deleted file mode 100644 index a6c3e0735..000000000 --- a/src/protocol.h +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2009-2012 the libgit2 contributors - * - * This file is part of libgit2, distributed under the GNU GPL v2 with - * a Linking Exception. For full terms see the included COPYING file. - */ -#ifndef INCLUDE_protocol_h__ -#define INCLUDE_protocol_h__ - -#include "transport.h" -#include "buffer.h" - -typedef struct { - git_transport *transport; - git_vector *refs; - git_buf buf; - int error; - unsigned int flush :1; -} git_protocol; - -int git_protocol_store_refs(git_protocol *p, const char *data, size_t len); - -#endif diff --git a/src/push.c b/src/push.c new file mode 100644 index 000000000..cec4c64af --- /dev/null +++ b/src/push.c @@ -0,0 +1,653 @@ +/* + * 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 "common.h" +#include "pack.h" +#include "pack-objects.h" +#include "remote.h" +#include "vector.h" +#include "push.h" +#include "tree.h" + +static int push_spec_rref_cmp(const void *a, const void *b) +{ + const push_spec *push_spec_a = a, *push_spec_b = b; + + return strcmp(push_spec_a->rref, push_spec_b->rref); +} + +static int push_status_ref_cmp(const void *a, const void *b) +{ + const push_status *push_status_a = a, *push_status_b = b; + + return strcmp(push_status_a->ref, push_status_b->ref); +} + +int git_push_new(git_push **out, git_remote *remote) +{ + git_push *p; + + *out = NULL; + + p = git__calloc(1, sizeof(*p)); + GITERR_CHECK_ALLOC(p); + + p->repo = remote->repo; + p->remote = remote; + p->report_status = 1; + p->pb_parallelism = 1; + + if (git_vector_init(&p->specs, 0, push_spec_rref_cmp) < 0) { + git__free(p); + return -1; + } + + if (git_vector_init(&p->status, 0, push_status_ref_cmp) < 0) { + git_vector_free(&p->specs); + git__free(p); + return -1; + } + + *out = p; + return 0; +} + +int git_push_set_options(git_push *push, const git_push_options *opts) +{ + if (!push || !opts) + return -1; + + GITERR_CHECK_VERSION(opts, GIT_PUSH_OPTIONS_VERSION, "git_push_options"); + + push->pb_parallelism = opts->pb_parallelism; + + return 0; +} + +static void free_refspec(push_spec *spec) +{ + if (spec == NULL) + return; + + if (spec->lref) + git__free(spec->lref); + + if (spec->rref) + git__free(spec->rref); + + git__free(spec); +} + +static int check_rref(char *ref) +{ + if (git__prefixcmp(ref, "refs/")) { + giterr_set(GITERR_INVALID, "Not a valid reference '%s'", ref); + return -1; + } + + return 0; +} + +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) + 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) +{ + push_spec *s; + char *delim; + + *spec = NULL; + + s = git__calloc(1, sizeof(*s)); + GITERR_CHECK_ALLOC(s); + + if (str[0] == '+') { + s->force = true; + str++; + } + + delim = strchr(str, ':'); + if (delim == NULL) { + s->lref = git__strdup(str); + if (!s->lref || check_lref(push, s->lref) < 0) + goto on_error; + } else { + if (delim - str) { + s->lref = git__strndup(str, delim - str); + if (!s->lref || check_lref(push, s->lref) < 0) + goto on_error; + } + + if (strlen(delim + 1)) { + s->rref = git__strdup(delim + 1); + if (!s->rref || check_rref(s->rref) < 0) + goto on_error; + } + } + + if (!s->lref && !s->rref) + goto on_error; + + /* If rref is ommitted, use the same ref name as lref */ + if (!s->rref) { + s->rref = git__strdup(s->lref); + if (!s->rref || check_rref(s->rref) < 0) + goto on_error; + } + + *spec = s; + return 0; + +on_error: + free_refspec(s); + return -1; +} + +int git_push_add_refspec(git_push *push, const char *refspec) +{ + push_spec *spec; + + if (parse_refspec(push, &spec, refspec) < 0 || + git_vector_insert(&push->specs, spec) < 0) + return -1; + + return 0; +} + +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_reference *remote_ref; + push_status *status; + int error = 0; + + git_vector_foreach(&push->status, i, status) { + /* If this ref update was successful (ok, not ng), it will have an empty message */ + if (status->msg) + continue; + + /* Find the corresponding remote ref */ + if (!git_refspec_src_matches(fetch_spec, status->ref)) + continue; + + if ((error = git_refspec_transform_r(&remote_ref_name, fetch_spec, status->ref)) < 0) + goto on_error; + + /* Find matching push ref spec */ + git_vector_foreach(&push->specs, j, push_spec) { + if (!strcmp(push_spec->rref, status->ref)) + break; + } + + /* Could not find the corresponding push ref spec for this push update */ + if (j == push->specs.length) + continue; + + /* Update the remote ref */ + if (git_oid_iszero(&push_spec->loid)) { + error = git_reference_lookup(&remote_ref, push->remote->repo, git_buf_cstr(&remote_ref_name)); + + if (!error) { + if ((error = git_reference_delete(remote_ref)) < 0) { + git_reference_free(remote_ref); + goto on_error; + } + git_reference_free(remote_ref); + } else if (error == GIT_ENOTFOUND) + giterr_clear(); + else + goto on_error; + } else if ((error = git_reference_create(NULL, push->remote->repo, git_buf_cstr(&remote_ref_name), &push_spec->loid, 1)) < 0) + goto on_error; + } + + error = 0; + +on_error: + git_buf_free(&remote_ref_name); + return error; +} + +static int revwalk(git_vector *commits, git_push *push) +{ + git_remote_head *head; + push_spec *spec; + git_revwalk *rw; + git_oid oid; + unsigned int i; + int error = -1; + + if (git_revwalk_new(&rw, push->repo) < 0) + return -1; + + git_revwalk_sorting(rw, GIT_SORT_TIME); + + git_vector_foreach(&push->specs, i, spec) { + git_otype type; + size_t size; + + if (git_oid_iszero(&spec->loid)) + /* + * Delete reference on remote side; + * nothing to do here. + */ + continue; + + if (git_oid_equal(&spec->loid, &spec->roid)) + continue; /* up-to-date */ + + if (git_odb_read_header(&size, &type, push->repo->_odb, &spec->loid) < 0) + goto on_error; + + if (type == GIT_OBJ_TAG) { + git_tag *tag; + git_object *target; + + if (git_packbuilder_insert(push->pb, &spec->loid, NULL) < 0) + goto on_error; + + if (git_tag_lookup(&tag, push->repo, &spec->loid) < 0) + goto on_error; + + if (git_tag_peel(&target, tag) < 0) { + git_tag_free(tag); + goto on_error; + } + git_tag_free(tag); + + if (git_object_type(target) == GIT_OBJ_COMMIT) { + if (git_revwalk_push(rw, git_object_id(target)) < 0) { + git_object_free(target); + goto on_error; + } + } else { + if (git_packbuilder_insert( + push->pb, git_object_id(target), NULL) < 0) { + git_object_free(target); + goto on_error; + } + } + git_object_free(target); + } else if (git_revwalk_push(rw, &spec->loid) < 0) + goto on_error; + + if (!spec->force) { + git_oid base; + + if (git_oid_iszero(&spec->roid)) + continue; + + if (!git_odb_exists(push->repo->_odb, &spec->roid)) { + giterr_clear(); + error = GIT_ENONFASTFORWARD; + goto on_error; + } + + error = git_merge_base(&base, push->repo, + &spec->loid, &spec->roid); + + if (error == GIT_ENOTFOUND || + (!error && !git_oid_equal(&base, &spec->roid))) { + giterr_clear(); + error = GIT_ENONFASTFORWARD; + goto on_error; + } + + if (error < 0) + goto on_error; + } + } + + git_vector_foreach(&push->remote->refs, i, head) { + if (git_oid_iszero(&head->oid)) + continue; + + /* TODO */ + git_revwalk_hide(rw, &head->oid); + } + + 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) { + error = -1; + goto on_error; + } + } + +on_error: + git_revwalk_free(rw); + return error == GIT_ITEROVER ? 0 : error; +} + +static int enqueue_object( + const git_tree_entry *entry, + git_packbuilder *pb) +{ + switch (git_tree_entry_type(entry)) { + case GIT_OBJ_COMMIT: + return 0; + case GIT_OBJ_TREE: + return git_packbuilder_insert_tree(pb, &entry->oid); + default: + return git_packbuilder_insert(pb, &entry->oid, entry->filename); + } +} + +static int queue_differences( + git_tree *base, + git_tree *delta, + git_packbuilder *pb) +{ + git_tree *b_child = NULL, *d_child = NULL; + size_t b_length = git_tree_entrycount(base); + size_t d_length = git_tree_entrycount(delta); + size_t i = 0, j = 0; + int error; + + while (i < b_length && j < d_length) { + const git_tree_entry *b_entry = git_tree_entry_byindex(base, i); + 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)) + goto loop; + + cmp = strcmp(b_entry->filename, d_entry->filename); + + /* If the entries are both trees and they have the same name but are + * different, then we'll recurse after adding the right-hand entry */ + if (!cmp && + git_tree_entry__is_tree(b_entry) && + git_tree_entry__is_tree(d_entry)) { + /* Add the right-hand entry */ + if ((error = git_packbuilder_insert(pb, &d_entry->oid, + d_entry->filename)) < 0) + goto on_error; + + /* Acquire the subtrees and recurse */ + if ((error = git_tree_lookup(&b_child, + git_tree_owner(base), &b_entry->oid)) < 0 || + (error = git_tree_lookup(&d_child, + git_tree_owner(delta), &d_entry->oid)) < 0 || + (error = queue_differences(b_child, d_child, pb)) < 0) + goto on_error; + + git_tree_free(b_child); b_child = NULL; + git_tree_free(d_child); d_child = NULL; + } + /* If the object is new or different in the right-hand tree, + * then enumerate it */ + else if (cmp >= 0 && + (error = enqueue_object(d_entry, pb)) < 0) + goto on_error; + + loop: + if (cmp <= 0) i++; + if (cmp >= 0) j++; + } + + /* Drain the right-hand tree of entries */ + for (; j < d_length; j++) + if ((error = enqueue_object(git_tree_entry_byindex(delta, j), pb)) < 0) + goto on_error; + + error = 0; + +on_error: + if (b_child) + git_tree_free(b_child); + + if (d_child) + git_tree_free(d_child); + + return error; +} + +static int queue_objects(git_push *push) +{ + git_vector commits = GIT_VECTOR_INIT; + git_oid *oid; + size_t i; + unsigned j; + int error; + + if ((error = revwalk(&commits, push)) < 0) + goto on_error; + + git_vector_foreach(&commits, i, oid) { + git_commit *parent = NULL, *commit; + git_tree *tree = NULL, *ptree = NULL; + size_t parentcount; + + if ((error = git_commit_lookup(&commit, push->repo, oid)) < 0) + goto on_error; + + /* Insert the commit */ + if ((error = git_packbuilder_insert(push->pb, oid, NULL)) < 0) + goto loop_error; + + parentcount = git_commit_parentcount(commit); + + if (!parentcount) { + if ((error = git_packbuilder_insert_tree(push->pb, + git_commit_tree_id(commit))) < 0) + goto loop_error; + } else { + if ((error = git_tree_lookup(&tree, push->repo, + git_commit_tree_id(commit))) < 0 || + (error = git_packbuilder_insert(push->pb, + git_commit_tree_id(commit), NULL)) < 0) + goto loop_error; + + /* For each parent, add the items which are different */ + for (j = 0; j < parentcount; j++) { + if ((error = git_commit_parent(&parent, commit, j)) < 0 || + (error = git_commit_tree(&ptree, parent)) < 0 || + (error = queue_differences(ptree, tree, push->pb)) < 0) + goto loop_error; + + git_tree_free(ptree); ptree = NULL; + git_commit_free(parent); parent = NULL; + } + } + + error = 0; + + loop_error: + if (tree) + git_tree_free(tree); + + if (ptree) + git_tree_free(ptree); + + if (parent) + git_commit_free(parent); + + git_commit_free(commit); + + if (error < 0) + goto on_error; + } + + error = 0; + +on_error: + git_vector_foreach(&commits, i, oid) + git__free(oid); + + git_vector_free(&commits); + return error; +} + +static int calculate_work(git_push *push) +{ + git_remote_head *head; + push_spec *spec; + unsigned int i, j; + + /* Update local and remote oids*/ + + git_vector_foreach(&push->specs, i, spec) { + if (spec->lref) { + /* 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); + return -1; + } + } + + if (spec->rref) { + /* Remote ref may or may not (e.g. during create) already exist. */ + git_vector_foreach(&push->remote->refs, j, head) { + if (!strcmp(spec->rref, head->name)) { + git_oid_cpy(&spec->roid, &head->oid); + break; + } + } + } + } + + return 0; +} + +static int do_push(git_push *push) +{ + int error; + git_transport *transport = push->remote->transport; + + if (!transport->push) { + giterr_set(GITERR_NET, "Remote transport doesn't support push"); + error = -1; + goto on_error; + } + + /* + * A pack-file MUST be sent if either create or update command + * is used, even if the server already has all the necessary + * objects. In this case the client MUST send an empty pack-file. + */ + + if ((error = git_packbuilder_new(&push->pb, push->repo)) < 0) + goto on_error; + + git_packbuilder_set_threads(push->pb, push->pb_parallelism); + + if ((error = calculate_work(push)) < 0 || + (error = queue_objects(push)) < 0 || + (error = transport->push(transport, push)) < 0) + goto on_error; + + error = 0; + +on_error: + git_packbuilder_free(push->pb); + return error; +} + +static int cb_filter_refs(git_remote_head *ref, void *data) +{ + git_remote *remote = (git_remote *) data; + return git_vector_insert(&remote->refs, ref); +} + +static int filter_refs(git_remote *remote) +{ + git_vector_clear(&remote->refs); + return git_remote_ls(remote, cb_filter_refs, remote); +} + +int git_push_finish(git_push *push) +{ + int error; + + if (!git_remote_connected(push->remote) && + (error = git_remote_connect(push->remote, GIT_DIRECTION_PUSH)) < 0) + return error; + + if ((error = filter_refs(push->remote)) < 0 || + (error = do_push(push)) < 0) + return error; + + return 0; +} + +int git_push_unpack_ok(git_push *push) +{ + return push->unpack_ok; +} + +int git_push_status_foreach(git_push *push, + int (*cb)(const char *ref, const char *msg, void *data), + void *data) +{ + push_status *status; + unsigned int i; + + git_vector_foreach(&push->status, i, status) { + if (cb(status->ref, status->msg, data) < 0) + return GIT_EUSER; + } + + return 0; +} + +void git_push_status_free(push_status *status) +{ + if (status == NULL) + return; + + if (status->msg) + git__free(status->msg); + + git__free(status->ref); + git__free(status); +} + +void git_push_free(git_push *push) +{ + push_spec *spec; + push_status *status; + unsigned int i; + + if (push == NULL) + return; + + git_vector_foreach(&push->specs, i, spec) { + free_refspec(spec); + } + git_vector_free(&push->specs); + + git_vector_foreach(&push->status, i, status) { + git_push_status_free(status); + } + git_vector_free(&push->status); + + git__free(push); +} diff --git a/src/push.h b/src/push.h new file mode 100644 index 000000000..e982b8385 --- /dev/null +++ b/src/push.h @@ -0,0 +1,51 @@ +/* + * 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_push_h__ +#define INCLUDE_push_h__ + +#include "git2.h" + +typedef struct push_spec { + char *lref; + char *rref; + + git_oid loid; + git_oid roid; + + bool force; +} push_spec; + +typedef struct push_status { + bool ok; + + char *ref; + char *msg; +} push_status; + +struct git_push { + git_repository *repo; + git_packbuilder *pb; + git_remote *remote; + git_vector specs; + bool report_status; + + /* report-status */ + bool unpack_ok; + git_vector status; + + /* options */ + unsigned pb_parallelism; +}; + +/** + * Free the given push status object + * + * @param status The push status object + */ +void git_push_status_free(push_status *status); + +#endif diff --git a/src/refdb.c b/src/refdb.c new file mode 100644 index 000000000..d9b73c6e7 --- /dev/null +++ b/src/refdb.c @@ -0,0 +1,185 @@ +/* + * 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 "posix.h" +#include "git2/object.h" +#include "git2/refs.h" +#include "git2/refdb.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; + + assert(out && repo); + + db = git__calloc(1, sizeof(*db)); + GITERR_CHECK_ALLOC(db); + + db->repo = repo; + + *out = db; + GIT_REFCOUNT_INC(db); + return 0; +} + +int git_refdb_open(git_refdb **out, git_repository *repo) +{ + git_refdb *db; + git_refdb_backend *dir; + + assert(out && repo); + + *out = NULL; + + if (git_refdb_new(&db, repo) < 0) + return -1; + + /* Add the default (filesystem) backend */ + if (git_refdb_backend_fs(&dir, repo, db) < 0) { + git_refdb_free(db); + return -1; + } + + db->repo = repo; + db->backend = dir; + + *out = db; + return 0; +} + +int git_refdb_set_backend(git_refdb *db, git_refdb_backend *backend) +{ + if (db->backend) { + if(db->backend->free) + db->backend->free(db->backend); + else + git__free(db->backend); + } + + db->backend = backend; + + return 0; +} + +int git_refdb_compress(git_refdb *db) +{ + assert(db); + + if (db->backend->compress) { + return db->backend->compress(db->backend); + } + + return 0; +} + +static void refdb_free(git_refdb *db) +{ + if (db->backend) { + if(db->backend->free) + db->backend->free(db->backend); + else + git__free(db->backend); + } + + git__free(db); +} + +void git_refdb_free(git_refdb *db) +{ + if (db == NULL) + return; + + GIT_REFCOUNT_DEC(db, refdb_free); +} + +int git_refdb_exists(int *exists, git_refdb *refdb, const char *ref_name) +{ + assert(exists && refdb && refdb->backend); + + return refdb->backend->exists(exists, refdb->backend, ref_name); +} + +int git_refdb_lookup(git_reference **out, git_refdb *db, const char *ref_name) +{ + assert(db && db->backend && ref_name); + + return db->backend->lookup(out, db->backend, ref_name); +} + +int git_refdb_foreach( + git_refdb *db, + unsigned int list_flags, + git_reference_foreach_cb callback, + void *payload) +{ + assert(db && db->backend); + + return db->backend->foreach(db->backend, list_flags, callback, payload); +} + +struct glob_cb_data { + const char *glob; + git_reference_foreach_cb callback; + void *payload; +}; + +static int fromglob_cb(const char *reference_name, void *payload) +{ + struct glob_cb_data *data = (struct glob_cb_data *)payload; + + if (!p_fnmatch(data->glob, reference_name, 0)) + return data->callback(reference_name, data->payload); + + 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 error; + struct glob_cb_data data; + + assert(db && db->backend && glob && callback); + + 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; + + error = db->backend->foreach(db->backend, + list_flags, fromglob_cb, &data); + } + + return error; +} + +int git_refdb_write(git_refdb *db, const git_reference *ref) +{ + assert(db && db->backend); + + return db->backend->write(db->backend, ref); +} + +int git_refdb_delete(struct git_refdb *db, const git_reference *ref) +{ + assert(db && db->backend); + + return db->backend->delete(db->backend, ref); +} diff --git a/src/refdb.h b/src/refdb.h new file mode 100644 index 000000000..0969711b9 --- /dev/null +++ b/src/refdb.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_refdb_h__ +#define INCLUDE_refdb_h__ + +#include "git2/refdb.h" +#include "repository.h" + +struct git_refdb { + git_refcount rc; + git_repository *repo; + git_refdb_backend *backend; +}; + +int git_refdb_exists( + int *exists, + git_refdb *refdb, + const char *ref_name); + +int git_refdb_lookup( + git_reference **out, + 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); + +#endif diff --git a/src/refdb_fs.c b/src/refdb_fs.c new file mode 100644 index 000000000..f00bd72a0 --- /dev/null +++ b/src/refdb_fs.c @@ -0,0 +1,1023 @@ +/* + * 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 "refs.h" +#include "hash.h" +#include "repository.h" +#include "fileops.h" +#include "pack.h" +#include "reflog.h" +#include "config.h" +#include "refdb.h" +#include "refdb_fs.h" + +#include <git2/tag.h> +#include <git2/object.h> +#include <git2/refdb.h> +#include <git2/refdb_backend.h> + +GIT__USE_STRMAP; + +#define DEFAULT_NESTING_LEVEL 5 +#define MAX_NESTING_LEVEL 10 + +enum { + GIT_PACKREF_HAS_PEEL = 1, + GIT_PACKREF_WAS_LOOSE = 2 +}; + +struct packref { + git_oid oid; + git_oid peel; + char flags; + char name[GIT_FLEX_ARRAY]; +}; + +typedef struct refdb_fs_backend { + git_refdb_backend parent; + + git_repository *repo; + const char *path; + git_refdb *refdb; + + git_refcache refcache; +} refdb_fs_backend; + +static int reference_read( + git_buf *file_content, + time_t *mtime, + const char *repo_path, + const char *ref_name, + int *updated) +{ + git_buf path = GIT_BUF_INIT; + int result; + + assert(file_content && repo_path && ref_name); + + /* 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); + + return result; +} + +static int packed_parse_oid( + struct packref **ref_out, + const char **buffer_out, + const char *buffer_end) +{ + struct packref *ref = NULL; + + const char *buffer = *buffer_out; + const char *refname_begin, *refname_end; + + size_t refname_len; + git_oid id; + + refname_begin = (buffer + GIT_OID_HEXSZ + 1); + if (refname_begin >= buffer_end || refname_begin[-1] != ' ') + goto corrupt; + + /* Is this a valid object id? */ + if (git_oid_fromstr(&id, buffer) < 0) + goto corrupt; + + refname_end = memchr(refname_begin, '\n', buffer_end - refname_begin); + if (refname_end == NULL) + refname_end = buffer_end; + + if (refname_end[-1] == '\r') + refname_end--; + + refname_len = refname_end - refname_begin; + + ref = git__malloc(sizeof(struct packref) + refname_len + 1); + GITERR_CHECK_ALLOC(ref); + + memcpy(ref->name, refname_begin, refname_len); + ref->name[refname_len] = 0; + + git_oid_cpy(&ref->oid, &id); + + ref->flags = 0; + + *ref_out = ref; + *buffer_out = refname_end + 1; + + return 0; + +corrupt: + git__free(ref); + giterr_set(GITERR_REFERENCE, "The packed references file is corrupted"); + return -1; +} + +static int packed_parse_peel( + struct packref *tag_ref, + const char **buffer_out, + const char *buffer_end) +{ + const char *buffer = *buffer_out + 1; + + assert(buffer[-1] == '^'); + + /* Ensure it's not the first entry of the file */ + 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; + + /* Is this a valid object id? */ + if (git_oid_fromstr(&tag_ref->peel, buffer) < 0) + goto corrupt; + + buffer = buffer + GIT_OID_HEXSZ; + if (*buffer == '\r') + buffer++; + + if (buffer != buffer_end) { + if (*buffer == '\n') + buffer++; + else + goto corrupt; + } + + *buffer_out = buffer; + return 0; + +corrupt: + giterr_set(GITERR_REFERENCE, "The packed references file is corrupted"); + return -1; +} + +static int packed_load(refdb_fs_backend *backend) +{ + int result, updated; + git_buf packfile = GIT_BUF_INIT; + const char *buffer_start, *buffer_end; + git_refcache *ref_cache = &backend->refcache; + + /* First we make sure we have allocated the hash table */ + if (ref_cache->packfile == NULL) { + ref_cache->packfile = git_strmap_alloc(); + GITERR_CHECK_ALLOC(ref_cache->packfile); + } + + result = reference_read(&packfile, &ref_cache->packfile_time, + backend->path, GIT_PACKEDREFS_FILE, &updated); + + /* + * If we couldn't find the file, we need to clear the table and + * return. On any other error, we return that error. If everything + * went fine and the file wasn't updated, then there's nothing new + * for us here, so just return. Anything else means we need to + * refresh the packed refs. + */ + if (result == GIT_ENOTFOUND) { + git_strmap_clear(ref_cache->packfile); + return 0; + } + + if (result < 0) + return -1; + + if (!updated) + return 0; + + /* + * At this point, we want to refresh the packed refs. We already + * have the contents in our buffer. + */ + git_strmap_clear(ref_cache->packfile); + + buffer_start = (const char *)packfile.ptr; + buffer_end = (const char *)(buffer_start) + packfile.size; + + while (buffer_start < buffer_end && buffer_start[0] == '#') { + buffer_start = strchr(buffer_start, '\n'); + if (buffer_start == NULL) + goto parse_failed; + + buffer_start++; + } + + while (buffer_start < buffer_end) { + int err; + struct packref *ref = NULL; + + if (packed_parse_oid(&ref, &buffer_start, buffer_end) < 0) + goto parse_failed; + + if (buffer_start[0] == '^') { + if (packed_parse_peel(ref, &buffer_start, buffer_end) < 0) + goto parse_failed; + } + + git_strmap_insert(ref_cache->packfile, ref->name, ref, err); + if (err < 0) + goto parse_failed; + } + + git_buf_free(&packfile); + return 0; + +parse_failed: + git_strmap_free(ref_cache->packfile); + ref_cache->packfile = NULL; + git_buf_free(&packfile); + return -1; +} + +static int loose_parse_oid(git_oid *oid, git_buf *file_content) +{ + size_t len; + const char *str; + + len = git_buf_len(file_content); + if (len < GIT_OID_HEXSZ) + goto corrupted; + + /* str is guranteed to be zero-terminated */ + str = git_buf_cstr(file_content); + + /* we need to get 40 OID characters from the file */ + if (git_oid_fromstr(oid, git_buf_cstr(file_content)) < 0) + goto corrupted; + + /* If the file is longer than 40 chars, the 41st must be a space */ + str += GIT_OID_HEXSZ; + if (*str == '\0' || git__isspace(*str)) + return 0; + +corrupted: + giterr_set(GITERR_REFERENCE, "Corrupted loose reference file"); + return -1; +} + +static int loose_lookup_to_packfile( + struct packref **ref_out, + refdb_fs_backend *backend, + const char *name) +{ + git_buf ref_file = GIT_BUF_INIT; + struct packref *ref = NULL; + size_t name_len; + + *ref_out = NULL; + + if (reference_read(&ref_file, NULL, backend->path, name, NULL) < 0) + return -1; + + git_buf_rtrim(&ref_file); + + name_len = strlen(name); + ref = git__malloc(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) { + git_buf_free(&ref_file); + git__free(ref); + return -1; + } + + ref->flags = GIT_PACKREF_WAS_LOOSE; + + *ref_out = ref; + git_buf_free(&ref_file); + return 0; +} + + +static int _dirent_loose_load(void *data, git_buf *full_path) +{ + refdb_fs_backend *backend = (refdb_fs_backend *)data; + void *old_ref = NULL; + struct packref *ref; + const char *file_path; + int err; + + if (git_path_isdir(full_path->ptr) == true) + return git_path_direach(full_path, _dirent_loose_load, backend); + + file_path = full_path->ptr + strlen(backend->path); + + if (loose_lookup_to_packfile(&ref, backend, file_path) < 0) + return -1; + + git_strmap_insert2( + backend->refcache.packfile, ref->name, ref, old_ref, err); + if (err < 0) { + git__free(ref); + return -1; + } + + git__free(old_ref); + return 0; +} + +/* + * Load all the loose references from the repository + * into the in-memory Packfile, and build a vector with + * all the references so it can be written back to + * disk. + */ +static int packed_loadloose(refdb_fs_backend *backend) +{ + git_buf refs_path = GIT_BUF_INIT; + int result; + + /* the packfile must have been previously loaded! */ + assert(backend->refcache.packfile); + + if (git_buf_joinpath(&refs_path, backend->path, GIT_REFS_DIR) < 0) + return -1; + + /* + * Load all the loose files from disk into the Packfile table. + * This will overwrite any old packed entries with their + * updated loose versions + */ + result = git_path_direach(&refs_path, _dirent_loose_load, backend); + git_buf_free(&refs_path); + + return result; +} + +static int refdb_fs_backend__exists( + int *exists, + git_refdb_backend *_backend, + const char *ref_name) +{ + refdb_fs_backend *backend; + git_buf ref_path = GIT_BUF_INIT; + + assert(_backend); + backend = (refdb_fs_backend *)_backend; + + if (packed_load(backend) < 0) + return -1; + + if (git_buf_joinpath(&ref_path, backend->path, ref_name) < 0) + return -1; + + if (git_path_isfile(ref_path.ptr) == true || + git_strmap_exists(backend->refcache.packfile, ref_path.ptr)) + *exists = 1; + else + *exists = 0; + + git_buf_free(&ref_path); + return 0; +} + +static const char *loose_parse_symbolic(git_buf *file_content) +{ + const unsigned int header_len = (unsigned int)strlen(GIT_SYMREF); + const char *refname_start; + + refname_start = (const char *)file_content->ptr; + + if (git_buf_len(file_content) < header_len + 1) { + giterr_set(GITERR_REFERENCE, "Corrupted loose reference file"); + return NULL; + } + + /* + * Assume we have already checked for the header + * before calling this function + */ + refname_start += header_len; + + return refname_start; +} + +static int loose_lookup( + git_reference **out, + refdb_fs_backend *backend, + const char *ref_name) +{ + const char *target; + git_oid oid; + git_buf ref_file = GIT_BUF_INIT; + int error = 0; + + error = reference_read(&ref_file, NULL, backend->path, ref_name, NULL); + + if (error < 0) + goto done; + + if (git__prefixcmp((const char *)(ref_file.ptr), GIT_SYMREF) == 0) { + git_buf_rtrim(&ref_file); + + if ((target = loose_parse_symbolic(&ref_file)) == NULL) { + error = -1; + goto done; + } + + *out = git_reference__alloc(backend->refdb, ref_name, NULL, target); + } else { + if ((error = loose_parse_oid(&oid, &ref_file)) < 0) + goto done; + + *out = git_reference__alloc(backend->refdb, ref_name, &oid, NULL); + } + + if (*out == NULL) + error = -1; + +done: + git_buf_free(&ref_file); + return error; +} + +static int packed_map_entry( + struct packref **entry, + khiter_t *pos, + refdb_fs_backend *backend, + const char *ref_name) +{ + git_strmap *packfile_refs; + + 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; +} + +static int packed_lookup( + git_reference **out, + refdb_fs_backend *backend, + const char *ref_name) +{ + 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) + return -1; + + return 0; +} + +static int refdb_fs_backend__lookup( + git_reference **out, + git_refdb_backend *_backend, + const char *ref_name) +{ + refdb_fs_backend *backend; + int result; + + assert(_backend); + + backend = (refdb_fs_backend *)_backend; + + if ((result = loose_lookup(out, backend, ref_name)) == 0) + return 0; + + /* only try to lookup this reference on the packfile if it + * wasn't found on the loose refs; not if there was a critical error */ + if (result == GIT_ENOTFOUND) { + giterr_clear(); + result = packed_lookup(out, backend, ref_name); + } + + return result; +} + +struct dirent_list_data { + refdb_fs_backend *backend; + size_t repo_path_len; + unsigned int list_type:2; + + git_reference_foreach_cb callback; + void *callback_payload; + int callback_error; +}; + +static git_ref_t loose_guess_rtype(const git_buf *full_path) +{ + git_buf ref_file = GIT_BUF_INIT; + git_ref_t type; + + type = GIT_REF_INVALID; + + 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; + } + + git_buf_free(&ref_file); + return type; +} + +static int _dirent_loose_listall(void *_data, git_buf *full_path) +{ + struct dirent_list_data *data = (struct dirent_list_data *)_data; + const char *file_path = full_path->ptr + data->repo_path_len; + + if (git_path_isdir(full_path->ptr) == true) + return git_path_direach(full_path, _dirent_loose_listall, _data); + + /* 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 (data->list_type != GIT_REF_LISTALL) { + if ((data->list_type & loose_guess_rtype(full_path)) == 0) + return 0; /* we are filtering out this reference */ + } + + /* Locked references aren't returned */ + if (!git__suffixcmp(file_path, GIT_FILELOCK_EXTENSION)) + return 0; + + if (data->callback(file_path, data->callback_payload)) + data->callback_error = GIT_EUSER; + + return data->callback_error; +} + +static int refdb_fs_backend__foreach( + git_refdb_backend *_backend, + unsigned int list_type, + git_reference_foreach_cb callback, + void *payload) +{ + 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; + }); + } + + /* now list the loose references, trying not to + * duplicate the ref names already in the packed-refs file */ + + 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; + + if (git_buf_joinpath(&refs_path, backend->path, GIT_REFS_DIR) < 0) + return -1; + + result = git_path_direach(&refs_path, _dirent_loose_listall, &data); + + git_buf_free(&refs_path); + + return data.callback_error ? GIT_EUSER : result; +} + +static int loose_write(refdb_fs_backend *backend, const git_reference *ref) +{ + git_filebuf file = GIT_FILEBUF_INIT; + git_buf ref_path = GIT_BUF_INIT; + + /* 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) + return -1; + + if (git_buf_joinpath(&ref_path, backend->path, ref->name) < 0) + return -1; + + if (git_filebuf_open(&file, ref_path.ptr, GIT_FILEBUF_FORCE) < 0) { + git_buf_free(&ref_path); + return -1; + } + + git_buf_free(&ref_path); + + if (ref->type == GIT_REF_OID) { + char oid[GIT_OID_HEXSZ + 1]; + + git_oid_fmt(oid, &ref->target.oid); + oid[GIT_OID_HEXSZ] = '\0'; + + git_filebuf_printf(&file, "%s\n", oid); + + } else if (ref->type == GIT_REF_SYMBOLIC) { + git_filebuf_printf(&file, GIT_SYMREF "%s\n", ref->target.symbolic); + } else { + assert(0); /* don't let this happen */ + } + + return git_filebuf_commit(&file, GIT_REFS_FILE_MODE); +} + +static int packed_sort(const void *a, const void *b) +{ + const struct packref *ref_a = (const struct packref *)a; + const struct packref *ref_b = (const struct packref *)b; + + return strcmp(ref_a->name, ref_b->name); +} + +/* + * Find out what object this reference resolves to. + * + * For references that point to a 'big' tag (e.g. an + * actual tag object on the repository), we need to + * cache on the packfile the OID of the object to + * which that 'big tag' is pointing to. + */ +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) + return 0; + + /* + * Find the tagged object in the repository + */ + if (git_object_lookup(&object, backend->repo, &ref->oid, GIT_OBJ_ANY) < 0) + return -1; + + /* + * If the tagged object is a Tag object, we need to resolve it; + * if the ref is actually a 'weak' ref, we don't need to resolve + * anything. + */ + if (git_object_type(object) == GIT_OBJ_TAG) { + git_tag *tag = (git_tag *)object; + + /* + * Find the object pointed at by this tag + */ + git_oid_cpy(&ref->peel, git_tag_target_id(tag)); + ref->flags |= GIT_PACKREF_HAS_PEEL; + + /* + * The reference has now cached the resolved OID, and is + * marked at such. When written to the packfile, it'll be + * accompanied by this resolved oid + */ + } + + git_object_free(object); + return 0; +} + +/* + * Write a single reference into a packfile + */ +static int packed_write_ref(struct packref *ref, git_filebuf *file) +{ + char oid[GIT_OID_HEXSZ + 1]; + + git_oid_fmt(oid, &ref->oid); + oid[GIT_OID_HEXSZ] = 0; + + /* + * For references that peel to an object in the repo, we must + * write the resulting peel on a separate line, e.g. + * + * 6fa8a902cc1d18527e1355773c86721945475d37 refs/tags/libgit2-0.4 + * ^2ec0cb7959b0bf965d54f95453f5b4b34e8d3100 + * + * This obviously only applies to tags. + * The required peels have already been loaded into `ref->peel_target`. + */ + if (ref->flags & GIT_PACKREF_HAS_PEEL) { + char peel[GIT_OID_HEXSZ + 1]; + git_oid_fmt(peel, &ref->peel); + peel[GIT_OID_HEXSZ] = 0; + + if (git_filebuf_printf(file, "%s %s\n^%s\n", oid, ref->name, peel) < 0) + return -1; + } else { + if (git_filebuf_printf(file, "%s %s\n", oid, ref->name) < 0) + return -1; + } + + return 0; +} + +/* + * Remove all loose references + * + * Once we have successfully written a packfile, + * all the loose references that were packed must be + * removed from disk. + * + * This is a dangerous method; make sure the packfile + * is well-written, because we are destructing references + * here otherwise. + */ +static int packed_remove_loose( + refdb_fs_backend *backend, + git_vector *packing_list) +{ + size_t i; + git_buf full_path = GIT_BUF_INIT; + int failed = 0; + + 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) + continue; + + if (git_buf_joinpath(&full_path, backend->path, ref->name) < 0) + return -1; /* critical; do not try to recover on oom */ + + if (git_path_exists(full_path.ptr) == true && p_unlink(full_path.ptr) < 0) { + if (failed) + continue; + + giterr_set(GITERR_REFERENCE, + "Failed to remove loose reference '%s' after packing: %s", + full_path.ptr, strerror(errno)); + + failed = 1; + } + + /* + * if we fail to remove a single file, this is *not* good, + * but we should keep going and remove as many as possible. + * After we've removed as many files as possible, we return + * the error code anyway. + */ + } + + git_buf_free(&full_path); + return failed ? -1 : 0; +} + +/* + * Write all the contents in the in-memory packfile to disk. + */ +static int packed_write(refdb_fs_backend *backend) +{ + git_filebuf pack_file = GIT_FILEBUF_INIT; + size_t i; + git_buf pack_file_path = GIT_BUF_INIT; + git_vector packing_list; + unsigned int total_refs; + + assert(backend && backend->refcache.packfile); + + total_refs = + (unsigned int)git_strmap_num_entries(backend->refcache.packfile); + + if (git_vector_init(&packing_list, total_refs, packed_sort) < 0) + return -1; + + /* Load all the packfile into a vector */ + { + struct packref *reference; + + /* cannot fail: vector already has the right size */ + git_strmap_foreach_value(backend->refcache.packfile, reference, { + git_vector_insert(&packing_list, reference); + }); + } + + /* sort the vector so the entries appear sorted on the packfile */ + git_vector_sort(&packing_list); + + /* Now we can open the file! */ + if (git_buf_joinpath(&pack_file_path, + backend->path, GIT_PACKEDREFS_FILE) < 0) + goto cleanup_memory; + + if (git_filebuf_open(&pack_file, pack_file_path.ptr, 0) < 0) + goto cleanup_packfile; + + /* Packfiles have a header... apparently + * This is in fact not required, but we might as well print it + * just for kicks */ + if (git_filebuf_printf(&pack_file, "%s\n", GIT_PACKEDREFS_HEADER) < 0) + goto cleanup_packfile; + + for (i = 0; i < packing_list.length; ++i) { + struct packref *ref = (struct packref *)git_vector_get(&packing_list, i); + + if (packed_find_peel(backend, ref) < 0) + goto cleanup_packfile; + + if (packed_write_ref(ref, &pack_file) < 0) + goto cleanup_packfile; + } + + /* if we've written all the references properly, we can commit + * the packfile to make the changes effective */ + if (git_filebuf_commit(&pack_file, GIT_PACKEDREFS_FILE_MODE) < 0) + goto cleanup_memory; + + /* when and only when the packfile has been properly written, + * we can go ahead and remove the loose refs */ + if (packed_remove_loose(backend, &packing_list) < 0) + goto cleanup_memory; + + { + struct stat st; + if (p_stat(pack_file_path.ptr, &st) == 0) + backend->refcache.packfile_time = st.st_mtime; + } + + git_vector_free(&packing_list); + git_buf_free(&pack_file_path); + + /* we're good now */ + return 0; + +cleanup_packfile: + git_filebuf_cleanup(&pack_file); + +cleanup_memory: + git_vector_free(&packing_list); + git_buf_free(&pack_file_path); + + return -1; +} + +static int refdb_fs_backend__write( + git_refdb_backend *_backend, + const git_reference *ref) +{ + refdb_fs_backend *backend; + + assert(_backend); + backend = (refdb_fs_backend *)_backend; + + return loose_write(backend, ref); +} + +static int refdb_fs_backend__delete( + git_refdb_backend *_backend, + const git_reference *ref) +{ + 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; + + assert(_backend); + assert(ref); + + 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) + 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 */ + + if ((pack_error = packed_map_entry(&pack_ref, &pack_ref_pos, backend, ref->name)) == 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__compress(git_refdb_backend *_backend) +{ + refdb_fs_backend *backend; + + assert(_backend); + backend = (refdb_fs_backend *)_backend; + + if (packed_load(backend) < 0 || /* load the existing packfile */ + packed_loadloose(backend) < 0 || /* add all the loose refs */ + packed_write(backend) < 0) /* write back to disk */ + return -1; + + return 0; +} + +static void refcache_free(git_refcache *refs) +{ + assert(refs); + + if (refs->packfile) { + struct packref *reference; + + git_strmap_foreach_value(refs->packfile, reference, { + git__free(reference); + }); + + git_strmap_free(refs->packfile); + } +} + +static void refdb_fs_backend__free(git_refdb_backend *_backend) +{ + refdb_fs_backend *backend; + + assert(_backend); + backend = (refdb_fs_backend *)_backend; + + refcache_free(&backend->refcache); + git__free(backend); +} + +int git_refdb_backend_fs( + git_refdb_backend **backend_out, + git_repository *repository, + git_refdb *refdb) +{ + 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; + + backend->parent.exists = &refdb_fs_backend__exists; + backend->parent.lookup = &refdb_fs_backend__lookup; + backend->parent.foreach = &refdb_fs_backend__foreach; + backend->parent.write = &refdb_fs_backend__write; + backend->parent.delete = &refdb_fs_backend__delete; + backend->parent.compress = &refdb_fs_backend__compress; + backend->parent.free = &refdb_fs_backend__free; + + *backend_out = (git_refdb_backend *)backend; + return 0; +} diff --git a/src/refdb_fs.h b/src/refdb_fs.h new file mode 100644 index 000000000..79e296833 --- /dev/null +++ b/src/refdb_fs.h @@ -0,0 +1,15 @@ +/* + * 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_refdb_fs_h__ +#define INCLUDE_refdb_fs_h__ + +typedef struct { + git_strmap *packfile; + time_t packfile_time; +} git_refcache; + +#endif diff --git a/src/reflog.c b/src/reflog.c index 3ea073e65..8c133fe53 100644 --- a/src/reflog.c +++ b/src/reflog.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -10,7 +10,7 @@ #include "filebuf.h" #include "signature.h" -static int reflog_init(git_reflog **reflog, git_reference *ref) +static int reflog_init(git_reflog **reflog, const git_reference *ref) { git_reflog *log; @@ -28,66 +28,68 @@ static int reflog_init(git_reflog **reflog, git_reference *ref) return -1; } + log->owner = git_reference_owner(ref); *reflog = log; return 0; } -static int reflog_write(const char *log_path, const char *oid_old, - const char *oid_new, const git_signature *committer, - const char *msg) +static int serialize_reflog_entry( + git_buf *buf, + const git_oid *oid_old, + const git_oid *oid_new, + const git_signature *committer, + const char *msg) { - int error; - git_buf log = GIT_BUF_INIT; - git_filebuf fbuf = GIT_FILEBUF_INIT; - bool trailing_newline = false; + char raw_old[GIT_OID_HEXSZ+1]; + char raw_new[GIT_OID_HEXSZ+1]; - assert(log_path && oid_old && oid_new && committer); + git_oid_tostr(raw_old, GIT_OID_HEXSZ+1, oid_old); + git_oid_tostr(raw_new, GIT_OID_HEXSZ+1, oid_new); - if (msg) { - const char *newline = strchr(msg, '\n'); - if (newline) { - if (*(newline + 1) == '\0') - trailing_newline = true; - else { - giterr_set(GITERR_INVALID, "Reflog message cannot contain newline"); - return -1; - } - } - } + git_buf_clear(buf); - git_buf_puts(&log, oid_old); - git_buf_putc(&log, ' '); + git_buf_puts(buf, raw_old); + git_buf_putc(buf, ' '); + git_buf_puts(buf, raw_new); - git_buf_puts(&log, oid_new); + git_signature__writebuf(buf, " ", committer); - git_signature__writebuf(&log, " ", committer); - git_buf_truncate(&log, log.size - 1); /* drop LF */ + /* drop trailing LF */ + git_buf_rtrim(buf); if (msg) { - git_buf_putc(&log, '\t'); - git_buf_puts(&log, msg); + git_buf_putc(buf, '\t'); + git_buf_puts(buf, msg); } - if (!trailing_newline) - git_buf_putc(&log, '\n'); + git_buf_putc(buf, '\n'); - if (git_buf_oom(&log)) { - git_buf_free(&log); - return -1; - } + return git_buf_oom(buf); +} - error = git_filebuf_open(&fbuf, log_path, GIT_FILEBUF_APPEND); - if (!error) { - if ((error = git_filebuf_write(&fbuf, log.ptr, log.size)) < 0) - git_filebuf_cleanup(&fbuf); - else - error = git_filebuf_commit(&fbuf, GIT_REFLOG_FILE_MODE); - } +static int reflog_entry_new(git_reflog_entry **entry) +{ + git_reflog_entry *e; - git_buf_free(&log); + assert(entry); - return error; + e = git__malloc(sizeof(git_reflog_entry)); + GITERR_CHECK_ALLOC(e); + + memset(e, 0, sizeof(git_reflog_entry)); + + *entry = e; + + return 0; +} + +static void reflog_entry_free(git_reflog_entry *entry) +{ + git_signature_free(entry->committer); + + git__free(entry->msg); + git__free(entry); } static int reflog_parse(git_reflog *log, const char *buf, size_t buf_size) @@ -105,8 +107,8 @@ static int reflog_parse(git_reflog *log, const char *buf, size_t buf_size) } while (0) while (buf_size > GIT_REFLOG_SIZE_MIN) { - entry = git__malloc(sizeof(git_reflog_entry)); - GITERR_CHECK_ALLOC(entry); + if (reflog_entry_new(&entry) < 0) + return -1; entry->committer = git__malloc(sizeof(git_signature)); GITERR_CHECK_ALLOC(entry->committer); @@ -153,25 +155,24 @@ static int reflog_parse(git_reflog *log, const char *buf, size_t buf_size) #undef seek_forward fail: - if (entry) { - git__free(entry->committer); - git__free(entry); - } + if (entry) + reflog_entry_free(entry); + return -1; } void git_reflog_free(git_reflog *reflog) { - unsigned int i; + size_t i; git_reflog_entry *entry; + if (reflog == NULL) + return; + for (i=0; i < reflog->entries.length; i++) { entry = git_vector_get(&reflog->entries, i); - git_signature_free(entry->committer); - - git__free(entry->msg); - git__free(entry); + reflog_entry_free(entry); } git_vector_free(&reflog->entries); @@ -179,110 +180,230 @@ void git_reflog_free(git_reflog *reflog) git__free(reflog); } -int git_reflog_read(git_reflog **reflog, git_reference *ref) +static int retrieve_reflog_path(git_buf *path, const git_reference *ref) { - int error; + return git_buf_join_n(path, '/', 3, + git_reference_owner(ref)->path_repository, GIT_REFLOG_DIR, ref->name); +} + +static int create_new_reflog_file(const char *filepath) +{ + int fd, error; + + if ((error = git_futils_mkpath2file(filepath, GIT_REFLOG_DIR_MODE)) < 0) + return error; + + if ((fd = p_open(filepath, + O_WRONLY | O_CREAT | O_TRUNC, + GIT_REFLOG_FILE_MODE)) < 0) + return -1; + + return p_close(fd); +} + +int git_reflog_read(git_reflog **reflog, const git_reference *ref) +{ + int error = -1; git_buf log_path = GIT_BUF_INIT; git_buf log_file = GIT_BUF_INIT; git_reflog *log = NULL; + assert(reflog && ref); + *reflog = NULL; if (reflog_init(&log, ref) < 0) return -1; - error = git_buf_join_n(&log_path, '/', 3, - ref->owner->path_repository, GIT_REFLOG_DIR, ref->name); + if (retrieve_reflog_path(&log_path, ref) < 0) + goto cleanup; + + error = git_futils_readbuffer(&log_file, git_buf_cstr(&log_path)); + if (error < 0 && error != GIT_ENOTFOUND) + goto cleanup; - if (!error) - error = git_futils_readbuffer(&log_file, log_path.ptr); + if ((error == GIT_ENOTFOUND) && + ((error = create_new_reflog_file(git_buf_cstr(&log_path))) < 0)) + goto cleanup; - if (!error) - error = reflog_parse(log, log_file.ptr, log_file.size); + if ((error = reflog_parse(log, + git_buf_cstr(&log_file), git_buf_len(&log_file))) < 0) + goto cleanup; - if (!error) - *reflog = log; - else - git_reflog_free(log); + *reflog = log; + goto success; + +cleanup: + git_reflog_free(log); +success: git_buf_free(&log_file); git_buf_free(&log_path); return error; } -int git_reflog_write(git_reference *ref, const git_oid *oid_old, - const git_signature *committer, const char *msg) +int git_reflog_write(git_reflog *reflog) { - int error; - char old[GIT_OID_HEXSZ+1]; - char new[GIT_OID_HEXSZ+1]; + int error = -1; + unsigned int i; + git_reflog_entry *entry; git_buf log_path = GIT_BUF_INIT; - git_reference *r; - const git_oid *oid; + git_buf log = GIT_BUF_INIT; + git_filebuf fbuf = GIT_FILEBUF_INIT; - if ((error = git_reference_resolve(&r, ref)) < 0) - return error; + assert(reflog); - oid = git_reference_oid(r); - if (oid == NULL) { - giterr_set(GITERR_REFERENCE, - "Failed to write reflog. Cannot resolve reference `%s`", r->name); - git_reference_free(r); + if (git_buf_join_n(&log_path, '/', 3, + git_repository_path(reflog->owner), GIT_REFLOG_DIR, reflog->ref_name) < 0) return -1; + + if (!git_path_isfile(git_buf_cstr(&log_path))) { + giterr_set(GITERR_INVALID, + "Log file for reference '%s' doesn't exist.", reflog->ref_name); + goto cleanup; + } + + if ((error = git_filebuf_open(&fbuf, git_buf_cstr(&log_path), 0)) < 0) + goto cleanup; + + git_vector_foreach(&reflog->entries, i, entry) { + if (serialize_reflog_entry(&log, &(entry->oid_old), &(entry->oid_cur), entry->committer, entry->msg) < 0) + goto cleanup; + + if ((error = git_filebuf_write(&fbuf, log.ptr, log.size)) < 0) + goto cleanup; } - git_oid_tostr(new, GIT_OID_HEXSZ+1, oid); + error = git_filebuf_commit(&fbuf, GIT_REFLOG_FILE_MODE); + goto success; - git_reference_free(r); +cleanup: + git_filebuf_cleanup(&fbuf); + +success: + git_buf_free(&log); + git_buf_free(&log_path); + return error; +} + +int git_reflog_append(git_reflog *reflog, const git_oid *new_oid, + const git_signature *committer, const char *msg) +{ + git_reflog_entry *entry; + const git_reflog_entry *previous; + const char *newline; - error = git_buf_join_n(&log_path, '/', 3, - ref->owner->path_repository, GIT_REFLOG_DIR, ref->name); - if (error < 0) + assert(reflog && new_oid && committer); + + if (reflog_entry_new(&entry) < 0) + return -1; + + if ((entry->committer = git_signature_dup(committer)) == NULL) goto cleanup; - if (git_path_exists(log_path.ptr) == false) { - error = git_futils_mkpath2file(log_path.ptr, GIT_REFLOG_DIR_MODE); - } else if (git_path_isfile(log_path.ptr) == false) { - giterr_set(GITERR_REFERENCE, - "Failed to write reflog. `%s` is directory", log_path.ptr); - error = -1; - } else if (oid_old == NULL) { - giterr_set(GITERR_REFERENCE, - "Failed to write reflog. Old OID cannot be NULL for existing reference"); - error = -1; + if (msg != NULL) { + if ((entry->msg = git__strdup(msg)) == NULL) + goto cleanup; + + newline = strchr(msg, '\n'); + + if (newline) { + if (newline[1] != '\0') { + giterr_set(GITERR_INVALID, "Reflog message cannot contain newline"); + goto cleanup; + } + + entry->msg[newline - msg] = '\0'; + } } - if (error < 0) - goto cleanup; - if (oid_old) - git_oid_tostr(old, sizeof(old), oid_old); + previous = git_reflog_entry_byindex(reflog, 0); + + if (previous == NULL) + git_oid_fromstr(&entry->oid_old, GIT_OID_HEX_ZERO); else - p_snprintf(old, sizeof(old), "%0*d", GIT_OID_HEXSZ, 0); + git_oid_cpy(&entry->oid_old, &previous->oid_cur); + + git_oid_cpy(&entry->oid_cur, new_oid); - error = reflog_write(log_path.ptr, old, new, committer, msg); + if (git_vector_insert(&reflog->entries, entry) < 0) + goto cleanup; + + return 0; cleanup: - git_buf_free(&log_path); - return error; + reflog_entry_free(entry); + return -1; } int git_reflog_rename(git_reference *ref, const char *new_name) { - int error; + int error = 0, fd; git_buf old_path = GIT_BUF_INIT; git_buf new_path = GIT_BUF_INIT; + git_buf temp_path = GIT_BUF_INIT; + git_buf normalized = GIT_BUF_INIT; - if (!git_buf_join_n(&old_path, '/', 3, ref->owner->path_repository, - GIT_REFLOG_DIR, ref->name) && - !git_buf_join_n(&new_path, '/', 3, ref->owner->path_repository, - GIT_REFLOG_DIR, new_name)) - error = p_rename(git_buf_cstr(&old_path), git_buf_cstr(&new_path)); - else + assert(ref && new_name); + + if ((error = git_reference__normalize_name( + &normalized, new_name, GIT_REF_FORMAT_ALLOW_ONELEVEL)) < 0) + return error; + + if (git_buf_joinpath(&temp_path, git_reference_owner(ref)->path_repository, GIT_REFLOG_DIR) < 0) + return -1; + + if (git_buf_joinpath(&old_path, git_buf_cstr(&temp_path), ref->name) < 0) + return -1; + + if (git_buf_joinpath(&new_path, git_buf_cstr(&temp_path), git_buf_cstr(&normalized)) < 0) + return -1; + + /* + * Move the reflog to a temporary place. This two-phase renaming is required + * in order to cope with funny renaming use cases when one tries to move a reference + * to a partially colliding namespace: + * - a/b -> a/b/c + * - a/b/c/d -> a/b/c + */ + if (git_buf_joinpath(&temp_path, git_buf_cstr(&temp_path), "temp_reflog") < 0) + return -1; + + if ((fd = git_futils_mktmp(&temp_path, git_buf_cstr(&temp_path))) < 0) { + error = -1; + goto cleanup; + } + + p_close(fd); + + if (p_rename(git_buf_cstr(&old_path), git_buf_cstr(&temp_path)) < 0) { + giterr_set(GITERR_OS, "Failed to rename reflog for %s", new_name); + error = -1; + goto cleanup; + } + + if (git_path_isdir(git_buf_cstr(&new_path)) && + (git_futils_rmdir_r(git_buf_cstr(&new_path), NULL, GIT_RMDIR_SKIP_NONEMPTY) < 0)) { + error = -1; + goto cleanup; + } + + if (git_futils_mkpath2file(git_buf_cstr(&new_path), GIT_REFLOG_DIR_MODE) < 0) { + error = -1; + goto cleanup; + } + + if (p_rename(git_buf_cstr(&temp_path), git_buf_cstr(&new_path)) < 0) { + giterr_set(GITERR_OS, "Failed to rename reflog for %s", new_name); error = -1; + } +cleanup: + git_buf_free(&temp_path); git_buf_free(&old_path); git_buf_free(&new_path); + git_buf_free(&normalized); return error; } @@ -292,8 +413,7 @@ int git_reflog_delete(git_reference *ref) int error; git_buf path = GIT_BUF_INIT; - error = git_buf_join_n( - &path, '/', 3, ref->owner->path_repository, GIT_REFLOG_DIR, ref->name); + error = retrieve_reflog_path(&path, ref); if (!error && git_path_exists(path.ptr)) error = p_unlink(path.ptr); @@ -303,38 +423,99 @@ int git_reflog_delete(git_reference *ref) return error; } -unsigned int git_reflog_entrycount(git_reflog *reflog) +size_t git_reflog_entrycount(git_reflog *reflog) { assert(reflog); return reflog->entries.length; } -const git_reflog_entry * git_reflog_entry_byindex(git_reflog *reflog, unsigned int idx) +GIT_INLINE(size_t) reflog_inverse_index(size_t idx, size_t total) +{ + return (total - 1) - idx; +} + +const git_reflog_entry * git_reflog_entry_byindex(git_reflog *reflog, size_t idx) { assert(reflog); - return git_vector_get(&reflog->entries, idx); + + if (idx >= reflog->entries.length) + return NULL; + + return git_vector_get( + &reflog->entries, reflog_inverse_index(idx, reflog->entries.length)); } -const git_oid * git_reflog_entry_oidold(const git_reflog_entry *entry) +const git_oid * git_reflog_entry_id_old(const git_reflog_entry *entry) { assert(entry); return &entry->oid_old; } -const git_oid * git_reflog_entry_oidnew(const git_reflog_entry *entry) +const git_oid * git_reflog_entry_id_new(const git_reflog_entry *entry) { assert(entry); return &entry->oid_cur; } -git_signature * git_reflog_entry_committer(const git_reflog_entry *entry) +const git_signature * git_reflog_entry_committer(const git_reflog_entry *entry) { assert(entry); return entry->committer; } -char * git_reflog_entry_msg(const git_reflog_entry *entry) +const char * git_reflog_entry_message(const git_reflog_entry *entry) { assert(entry); return entry->msg; } + +int git_reflog_drop( + git_reflog *reflog, + size_t idx, + int rewrite_previous_entry) +{ + size_t entrycount; + git_reflog_entry *entry, *previous; + + assert(reflog); + + entrycount = git_reflog_entrycount(reflog); + + entry = (git_reflog_entry *)git_reflog_entry_byindex(reflog, idx); + + if (entry == NULL) + return GIT_ENOTFOUND; + + reflog_entry_free(entry); + + if (git_vector_remove( + &reflog->entries, reflog_inverse_index(idx, entrycount)) < 0) + return -1; + + if (!rewrite_previous_entry) + return 0; + + /* No need to rewrite anything when removing the most recent entry */ + if (idx == 0) + return 0; + + /* Have the latest entry just been dropped? */ + if (entrycount == 1) + return 0; + + entry = (git_reflog_entry *)git_reflog_entry_byindex(reflog, idx - 1); + + /* If the oldest entry has just been removed... */ + if (idx == entrycount - 1) { + /* ...clear the oid_old member of the "new" oldest entry */ + if (git_oid_fromstr(&entry->oid_old, GIT_OID_HEX_ZERO) < 0) + return -1; + + return 0; + } + + previous = (git_reflog_entry *)git_reflog_entry_byindex(reflog, idx); + git_oid_cpy(&entry->oid_old, &previous->oid_cur); + + return 0; +} diff --git a/src/reflog.h b/src/reflog.h index 33cf0776c..9444ebd10 100644 --- a/src/reflog.h +++ b/src/reflog.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -28,6 +28,7 @@ struct git_reflog_entry { struct git_reflog { char *ref_name; + git_repository *owner; git_vector entries; }; diff --git a/src/refs.c b/src/refs.c index 1ef3e13a4..b1f679632 100644 --- a/src/refs.c +++ b/src/refs.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -11,9 +11,15 @@ #include "fileops.h" #include "pack.h" #include "reflog.h" +#include "refdb.h" #include <git2/tag.h> #include <git2/object.h> +#include <git2/oid.h> +#include <git2/branch.h> +#include <git2/refs.h> +#include <git2/refdb.h> +#include <git2/refdb_backend.h> GIT__USE_STRMAP; @@ -25,789 +31,55 @@ enum { GIT_PACKREF_WAS_LOOSE = 2 }; -struct packref { - git_oid oid; - git_oid peel; - char flags; - char name[GIT_FLEX_ARRAY]; -}; - -static int reference_read( - git_buf *file_content, - time_t *mtime, - const char *repo_path, - const char *ref_name, - int *updated); - -/* loose refs */ -static int loose_parse_symbolic(git_reference *ref, git_buf *file_content); -static int loose_parse_oid(git_oid *ref, git_buf *file_content); -static int loose_lookup(git_reference *ref); -static int loose_lookup_to_packfile(struct packref **ref_out, - git_repository *repo, const char *name); -static int loose_write(git_reference *ref); - -/* packed refs */ -static int packed_parse_peel(struct packref *tag_ref, - const char **buffer_out, const char *buffer_end); -static int packed_parse_oid(struct packref **ref_out, - const char **buffer_out, const char *buffer_end); -static int packed_load(git_repository *repo); -static int packed_loadloose(git_repository *repository); -static int packed_write_ref(struct packref *ref, git_filebuf *file); -static int packed_find_peel(git_repository *repo, struct packref *ref); -static int packed_remove_loose(git_repository *repo, git_vector *packing_list); -static int packed_sort(const void *a, const void *b); -static int packed_lookup(git_reference *ref); -static int packed_write(git_repository *repo); - -/* internal helpers */ -static int reference_path_available(git_repository *repo, - const char *ref, const char *old_ref); -static int reference_delete(git_reference *ref); -static int reference_lookup(git_reference *ref); - -/* name normalization */ -static int normalize_name(char *buffer_out, size_t out_size, - const char *name, int is_oid_ref); - - -void git_reference_free(git_reference *reference) -{ - if (reference == NULL) - return; - - git__free(reference->name); - reference->name = NULL; - - if (reference->flags & GIT_REF_SYMBOLIC) { - git__free(reference->target.symbolic); - reference->target.symbolic = NULL; - } - - git__free(reference); -} - -static int reference_alloc( - git_reference **ref_out, - git_repository *repo, - const char *name) -{ - git_reference *reference = NULL; - - assert(ref_out && repo && name); - - reference = git__malloc(sizeof(git_reference)); - GITERR_CHECK_ALLOC(reference); - - memset(reference, 0x0, sizeof(git_reference)); - reference->owner = repo; - - reference->name = git__strdup(name); - GITERR_CHECK_ALLOC(reference->name); - - *ref_out = reference; - return 0; -} - -static int reference_read( - git_buf *file_content, - time_t *mtime, - const char *repo_path, - const char *ref_name, - int *updated) -{ - git_buf path = GIT_BUF_INIT; - int result; - - assert(file_content && repo_path && ref_name); - - /* 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, updated); - git_buf_free(&path); - return result; -} - -static int loose_parse_symbolic(git_reference *ref, git_buf *file_content) -{ - const unsigned int header_len = (unsigned int)strlen(GIT_SYMREF); - const char *refname_start; - char *eol; - - refname_start = (const char *)file_content->ptr; - - if (git_buf_len(file_content) < header_len + 1) - goto corrupt; - - /* - * Assume we have already checked for the header - * before calling this function - */ - refname_start += header_len; - - ref->target.symbolic = git__strdup(refname_start); - GITERR_CHECK_ALLOC(ref->target.symbolic); - - /* remove newline at the end of file */ - eol = strchr(ref->target.symbolic, '\n'); - if (eol == NULL) - goto corrupt; - - *eol = '\0'; - if (eol[-1] == '\r') - eol[-1] = '\0'; - - return 0; - -corrupt: - giterr_set(GITERR_REFERENCE, "Corrupted loose reference file"); - return -1; -} - -static int loose_parse_oid(git_oid *oid, git_buf *file_content) -{ - char *buffer; - - buffer = (char *)file_content->ptr; - - /* File format: 40 chars (OID) + newline */ - if (git_buf_len(file_content) < GIT_OID_HEXSZ + 1) - goto corrupt; - - if (git_oid_fromstr(oid, buffer) < 0) - goto corrupt; - - buffer = buffer + GIT_OID_HEXSZ; - if (*buffer == '\r') - buffer++; - - if (*buffer != '\n') - goto corrupt; - - return 0; - -corrupt: - giterr_set(GITERR_REFERENCE, "Corrupted loose reference file"); - return -1; -} - -static git_ref_t loose_guess_rtype(const git_buf *full_path) -{ - git_buf ref_file = GIT_BUF_INIT; - git_ref_t type; - - type = GIT_REF_INVALID; - - 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; - } - - git_buf_free(&ref_file); - return type; -} - -static int loose_lookup(git_reference *ref) -{ - int result, updated; - git_buf ref_file = GIT_BUF_INIT; - - result = reference_read(&ref_file, &ref->mtime, - ref->owner->path_repository, ref->name, &updated); - - if (result < 0) - return result; - - if (!updated) - return 0; - - if (ref->flags & GIT_REF_SYMBOLIC) { - git__free(ref->target.symbolic); - ref->target.symbolic = NULL; - } - - ref->flags = 0; - - if (git__prefixcmp((const char *)(ref_file.ptr), GIT_SYMREF) == 0) { - ref->flags |= GIT_REF_SYMBOLIC; - result = loose_parse_symbolic(ref, &ref_file); - } else { - ref->flags |= GIT_REF_OID; - result = loose_parse_oid(&ref->target.oid, &ref_file); - } - - git_buf_free(&ref_file); - return result; -} - -static int loose_lookup_to_packfile( - struct packref **ref_out, - git_repository *repo, - const char *name) -{ - git_buf ref_file = GIT_BUF_INIT; - struct packref *ref = NULL; - size_t name_len; - - *ref_out = NULL; - - if (reference_read(&ref_file, NULL, repo->path_repository, name, NULL) < 0) - return -1; - - name_len = strlen(name); - ref = git__malloc(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) { - git_buf_free(&ref_file); - git__free(ref); - return -1; - } - - ref->flags = GIT_PACKREF_WAS_LOOSE; - - *ref_out = ref; - git_buf_free(&ref_file); - return 0; -} - -static int loose_write(git_reference *ref) -{ - git_filebuf file = GIT_FILEBUF_INIT; - git_buf ref_path = GIT_BUF_INIT; - struct stat st; - - if (git_buf_joinpath(&ref_path, ref->owner->path_repository, ref->name) < 0) - return -1; - - /* Remove a possibly existing empty directory hierarchy - * which name would collide with the reference name - */ - if (git_path_isdir(git_buf_cstr(&ref_path)) && - (git_futils_rmdir_r(git_buf_cstr(&ref_path), GIT_DIRREMOVAL_ONLY_EMPTY_DIRS) < 0)) { - git_buf_free(&ref_path); - return -1; - } - - if (git_filebuf_open(&file, ref_path.ptr, GIT_FILEBUF_FORCE) < 0) { - git_buf_free(&ref_path); - return -1; - } - - git_buf_free(&ref_path); - - if (ref->flags & GIT_REF_OID) { - char oid[GIT_OID_HEXSZ + 1]; - - git_oid_fmt(oid, &ref->target.oid); - oid[GIT_OID_HEXSZ] = '\0'; - - git_filebuf_printf(&file, "%s\n", oid); - - } else if (ref->flags & GIT_REF_SYMBOLIC) { - git_filebuf_printf(&file, GIT_SYMREF "%s\n", ref->target.symbolic); - } else { - assert(0); /* don't let this happen */ - } - - if (p_stat(ref_path.ptr, &st) == 0) - ref->mtime = st.st_mtime; - - return git_filebuf_commit(&file, GIT_REFS_FILE_MODE); -} - -static int packed_parse_peel( - struct packref *tag_ref, - const char **buffer_out, - const char *buffer_end) -{ - const char *buffer = *buffer_out + 1; - - assert(buffer[-1] == '^'); - - /* Ensure it's not the first entry of the file */ - 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; - - /* Is this a valid object id? */ - if (git_oid_fromstr(&tag_ref->peel, buffer) < 0) - goto corrupt; - - buffer = buffer + GIT_OID_HEXSZ; - if (*buffer == '\r') - buffer++; - - if (*buffer != '\n') - goto corrupt; - - *buffer_out = buffer + 1; - return 0; - -corrupt: - giterr_set(GITERR_REFERENCE, "The packed references file is corrupted"); - return -1; -} - -static int packed_parse_oid( - struct packref **ref_out, - const char **buffer_out, - const char *buffer_end) -{ - struct packref *ref = NULL; - - const char *buffer = *buffer_out; - const char *refname_begin, *refname_end; - - size_t refname_len; - git_oid id; - - refname_begin = (buffer + GIT_OID_HEXSZ + 1); - if (refname_begin >= buffer_end || refname_begin[-1] != ' ') - goto corrupt; - - /* Is this a valid object id? */ - if (git_oid_fromstr(&id, buffer) < 0) - goto corrupt; - - refname_end = memchr(refname_begin, '\n', buffer_end - refname_begin); - if (refname_end == NULL) - goto corrupt; - - if (refname_end[-1] == '\r') - refname_end--; - - refname_len = refname_end - refname_begin; - - ref = git__malloc(sizeof(struct packref) + refname_len + 1); - GITERR_CHECK_ALLOC(ref); - - memcpy(ref->name, refname_begin, refname_len); - ref->name[refname_len] = 0; - - git_oid_cpy(&ref->oid, &id); - - ref->flags = 0; - - *ref_out = ref; - *buffer_out = refname_end + 1; - - return 0; - -corrupt: - git__free(ref); - giterr_set(GITERR_REFERENCE, "The packed references file is corrupted"); - return -1; -} - -static int packed_load(git_repository *repo) -{ - int result, updated; - git_buf packfile = GIT_BUF_INIT; - const char *buffer_start, *buffer_end; - git_refcache *ref_cache = &repo->references; - - /* First we make sure we have allocated the hash table */ - if (ref_cache->packfile == NULL) { - ref_cache->packfile = git_strmap_alloc(); - GITERR_CHECK_ALLOC(ref_cache->packfile); - } - - result = reference_read(&packfile, &ref_cache->packfile_time, - repo->path_repository, GIT_PACKEDREFS_FILE, &updated); - - /* - * If we couldn't find the file, we need to clear the table and - * return. On any other error, we return that error. If everything - * went fine and the file wasn't updated, then there's nothing new - * for us here, so just return. Anything else means we need to - * refresh the packed refs. - */ - if (result == GIT_ENOTFOUND) { - git_strmap_clear(ref_cache->packfile); - return 0; - } - - if (result < 0) - return -1; - - if (!updated) - return 0; - - /* - * At this point, we want to refresh the packed refs. We already - * have the contents in our buffer. - */ - git_strmap_clear(ref_cache->packfile); - - buffer_start = (const char *)packfile.ptr; - buffer_end = (const char *)(buffer_start) + packfile.size; - while (buffer_start < buffer_end && buffer_start[0] == '#') { - buffer_start = strchr(buffer_start, '\n'); - if (buffer_start == NULL) - goto parse_failed; - - buffer_start++; - } - - while (buffer_start < buffer_end) { - int err; - struct packref *ref = NULL; - - if (packed_parse_oid(&ref, &buffer_start, buffer_end) < 0) - goto parse_failed; - - if (buffer_start[0] == '^') { - if (packed_parse_peel(ref, &buffer_start, buffer_end) < 0) - goto parse_failed; - } - - git_strmap_insert(ref_cache->packfile, ref->name, ref, err); - if (err < 0) - goto parse_failed; - } - - git_buf_free(&packfile); - return 0; - -parse_failed: - git_strmap_free(ref_cache->packfile); - ref_cache->packfile = NULL; - git_buf_free(&packfile); - return -1; -} - - -struct dirent_list_data { - git_repository *repo; - size_t repo_path_len; - unsigned int list_flags; - - int (*callback)(const char *, void *); - void *callback_payload; -}; - -static int _dirent_loose_listall(void *_data, git_buf *full_path) -{ - struct dirent_list_data *data = (struct dirent_list_data *)_data; - const char *file_path = full_path->ptr + data->repo_path_len; - - if (git_path_isdir(full_path->ptr) == true) - return git_path_direach(full_path, _dirent_loose_listall, _data); - - /* do not add twice a reference that exists already in the packfile */ - if ((data->list_flags & GIT_REF_PACKED) != 0 && - git_strmap_exists(data->repo->references.packfile, file_path)) - return 0; - - if (data->list_flags != GIT_REF_LISTALL) { - if ((data->list_flags & loose_guess_rtype(full_path)) == 0) - return 0; /* we are filtering out this reference */ - } - - return data->callback(file_path, data->callback_payload); -} - -static int _dirent_loose_load(void *data, git_buf *full_path) -{ - git_repository *repository = (git_repository *)data; - void *old_ref = NULL; - struct packref *ref; - const char *file_path; - int err; - - if (git_path_isdir(full_path->ptr) == true) - return git_path_direach(full_path, _dirent_loose_load, repository); - - file_path = full_path->ptr + strlen(repository->path_repository); - - if (loose_lookup_to_packfile(&ref, repository, file_path) < 0) - return -1; - - git_strmap_insert2( - repository->references.packfile, ref->name, ref, old_ref, err); - if (err < 0) { - git__free(ref); - return -1; - } - - git__free(old_ref); - return 0; -} - -/* - * Load all the loose references from the repository - * into the in-memory Packfile, and build a vector with - * all the references so it can be written back to - * disk. - */ -static int packed_loadloose(git_repository *repository) +git_reference *git_reference__alloc( + git_refdb *refdb, + const char *name, + const git_oid *oid, + const char *symbolic) { - git_buf refs_path = GIT_BUF_INIT; - int result; - - /* the packfile must have been previously loaded! */ - assert(repository->references.packfile); - - if (git_buf_joinpath(&refs_path, repository->path_repository, GIT_REFS_DIR) < 0) - return -1; - - /* - * Load all the loose files from disk into the Packfile table. - * This will overwrite any old packed entries with their - * updated loose versions - */ - result = git_path_direach(&refs_path, _dirent_loose_load, repository); - git_buf_free(&refs_path); - - return result; -} + git_reference *ref; + size_t namelen; -/* - * Write a single reference into a packfile - */ -static int packed_write_ref(struct packref *ref, git_filebuf *file) -{ - char oid[GIT_OID_HEXSZ + 1]; + assert(refdb && name && ((oid && !symbolic) || (!oid && symbolic))); - git_oid_fmt(oid, &ref->oid); - oid[GIT_OID_HEXSZ] = 0; + namelen = strlen(name); - /* - * For references that peel to an object in the repo, we must - * write the resulting peel on a separate line, e.g. - * - * 6fa8a902cc1d18527e1355773c86721945475d37 refs/tags/libgit2-0.4 - * ^2ec0cb7959b0bf965d54f95453f5b4b34e8d3100 - * - * This obviously only applies to tags. - * The required peels have already been loaded into `ref->peel_target`. - */ - if (ref->flags & GIT_PACKREF_HAS_PEEL) { - char peel[GIT_OID_HEXSZ + 1]; - git_oid_fmt(peel, &ref->peel); - peel[GIT_OID_HEXSZ] = 0; + if ((ref = git__calloc(1, sizeof(git_reference) + namelen + 1)) == NULL) + return NULL; - if (git_filebuf_printf(file, "%s %s\n^%s\n", oid, ref->name, peel) < 0) - return -1; + if (oid) { + ref->type = GIT_REF_OID; + git_oid_cpy(&ref->target.oid, oid); } else { - if (git_filebuf_printf(file, "%s %s\n", oid, ref->name) < 0) - return -1; - } - - return 0; -} - -/* - * Find out what object this reference resolves to. - * - * For references that point to a 'big' tag (e.g. an - * actual tag object on the repository), we need to - * cache on the packfile the OID of the object to - * which that 'big tag' is pointing to. - */ -static int packed_find_peel(git_repository *repo, 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) - return 0; - - /* - * Find the tagged object in the repository - */ - if (git_object_lookup(&object, repo, &ref->oid, GIT_OBJ_ANY) < 0) - return -1; - - /* - * If the tagged object is a Tag object, we need to resolve it; - * if the ref is actually a 'weak' ref, we don't need to resolve - * anything. - */ - if (git_object_type(object) == GIT_OBJ_TAG) { - git_tag *tag = (git_tag *)object; - - /* - * Find the object pointed at by this tag - */ - git_oid_cpy(&ref->peel, git_tag_target_oid(tag)); - ref->flags |= GIT_PACKREF_HAS_PEEL; - - /* - * The reference has now cached the resolved OID, and is - * marked at such. When written to the packfile, it'll be - * accompanied by this resolved oid - */ - } - - git_object_free(object); - return 0; -} - -/* - * Remove all loose references - * - * Once we have successfully written a packfile, - * all the loose references that were packed must be - * removed from disk. - * - * This is a dangerous method; make sure the packfile - * is well-written, because we are destructing references - * here otherwise. - */ -static int packed_remove_loose(git_repository *repo, git_vector *packing_list) -{ - unsigned int i; - git_buf full_path = GIT_BUF_INIT; - int failed = 0; - - 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) - continue; - - if (git_buf_joinpath(&full_path, repo->path_repository, ref->name) < 0) - return -1; /* critical; do not try to recover on oom */ - - if (git_path_exists(full_path.ptr) == true && p_unlink(full_path.ptr) < 0) { - if (failed) - continue; + ref->type = GIT_REF_SYMBOLIC; - giterr_set(GITERR_REFERENCE, - "Failed to remove loose reference '%s' after packing: %s", - full_path.ptr, strerror(errno)); - - failed = 1; + if ((ref->target.symbolic = git__strdup(symbolic)) == NULL) { + git__free(ref); + return NULL; } - - /* - * if we fail to remove a single file, this is *not* good, - * but we should keep going and remove as many as possible. - * After we've removed as many files as possible, we return - * the error code anyway. - */ } - git_buf_free(&full_path); - return failed ? -1 : 0; -} - -static int packed_sort(const void *a, const void *b) -{ - const struct packref *ref_a = (const struct packref *)a; - const struct packref *ref_b = (const struct packref *)b; + ref->db = refdb; + memcpy(ref->name, name, namelen + 1); - return strcmp(ref_a->name, ref_b->name); + return ref; } -/* - * Write all the contents in the in-memory packfile to disk. - */ -static int packed_write(git_repository *repo) +void git_reference_free(git_reference *reference) { - git_filebuf pack_file = GIT_FILEBUF_INIT; - unsigned int i; - git_buf pack_file_path = GIT_BUF_INIT; - git_vector packing_list; - unsigned int total_refs; - - assert(repo && repo->references.packfile); - - total_refs = - (unsigned int)git_strmap_num_entries(repo->references.packfile); - - if (git_vector_init(&packing_list, total_refs, packed_sort) < 0) - return -1; - - /* Load all the packfile into a vector */ - { - struct packref *reference; - - /* cannot fail: vector already has the right size */ - git_strmap_foreach_value(repo->references.packfile, reference, { - git_vector_insert(&packing_list, reference); - }); - } - - /* sort the vector so the entries appear sorted on the packfile */ - git_vector_sort(&packing_list); - - /* Now we can open the file! */ - if (git_buf_joinpath(&pack_file_path, repo->path_repository, GIT_PACKEDREFS_FILE) < 0) - goto cleanup_memory; - - if (git_filebuf_open(&pack_file, pack_file_path.ptr, 0) < 0) - goto cleanup_packfile; - - /* Packfiles have a header... apparently - * This is in fact not required, but we might as well print it - * just for kicks */ - if (git_filebuf_printf(&pack_file, "%s\n", GIT_PACKEDREFS_HEADER) < 0) - goto cleanup_packfile; - - for (i = 0; i < packing_list.length; ++i) { - struct packref *ref = (struct packref *)git_vector_get(&packing_list, i); - - if (packed_find_peel(repo, ref) < 0) - goto cleanup_packfile; + if (reference == NULL) + return; - if (packed_write_ref(ref, &pack_file) < 0) - goto cleanup_packfile; + if (reference->type == GIT_REF_SYMBOLIC) { + git__free(reference->target.symbolic); + reference->target.symbolic = NULL; } - /* if we've written all the references properly, we can commit - * the packfile to make the changes effective */ - if (git_filebuf_commit(&pack_file, GIT_PACKEDREFS_FILE_MODE) < 0) - goto cleanup_memory; - - /* when and only when the packfile has been properly written, - * we can go ahead and remove the loose refs */ - if (packed_remove_loose(repo, &packing_list) < 0) - goto cleanup_memory; - - { - struct stat st; - if (p_stat(pack_file_path.ptr, &st) == 0) - repo->references.packfile_time = st.st_mtime; - } - - git_vector_free(&packing_list); - git_buf_free(&pack_file_path); - - /* we're good now */ - return 0; - -cleanup_packfile: - git_filebuf_cleanup(&pack_file); + reference->db = NULL; + reference->type = GIT_REF_INVALID; -cleanup_memory: - git_vector_free(&packing_list); - git_buf_free(&pack_file_path); - - return -1; + git__free(reference); } struct reference_available_t { @@ -843,15 +115,17 @@ static int reference_path_available( const char *ref, const char* old_ref) { + int error; struct reference_available_t data; data.new_ref = ref; data.old_ref = old_ref; data.available = 1; - if (git_reference_foreach(repo, GIT_REF_LISTALL, - _reference_available_cb, (void *)&data) < 0) - return -1; + error = git_reference_foreach( + repo, GIT_REF_LISTALL, _reference_available_cb, (void *)&data); + if (error < 0) + return error; if (!data.available) { giterr_set(GITERR_REFERENCE, @@ -862,28 +136,6 @@ static int reference_path_available( return 0; } -static int reference_exists(int *exists, git_repository *repo, const char *ref_name) -{ - git_buf ref_path = GIT_BUF_INIT; - - if (packed_load(repo) < 0) - return -1; - - if (git_buf_joinpath(&ref_path, repo->path_repository, ref_name) < 0) - return -1; - - if (git_path_isfile(ref_path.ptr) == true || - git_strmap_exists(repo->references.packfile, ref_path.ptr)) - { - *exists = 1; - } else { - *exists = 0; - } - - git_buf_free(&ref_path); - return 0; -} - /* * Check if a reference could be written to disk, based on: * @@ -899,6 +151,11 @@ static int reference_can_write( const char *previous_name, int force) { + 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) @@ -909,7 +166,7 @@ static int reference_can_write( if (!force) { int exists; - if (reference_exists(&exists, repo, refname) < 0) + if (git_refdb_exists(&exists, refdb, refname) < 0) return -1; /* We cannot proceed if the reference already exists and we're not forcing @@ -936,139 +193,9 @@ static int reference_can_write( return 0; } - -static int packed_lookup(git_reference *ref) -{ - struct packref *pack_ref = NULL; - git_strmap *packfile_refs; - khiter_t pos; - - if (packed_load(ref->owner) < 0) - return -1; - - /* maybe the packfile hasn't changed at all, so we don't - * have to re-lookup the reference */ - if ((ref->flags & GIT_REF_PACKED) && - ref->mtime == ref->owner->references.packfile_time) - return 0; - - if (ref->flags & GIT_REF_SYMBOLIC) { - git__free(ref->target.symbolic); - ref->target.symbolic = NULL; - } - - /* Look up on the packfile */ - packfile_refs = ref->owner->references.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; - } - - pack_ref = git_strmap_value_at(packfile_refs, pos); - - ref->flags = GIT_REF_OID | GIT_REF_PACKED; - ref->mtime = ref->owner->references.packfile_time; - git_oid_cpy(&ref->target.oid, &pack_ref->oid); - - return 0; -} - -static int reference_lookup(git_reference *ref) -{ - int result; - - result = loose_lookup(ref); - if (result == 0) - return 0; - - /* only try to lookup this reference on the packfile if it - * wasn't found on the loose refs; not if there was a critical error */ - if (result == GIT_ENOTFOUND) { - giterr_clear(); - result = packed_lookup(ref); - if (result == 0) - return 0; - } - - /* unexpected error; free the reference */ - git_reference_free(ref); - return result; -} - -/* - * Delete a reference. - * This is an internal method; the reference is removed - * from disk or the packfile, but the pointer is not freed - */ -static int reference_delete(git_reference *ref) -{ - int result; - - assert(ref); - - /* If the reference is packed, this is an expensive operation. - * We need to reload the packfile, remove the reference from the - * packing list, and repack */ - if (ref->flags & GIT_REF_PACKED) { - git_strmap *packfile_refs; - struct packref *packref; - khiter_t pos; - - /* load the existing packfile */ - if (packed_load(ref->owner) < 0) - return -1; - - packfile_refs = ref->owner->references.packfile; - pos = git_strmap_lookup_index(packfile_refs, ref->name); - if (!git_strmap_valid_index(packfile_refs, pos)) { - giterr_set(GITERR_REFERENCE, - "Reference %s stopped existing in the packfile", ref->name); - return -1; - } - - packref = git_strmap_value_at(packfile_refs, pos); - git_strmap_delete_at(packfile_refs, pos); - - git__free(packref); - if (packed_write(ref->owner) < 0) - return -1; - - /* If the reference is loose, we can just remove the reference - * from the filesystem */ - } else { - git_reference *ref_in_pack; - git_buf full_path = GIT_BUF_INIT; - - if (git_buf_joinpath(&full_path, ref->owner->path_repository, ref->name) < 0) - return -1; - - result = p_unlink(full_path.ptr); - git_buf_free(&full_path); /* done with path at this point */ - - if (result < 0) { - giterr_set(GITERR_OS, "Failed to unlink '%s'", full_path.ptr); - return -1; - } - - /* When deleting a loose reference, we have to ensure that an older - * packed version of it doesn't exist */ - if (git_reference_lookup(&ref_in_pack, ref->owner, ref->name) == 0) { - assert((ref_in_pack->flags & GIT_REF_PACKED) != 0); - return git_reference_delete(ref_in_pack); - } - - giterr_clear(); - } - - return 0; -} - int git_reference_delete(git_reference *ref) { - int result = reference_delete(ref); - git_reference_free(ref); - return result; + return git_refdb_delete(ref->db, ref); } int git_reference_lookup(git_reference **ref_out, @@ -1077,7 +204,7 @@ int git_reference_lookup(git_reference **ref_out, return git_reference_lookup_resolved(ref_out, repo, name, 0); } -int git_reference_name_to_oid( +int git_reference_name_to_id( git_oid *out, git_repository *repo, const char *name) { int error; @@ -1086,7 +213,7 @@ int git_reference_name_to_oid( if ((error = git_reference_lookup_resolved(&ref, repo, name, -1)) < 0) return error; - git_oid_cpy(out, git_reference_oid(ref)); + git_oid_cpy(out, git_reference_target(ref)); git_reference_free(ref); return 0; } @@ -1097,8 +224,11 @@ int git_reference_lookup_resolved( const char *name, int max_nesting) { - git_reference *scan; - int result, nesting; + char scan_name[GIT_REFNAME_MAX]; + git_ref_t scan_type; + int error = 0, nesting; + git_reference *ref = NULL; + git_refdb *refdb; assert(ref_out && repo && name); @@ -1108,409 +238,294 @@ 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; - scan = git__calloc(1, sizeof(git_reference)); - GITERR_CHECK_ALLOC(scan); - - scan->name = git__calloc(GIT_REFNAME_MAX + 1, sizeof(char)); - GITERR_CHECK_ALLOC(scan->name); - - if ((result = normalize_name(scan->name, GIT_REFNAME_MAX, name, 0)) < 0) { - git_reference_free(scan); - return result; - } - - scan->target.symbolic = git__strdup(scan->name); - GITERR_CHECK_ALLOC(scan->target.symbolic); - - scan->owner = repo; - scan->flags = GIT_REF_SYMBOLIC; + if ((error = git_reference__normalize_name_lax(scan_name, GIT_REFNAME_MAX, name)) < 0) + return error; for (nesting = max_nesting; - nesting >= 0 && (scan->flags & GIT_REF_SYMBOLIC) != 0; + nesting >= 0 && scan_type == GIT_REF_SYMBOLIC; nesting--) { - if (nesting != max_nesting) - strncpy(scan->name, scan->target.symbolic, GIT_REFNAME_MAX); - - scan->mtime = 0; + if (nesting != max_nesting) { + strncpy(scan_name, ref->target.symbolic, GIT_REFNAME_MAX); + git_reference_free(ref); + } - if ((result = reference_lookup(scan)) < 0) - return result; /* lookup git_reference_free on scan already */ + if ((error = git_refdb_lookup(&ref, refdb, scan_name)) < 0) + return error; + + scan_type = ref->type; } - if ((scan->flags & GIT_REF_OID) == 0 && max_nesting != 0) { + if (scan_type != GIT_REF_OID && max_nesting != 0) { giterr_set(GITERR_REFERENCE, "Cannot resolve reference (>%u levels deep)", max_nesting); - git_reference_free(scan); + git_reference_free(ref); return -1; } - *ref_out = scan; + *ref_out = ref; return 0; } /** * Getters */ -git_ref_t git_reference_type(git_reference *ref) -{ - assert(ref); - - if (ref->flags & GIT_REF_OID) - return GIT_REF_OID; - - if (ref->flags & GIT_REF_SYMBOLIC) - return GIT_REF_SYMBOLIC; - - return GIT_REF_INVALID; -} - -int git_reference_is_packed(git_reference *ref) +git_ref_t git_reference_type(const git_reference *ref) { assert(ref); - return !!(ref->flags & GIT_REF_PACKED); + return ref->type; } -const char *git_reference_name(git_reference *ref) +const char *git_reference_name(const git_reference *ref) { assert(ref); return ref->name; } -git_repository *git_reference_owner(git_reference *ref) +git_repository *git_reference_owner(const git_reference *ref) { assert(ref); - return ref->owner; + return ref->db->repo; } -const git_oid *git_reference_oid(git_reference *ref) +const git_oid *git_reference_target(const git_reference *ref) { assert(ref); - if ((ref->flags & GIT_REF_OID) == 0) + if (ref->type != GIT_REF_OID) return NULL; return &ref->target.oid; } -const char *git_reference_target(git_reference *ref) +const char *git_reference_symbolic_target(const git_reference *ref) { assert(ref); - if ((ref->flags & GIT_REF_SYMBOLIC) == 0) + if (ref->type != GIT_REF_SYMBOLIC) return NULL; return ref->target.symbolic; } -int git_reference_create_symbolic( +static int reference__create( git_reference **ref_out, git_repository *repo, const char *name, - const char *target, + const git_oid *oid, + const char *symbolic, int force) { char normalized[GIT_REFNAME_MAX]; + git_refdb *refdb; git_reference *ref = NULL; - - if (normalize_name(normalized, sizeof(normalized), name, 0) < 0) - return -1; - - if (reference_can_write(repo, normalized, NULL, force) < 0) - return -1; - - if (reference_alloc(&ref, repo, normalized) < 0) + 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) + return error; + + if ((ref = git_reference__alloc(refdb, name, oid, symbolic)) == NULL) return -1; - ref->flags |= GIT_REF_SYMBOLIC; - - /* set the target; this will normalize the name automatically - * and write the reference on disk */ - if (git_reference_set_target(ref, target) < 0) { + if ((error = git_refdb_write(refdb, ref)) < 0) { git_reference_free(ref); - return -1; + return error; } - if (ref_out == NULL) { + + if (ref_out == NULL) git_reference_free(ref); - } else { + else *ref_out = ref; - } return 0; } -int git_reference_create_oid( +int git_reference_create( git_reference **ref_out, git_repository *repo, const char *name, - const git_oid *id, + const git_oid *oid, int force) { - git_reference *ref = NULL; - char normalized[GIT_REFNAME_MAX]; + git_odb *odb; + int error = 0; - if (normalize_name(normalized, sizeof(normalized), name, 1) < 0) - return -1; - - if (reference_can_write(repo, normalized, NULL, force) < 0) - return -1; - - if (reference_alloc(&ref, repo, name) < 0) - return -1; - - ref->flags |= GIT_REF_OID; - - /* set the oid; this will write the reference on disk */ - if (git_reference_set_oid(ref, id) < 0) { - git_reference_free(ref); + 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; } - - if (ref_out == NULL) { - git_reference_free(ref); - } else { - *ref_out = ref; - } - - return 0; + + return reference__create(ref_out, repo, name, oid, NULL, force); } -/* - * Change the OID target of a reference. - * - * For both loose and packed references, just change - * the oid in memory and (over)write the file in disk. - * - * We do not repack packed references because of performance - * reasons. - */ -int git_reference_set_oid(git_reference *ref, const git_oid *id) -{ - git_odb *odb = NULL; - if ((ref->flags & GIT_REF_OID) == 0) { - giterr_set(GITERR_REFERENCE, "Cannot set OID on symbolic reference"); - return -1; - } +int git_reference_symbolic_create( + git_reference **ref_out, + git_repository *repo, + const char *name, + const char *target, + int force) +{ + char normalized[GIT_REFNAME_MAX]; + int error = 0; - assert(ref->owner); + assert(repo && name && target); + + if ((error = git_reference__normalize_name_lax( + normalized, sizeof(normalized), target)) < 0) + return error; - if (git_repository_odb__weakptr(&odb, ref->owner) < 0) - return -1; + return reference__create(ref_out, repo, name, NULL, normalized, force); +} - /* Don't let the user create references to OIDs that - * don't exist in the ODB */ - if (!git_odb_exists(odb, id)) { - giterr_set(GITERR_REFERENCE, - "Target OID for the reference doesn't exist on the repository"); +int git_reference_set_target( + git_reference **out, + git_reference *ref, + 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; } - /* Update the OID value on `ref` */ - git_oid_cpy(&ref->target.oid, id); - - /* Write back to disk */ - return loose_write(ref); + return git_reference_create(out, ref->db->repo, ref->name, id, 1); } -/* - * Change the target of a symbolic reference. - * - * This is easy because symrefs cannot be inside - * a pack. We just change the target in memory - * and overwrite the file on disk. - */ -int git_reference_set_target(git_reference *ref, const char *target) +int git_reference_symbolic_set_target( + git_reference **out, + git_reference *ref, + const char *target) { - char normalized[GIT_REFNAME_MAX]; - - if ((ref->flags & GIT_REF_SYMBOLIC) == 0) { + assert(out && ref && target); + + if (ref->type != GIT_REF_SYMBOLIC) { giterr_set(GITERR_REFERENCE, "Cannot set symbolic target on a direct reference"); return -1; } - - if (normalize_name(normalized, sizeof(normalized), target, 0)) - return -1; - - git__free(ref->target.symbolic); - ref->target.symbolic = git__strdup(normalized); - GITERR_CHECK_ALLOC(ref->target.symbolic); - - return loose_write(ref); + + return git_reference_symbolic_create(out, ref->db->repo, ref->name, target, 1); } -int git_reference_rename(git_reference *ref, const char *new_name, int force) +int git_reference_rename( + git_reference **out, + git_reference *ref, + const char *new_name, + int force) { - int result; - git_buf aux_path = GIT_BUF_INIT; + unsigned int normalization_flags; char normalized[GIT_REFNAME_MAX]; - - const char *head_target = NULL; - git_reference *head = NULL; - - if (normalize_name(normalized, sizeof(normalized), - new_name, ref->flags & GIT_REF_OID) < 0) - return -1; - - if (reference_can_write(ref->owner, normalized, ref->name, force) < 0) - return -1; - - /* Initialize path now so we won't get an allocation failure once - * we actually start removing things. */ - if (git_buf_joinpath(&aux_path, ref->owner->path_repository, new_name) < 0) - return -1; - - /* - * Now delete the old ref and remove an possibly existing directory - * named `new_name`. Note that using the internal `reference_delete` - * method deletes the ref from disk but doesn't free the pointer, so - * we can still access the ref's attributes for creating the new one - */ - if (reference_delete(ref) < 0) - goto cleanup; + 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) + return error; /* - * Finally we can create the new reference. + * Create the new reference. */ - if (ref->flags & GIT_REF_SYMBOLIC) { - result = git_reference_create_symbolic( - NULL, ref->owner, new_name, ref->target.symbolic, force); + if (ref->type == GIT_REF_OID) { + oid = &ref->target.oid; + symbolic = NULL; } else { - result = git_reference_create_oid( - NULL, ref->owner, new_name, &ref->target.oid, force); + oid = NULL; + symbolic = ref->target.symbolic; } + + if ((result = git_reference__alloc(ref->db, new_name, oid, symbolic)) == NULL) + return -1; - if (result < 0) - goto rollback; - - /* - * Check if we have to update HEAD. - */ - if (git_reference_lookup(&head, ref->owner, GIT_HEAD_FILE) < 0) { - giterr_set(GITERR_REFERENCE, - "Failed to update HEAD after renaming reference"); - goto cleanup; - } + /* Check if we have to update HEAD. */ + if ((error = git_branch_is_head(ref)) < 0) + goto on_error; - head_target = git_reference_target(head); + should_head_be_updated = (error > 0); - if (head_target && !strcmp(head_target, ref->name)) { - if (git_reference_create_symbolic(&head, ref->owner, "HEAD", new_name, 1) < 0) { - giterr_set(GITERR_REFERENCE, - "Failed to update HEAD after renaming reference"); - goto cleanup; - } + /* 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; + + /* 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) { + giterr_set(GITERR_REFERENCE, "Failed to update HEAD after renaming reference"); + goto on_error; } - /* - * Rename the reflog file. - */ - if (git_buf_join_n(&aux_path, '/', 3, ref->owner->path_repository, GIT_REFLOG_DIR, ref->name) < 0) - goto cleanup; - - if (git_path_exists(aux_path.ptr) == true) { - if (git_reflog_rename(ref, new_name) < 0) - goto cleanup; - } else { - giterr_clear(); + /* 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 && (error = git_reflog_rename(ref, new_name)) < 0) + goto on_error; - /* - * Change the name of the reference given by the user. - */ - git__free(ref->name); - ref->name = git__strdup(new_name); + *out = result; - /* The reference is no longer packed */ - ref->flags &= ~GIT_REF_PACKED; - - git_reference_free(head); - git_buf_free(&aux_path); - return 0; - -cleanup: - git_reference_free(head); - git_buf_free(&aux_path); - return -1; + return error; rollback: - /* - * Try to create the old reference again, ignore failures - */ - if (ref->flags & GIT_REF_SYMBOLIC) - git_reference_create_symbolic( - NULL, ref->owner, ref->name, ref->target.symbolic, 0); - else - git_reference_create_oid( - NULL, ref->owner, ref->name, &ref->target.oid, 0); + git_refdb_write(ref->db, ref); - /* The reference is no longer packed */ - ref->flags &= ~GIT_REF_PACKED; +on_error: + git_reference_free(result); - git_buf_free(&aux_path); - return -1; + return error; } -int git_reference_resolve(git_reference **ref_out, git_reference *ref) +int git_reference_resolve(git_reference **ref_out, const git_reference *ref) { - if (ref->flags & GIT_REF_OID) - return git_reference_lookup(ref_out, ref->owner, ref->name); + 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->owner, ref->target.symbolic, -1); -} - -int git_reference_packall(git_repository *repo) -{ - if (packed_load(repo) < 0 || /* load the existing packfile */ - packed_loadloose(repo) < 0 || /* add all the loose refs */ - packed_write(repo) < 0) /* write back to disk */ - return -1; - - return 0; + return git_reference_lookup_resolved(ref_out, ref->db->repo, + ref->target.symbolic, -1); } int git_reference_foreach( git_repository *repo, unsigned int list_flags, - int (*callback)(const char *, void *), + git_reference_foreach_cb callback, void *payload) { - int result; - struct dirent_list_data data; - git_buf refs_path = GIT_BUF_INIT; - - /* list all the packed references first */ - if (list_flags & GIT_REF_PACKED) { - const char *ref_name; - void *ref; - GIT_UNUSED(ref); - - if (packed_load(repo) < 0) - return -1; - - git_strmap_foreach(repo->references.packfile, ref_name, ref, { - if (callback(ref_name, payload) < 0) - return 0; - }); - } - - /* now list the loose references, trying not to - * duplicate the ref names already in the packed-refs file */ - - data.repo_path_len = strlen(repo->path_repository); - data.list_flags = list_flags; - data.repo = repo; - data.callback = callback; - data.callback_payload = payload; - - if (git_buf_joinpath(&refs_path, repo->path_repository, GIT_REFS_DIR) < 0) - return -1; - - result = git_path_direach(&refs_path, _dirent_loose_listall, &data); - git_buf_free(&refs_path); + git_refdb *refdb; + git_repository_refdb__weakptr(&refdb, repo); - return result; + return git_refdb_foreach(refdb, list_flags, callback, payload); } static int cb__reflist_add(const char *ref, void *data) @@ -1544,26 +559,6 @@ int git_reference_list( return 0; } -int git_reference_reload(git_reference *ref) -{ - return reference_lookup(ref); -} - -void git_repository__refcache_free(git_refcache *refs) -{ - assert(refs); - - if (refs->packfile) { - struct packref *reference; - - git_strmap_foreach_value(refs->packfile, reference, { - git__free(reference); - }); - - git_strmap_free(refs->packfile); - } -} - static int is_valid_ref_char(char ch) { if ((unsigned) ch <= ' ') @@ -1583,112 +578,202 @@ static int is_valid_ref_char(char ch) } } -static int normalize_name( - char *buffer_out, - size_t out_size, - const char *name, - int is_oid_ref) +static int ensure_segment_validity(const char *name) { - const char *name_end, *buffer_out_start; - const char *current; - int contains_a_slash = 0; + const char *current = name; + char prev = '\0'; + const int lock_len = (int)strlen(GIT_FILELOCK_EXTENSION); + int segment_len; - assert(name && buffer_out); + if (*current == '.') + return -1; /* Refname starts with "." */ - buffer_out_start = buffer_out; - current = name; - name_end = name + strlen(name); + for (current = name; ; current++) { + if (*current == '\0' || *current == '/') + break; - /* Terminating null byte */ - out_size--; + if (!is_valid_ref_char(*current)) + return -1; /* Illegal character in refname */ - /* A refname can not be empty */ - if (name_end == name) - goto invalid_name; + if (prev == '.' && *current == '.') + return -1; /* Refname contains ".." */ - /* A refname can not end with a dot or a slash */ - if (*(name_end - 1) == '.' || *(name_end - 1) == '/') - goto invalid_name; + if (prev == '@' && *current == '{') + return -1; /* Refname contains "@{" */ - while (current < name_end && out_size) { - if (!is_valid_ref_char(*current)) - goto invalid_name; + prev = *current; + } + + segment_len = (int)(current - name); + + /* A refname component can not end with ".lock" */ + if (segment_len >= lock_len && + !memcmp(current - lock_len, GIT_FILELOCK_EXTENSION, lock_len)) + return -1; + + return segment_len; +} + +static bool is_all_caps_and_underscore(const char *name, size_t len) +{ + size_t i; + char c; - if (buffer_out > buffer_out_start) { - char prev = *(buffer_out - 1); + assert(name && len > 0); + + for (i = 0; i < len; i++) + { + c = name[i]; + if ((c < 'A' || c > 'Z') && c != '_') + return false; + } - /* A refname can not start with a dot nor contain a double dot */ - if (*current == '.' && ((prev == '.') || (prev == '/'))) - goto invalid_name; + if (*name == '_' || name[len - 1] == '_') + return false; - /* '@{' is forbidden within a refname */ - if (*current == '{' && prev == '@') - goto invalid_name; + return true; +} + +int git_reference__normalize_name( + git_buf *buf, + const char *name, + unsigned int flags) +{ + // Inspired from https://github.com/git/git/blob/f06d47e7e0d9db709ee204ed13a8a7486149f494/refs.c#L36-100 - /* Prevent multiple slashes from being added to the output */ - if (*current == '/' && prev == '/') { - current++; - continue; + char *current; + int segment_len, segments_count = 0, error = GIT_EINVALIDSPEC; + unsigned int process_flags; + bool normalize = (buf != NULL); + assert(name); + + process_flags = flags; + current = (char *)name; + + if (*current == '/') + goto cleanup; + + if (normalize) + git_buf_clear(buf); + + while (true) { + segment_len = ensure_segment_validity(current); + if (segment_len < 0) { + if ((process_flags & GIT_REF_FORMAT_REFSPEC_PATTERN) && + current[0] == '*' && + (current[1] == '\0' || current[1] == '/')) { + /* Accept one wildcard as a full refname component. */ + process_flags &= ~GIT_REF_FORMAT_REFSPEC_PATTERN; + segment_len = 1; + } else + goto cleanup; + } + + if (segment_len > 0) { + if (normalize) { + size_t cur_len = git_buf_len(buf); + + git_buf_joinpath(buf, git_buf_cstr(buf), current); + git_buf_truncate(buf, + cur_len + segment_len + (segments_count ? 1 : 0)); + + if (git_buf_oom(buf)) { + error = -1; + goto cleanup; + } } + + segments_count++; } - if (*current == '/') - contains_a_slash = 1; + /* No empty segment is allowed when not normalizing */ + if (segment_len == 0 && !normalize) + goto cleanup; + + if (current[segment_len] == '\0') + break; - *buffer_out++ = *current++; - out_size--; + current += segment_len + 1; } - if (!out_size) - goto invalid_name; + /* A refname can not be empty */ + if (segment_len == 0 && segments_count == 0) + goto cleanup; - /* Object id refname have to contain at least one slash, except - * for HEAD in a detached state or MERGE_HEAD if we're in the - * middle of a merge */ - if (is_oid_ref && - !contains_a_slash && - strcmp(name, GIT_HEAD_FILE) != 0 && - strcmp(name, GIT_MERGE_HEAD_FILE) != 0 && - strcmp(name, GIT_FETCH_HEAD_FILE) != 0) - goto invalid_name; + /* A refname can not end with "." */ + if (current[segment_len - 1] == '.') + goto cleanup; - /* A refname can not end with ".lock" */ - if (!git__suffixcmp(name, GIT_FILELOCK_EXTENSION)) - goto invalid_name; + /* A refname can not end with "/" */ + if (current[segment_len - 1] == '/') + goto cleanup; - *buffer_out = '\0'; + if ((segments_count == 1 ) && !(flags & GIT_REF_FORMAT_ALLOW_ONELEVEL)) + goto cleanup; - /* - * For object id references, name has to start with refs/. Again, - * we need to allow HEAD to be in a detached state. - */ - if (is_oid_ref && !(git__prefixcmp(buffer_out_start, GIT_REFS_DIR) || - strcmp(buffer_out_start, GIT_HEAD_FILE))) - goto invalid_name; + if ((segments_count == 1 ) && + !(is_all_caps_and_underscore(name, (size_t)segment_len) || + ((flags & GIT_REF_FORMAT_REFSPEC_PATTERN) && !strcmp("*", name)))) + goto cleanup; - return 0; + if ((segments_count > 1) + && (is_all_caps_and_underscore(name, strchr(name, '/') - name))) + goto cleanup; + + error = 0; -invalid_name: - giterr_set(GITERR_REFERENCE, "The given reference name is not valid"); - return -1; +cleanup: + if (error == GIT_EINVALIDSPEC) + giterr_set( + GITERR_REFERENCE, + "The given reference name '%s' is not valid", name); + + if (error && normalize) + git_buf_free(buf); + + return error; } -int git_reference__normalize_name( +int git_reference_normalize_name( char *buffer_out, - size_t out_size, - const char *name) + size_t buffer_size, + const char *name, + unsigned int flags) { - return normalize_name(buffer_out, out_size, name, 0); + git_buf buf = GIT_BUF_INIT; + int error; + + if ((error = git_reference__normalize_name(&buf, name, flags)) < 0) + goto cleanup; + + if (git_buf_len(&buf) > buffer_size - 1) { + giterr_set( + GITERR_REFERENCE, + "The provided buffer is too short to hold the normalization of '%s'", name); + error = GIT_EBUFS; + goto cleanup; + } + + git_buf_copy_cstr(buffer_out, buffer_size, &buf); + + error = 0; + +cleanup: + git_buf_free(&buf); + return error; } -int git_reference__normalize_name_oid( +int git_reference__normalize_name_lax( char *buffer_out, size_t out_size, const char *name) { - return normalize_name(buffer_out, out_size, name, 1); + return git_reference_normalize_name( + buffer_out, + out_size, + name, + GIT_REF_FORMAT_ALLOW_ONELEVEL); } - #define GIT_REF_TYPEMASK (GIT_REF_OID | GIT_REF_SYMBOLIC) int git_reference_cmp(git_reference *ref1, git_reference *ref2) @@ -1696,12 +781,185 @@ int git_reference_cmp(git_reference *ref1, git_reference *ref2) assert(ref1 && ref2); /* let's put symbolic refs before OIDs */ - if ((ref1->flags & GIT_REF_TYPEMASK) != (ref2->flags & GIT_REF_TYPEMASK)) - return (ref1->flags & GIT_REF_SYMBOLIC) ? -1 : 1; + if (ref1->type != ref2->type) + return (ref1->type == GIT_REF_SYMBOLIC) ? -1 : 1; - if (ref1->flags & GIT_REF_SYMBOLIC) + if (ref1->type == GIT_REF_SYMBOLIC) return strcmp(ref1->target.symbolic, ref2->target.symbolic); return git_oid_cmp(&ref1->target.oid, &ref2->target.oid); } +static int reference__update_terminal( + git_repository *repo, + const char *ref_name, + const git_oid *oid, + int nesting) +{ + git_reference *ref; + int error = 0; + + if (nesting > MAX_NESTING_LEVEL) + return GIT_ENOTFOUND; + + error = git_reference_lookup(&ref, repo, ref_name); + + /* If we haven't found the reference at all, create a new reference. */ + if (error == GIT_ENOTFOUND) { + 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, + nesting+1); + git_reference_free(ref); + } else { + git_reference_free(ref); + error = git_reference_create(NULL, repo, ref_name, oid, 1); + } + + return error; +} + +/* + * Starting with the reference given by `ref_name`, follows symbolic + * references until a direct reference is found and updated the OID + * on that direct reference to `oid`. + */ +int git_reference__update_terminal( + git_repository *repo, + const char *ref_name, + const git_oid *oid) +{ + 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) +{ + git_buf path = GIT_BUF_INIT; + int result; + + assert(ref); + + if (git_buf_join_n(&path, '/', 3, ref->db->repo->path_repository, + GIT_REFLOG_DIR, ref->name) < 0) + return -1; + + result = git_path_isfile(git_buf_cstr(&path)); + git_buf_free(&path); + + return result; +} + +int git_reference__is_branch(const char *ref_name) +{ + return git__prefixcmp(ref_name, GIT_REFS_HEADS_DIR) == 0; +} + +int git_reference_is_branch(git_reference *ref) +{ + assert(ref); + return git_reference__is_branch(ref->name); +} + +int git_reference__is_remote(const char *ref_name) +{ + return git__prefixcmp(ref_name, GIT_REFS_REMOTES_DIR) == 0; +} + +int git_reference_is_remote(git_reference *ref) +{ + assert(ref); + return git_reference__is_remote(ref->name); +} + +static int peel_error(int error, git_reference *ref, const char* msg) +{ + giterr_set( + GITERR_INVALID, + "The reference '%s' cannot be peeled - %s", git_reference_name(ref), 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, + git_otype target_type) +{ + git_reference *resolved = NULL; + git_object *target = NULL; + int error; + + assert(ref); + + if ((error = git_reference_resolve(&resolved, ref)) < 0) + return peel_error(error, ref, "Cannot resolve reference"); + + if ((error = reference_target(&target, resolved)) < 0) { + peel_error(error, ref, "Cannot retrieve reference target"); + goto cleanup; + } + + if (target_type == GIT_OBJ_ANY && git_object_type(target) != GIT_OBJ_TAG) + error = git_object_dup(peeled, target); + else + error = git_object_peel(peeled, target, target_type); + +cleanup: + git_object_free(target); + git_reference_free(resolved); + return error; +} + +int git_reference__is_valid_name( + const char *refname, + unsigned int flags) +{ + int error; + + error = git_reference__normalize_name(NULL, refname, flags) == 0; + giterr_clear(); + + return error; +} + +int git_reference_is_valid_name( + const char *refname) +{ + return git_reference__is_valid_name( + refname, + GIT_REF_FORMAT_ALLOW_ONELEVEL); +} diff --git a/src/refs.h b/src/refs.h index 369e91e1c..7d63c3fbd 100644 --- a/src/refs.h +++ b/src/refs.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -10,7 +10,9 @@ #include "common.h" #include "git2/oid.h" #include "git2/refs.h" +#include "git2/refdb.h" #include "strmap.h" +#include "buffer.h" #define GIT_REFS_DIR "refs/" #define GIT_REFS_HEADS_DIR GIT_REFS_DIR "heads/" @@ -27,33 +29,43 @@ #define GIT_PACKEDREFS_FILE_MODE 0666 #define GIT_HEAD_FILE "HEAD" +#define GIT_ORIG_HEAD_FILE "ORIG_HEAD" #define GIT_FETCH_HEAD_FILE "FETCH_HEAD" #define GIT_MERGE_HEAD_FILE "MERGE_HEAD" +#define GIT_REVERT_HEAD_FILE "REVERT_HEAD" +#define GIT_CHERRY_PICK_HEAD_FILE "CHERRY_PICK_HEAD" +#define GIT_BISECT_LOG_FILE "BISECT_LOG" +#define GIT_REBASE_MERGE_DIR "rebase-merge/" +#define GIT_REBASE_MERGE_INTERACTIVE_FILE GIT_REBASE_MERGE_DIR "interactive" +#define GIT_REBASE_APPLY_DIR "rebase-apply/" +#define GIT_REBASE_APPLY_REBASING_FILE GIT_REBASE_APPLY_DIR "rebasing" +#define GIT_REBASE_APPLY_APPLYING_FILE GIT_REBASE_APPLY_DIR "applying" #define GIT_REFS_HEADS_MASTER_FILE GIT_REFS_HEADS_DIR "master" +#define GIT_STASH_FILE "stash" +#define GIT_REFS_STASH_FILE GIT_REFS_DIR GIT_STASH_FILE + #define GIT_REFNAME_MAX 1024 struct git_reference { - unsigned int flags; - git_repository *owner; - char *name; - time_t mtime; + git_refdb *db; + + git_ref_t type; union { git_oid oid; char *symbolic; } target; + + char name[0]; }; -typedef struct { - git_strmap *packfile; - time_t packfile_time; -} git_refcache; - -void git_repository__refcache_free(git_refcache *refs); - -int git_reference__normalize_name(char *buffer_out, size_t out_size, const char *name); -int git_reference__normalize_name_oid(char *buffer_out, size_t out_size, const char *name); +int git_reference__normalize_name_lax(char *buffer_out, size_t out_size, const char *name); +int git_reference__normalize_name(git_buf *buf, const char *name, unsigned int flags); +int git_reference__update_terminal(git_repository *repo, const char *ref_name, const git_oid *oid); +int git_reference__is_valid_name(const char *refname, unsigned int flags); +int git_reference__is_branch(const char *ref_name); +int git_reference__is_remote(const char *ref_name); /** * Lookup a reference by name and try to resolve to an OID. diff --git a/src/refspec.c b/src/refspec.c index 697b1bf87..a51b0cfab 100644 --- a/src/refspec.c +++ b/src/refspec.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -11,36 +11,127 @@ #include "refspec.h" #include "util.h" #include "posix.h" +#include "refs.h" -int git_refspec_parse(git_refspec *refspec, const char *str) +int git_refspec__parse(git_refspec *refspec, const char *input, bool is_fetch) { - char *delim; + // Ported from https://github.com/git/git/blob/f06d47e7e0d9db709ee204ed13a8a7486149f494/remote.c#L518-636 + + size_t llen; + int is_glob = 0; + const char *lhs, *rhs; + int flags; + + assert(refspec && input); memset(refspec, 0x0, sizeof(git_refspec)); - if (*str == '+') { + lhs = input; + if (*lhs == '+') { refspec->force = 1; - str++; + lhs++; } - delim = strchr(str, ':'); - if (delim == NULL) { - refspec->src = git__strdup(str); - GITERR_CHECK_ALLOC(refspec->src); + rhs = strrchr(lhs, ':'); + + /* + * Before going on, special case ":" (or "+:") as a refspec + * for matching refs. + */ + if (!is_fetch && rhs == lhs && rhs[1] == '\0') { + refspec->matching = 1; return 0; } - refspec->src = git__strndup(str, delim - str); - GITERR_CHECK_ALLOC(refspec->src); + if (rhs) { + size_t rlen = strlen(++rhs); + is_glob = (1 <= rlen && strchr(rhs, '*')); + refspec->dst = git__strndup(rhs, rlen); + } + + llen = (rhs ? (size_t)(rhs - lhs - 1) : strlen(lhs)); + if (1 <= llen && memchr(lhs, '*', llen)) { + if ((rhs && !is_glob) || (!rhs && is_fetch)) + goto invalid; + is_glob = 1; + } else if (rhs && is_glob) + goto invalid; - refspec->dst = git__strdup(delim + 1); - if (refspec->dst == NULL) { - git__free(refspec->src); - refspec->src = NULL; - return -1; + refspec->pattern = is_glob; + refspec->src = git__strndup(lhs, llen); + flags = GIT_REF_FORMAT_ALLOW_ONELEVEL + | (is_glob ? GIT_REF_FORMAT_REFSPEC_PATTERN : 0); + + if (is_fetch) { + /* + * LHS + * - empty is allowed; it means HEAD. + * - otherwise it must be a valid looking ref. + */ + if (!*refspec->src) + ; /* empty is ok */ + else if (!git_reference__is_valid_name(refspec->src, flags)) + goto invalid; + /* + * RHS + * - missing is ok, and is same as empty. + * - empty is ok; it means not to store. + * - otherwise it must be a valid looking ref. + */ + if (!refspec->dst) + ; /* ok */ + else if (!*refspec->dst) + ; /* ok */ + else if (!git_reference__is_valid_name(refspec->dst, flags)) + goto invalid; + } else { + /* + * LHS + * - empty is allowed; it means delete. + * - when wildcarded, it must be a valid looking ref. + * - otherwise, it must be an extended SHA-1, but + * there is no existing way to validate this. + */ + if (!*refspec->src) + ; /* empty is ok */ + else if (is_glob) { + if (!git_reference__is_valid_name(refspec->src, flags)) + goto invalid; + } + else { + ; /* anything goes, for now */ + } + /* + * RHS + * - missing is allowed, but LHS then must be a + * valid looking ref. + * - empty is not allowed. + * - otherwise it must be a valid looking ref. + */ + if (!refspec->dst) { + if (!git_reference__is_valid_name(refspec->src, flags)) + goto invalid; + } else if (!*refspec->dst) { + goto invalid; + } else { + if (!git_reference__is_valid_name(refspec->dst, flags)) + goto invalid; + } } return 0; + + invalid: + return -1; +} + +void git_refspec__free(git_refspec *refspec) +{ + if (refspec == NULL) + return; + + git__free(refspec->src); + git__free(refspec->dst); } const char *git_refspec_src(const git_refspec *refspec) @@ -53,6 +144,13 @@ const char *git_refspec_dst(const git_refspec *refspec) return refspec == NULL ? NULL : refspec->dst; } +int git_refspec_force(const git_refspec *refspec) +{ + assert(refspec); + + return refspec->force; +} + int git_refspec_src_matches(const git_refspec *refspec, const char *refname) { if (refspec == NULL || refspec->src == NULL) @@ -61,11 +159,19 @@ int git_refspec_src_matches(const git_refspec *refspec, const char *refname) return (p_fnmatch(refspec->src, refname, 0) == 0); } -int git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, const char *name) +int git_refspec_dst_matches(const git_refspec *refspec, const char *refname) +{ + if (refspec == NULL || refspec->dst == NULL) + return false; + + return (p_fnmatch(refspec->dst, refname, 0) == 0); +} + +static int refspec_transform_internal(char *out, size_t outlen, const char *from, const char *to, const char *name) { size_t baselen, namelen; - baselen = strlen(spec->dst); + baselen = strlen(to); if (outlen <= baselen) { giterr_set(GITERR_INVALID, "Reference name too long"); return GIT_EBUFS; @@ -75,8 +181,8 @@ int git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, con * No '*' at the end means that it's mapped to one specific local * branch, so no actual transformation is needed. */ - if (spec->dst[baselen - 1] != '*') { - memcpy(out, spec->dst, baselen + 1); /* include '\0' */ + if (to[baselen - 1] != '*') { + memcpy(out, to, baselen + 1); /* include '\0' */ return 0; } @@ -84,7 +190,7 @@ int git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, con baselen--; /* skip the prefix, -1 is for the '*' */ - name += strlen(spec->src) - 1; + name += strlen(from) - 1; namelen = strlen(name); @@ -93,26 +199,36 @@ int git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, con return GIT_EBUFS; } - memcpy(out, spec->dst, baselen); + memcpy(out, to, baselen); memcpy(out + baselen, name, namelen + 1); return 0; } -int git_refspec_transform_r(git_buf *out, const git_refspec *spec, const char *name) +int git_refspec_transform(char *out, size_t outlen, const git_refspec *spec, const char *name) +{ + return refspec_transform_internal(out, outlen, spec->src, spec->dst, name); +} + +int git_refspec_rtransform(char *out, size_t outlen, const git_refspec *spec, const char *name) +{ + 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) { - if (git_buf_sets(out, spec->dst) < 0) + if (git_buf_sets(out, to) < 0) return -1; /* - * No '*' at the end means that it's mapped to one specific local + * 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; git_buf_truncate(out, git_buf_len(out) - 1); /* remove trailing '*' */ - git_buf_puts(out, name + strlen(spec->src) - 1); + git_buf_puts(out, name + strlen(from) - 1); if (git_buf_oom(out)) return -1; @@ -120,3 +236,31 @@ int git_refspec_transform_r(git_buf *out, const git_refspec *spec, const char *n return 0; } +int git_refspec_transform_r(git_buf *out, const git_refspec *spec, const char *name) +{ + return refspec_transform(out, spec->src, spec->dst, name); +} + +int git_refspec_transform_l(git_buf *out, const git_refspec *spec, const char *name) +{ + return refspec_transform(out, spec->dst, spec->src, name); +} + +int git_refspec__serialize(git_buf *out, const git_refspec *refspec) +{ + if (refspec->force) + git_buf_putc(out, '+'); + + git_buf_printf(out, "%s:%s", + refspec->src != NULL ? refspec->src : "", + refspec->dst != NULL ? refspec->dst : ""); + + return git_buf_oom(out) == false; +} + +int git_refspec_is_wildcard(const git_refspec *spec) +{ + assert(spec && spec->src); + + return (spec->src[strlen(spec->src) - 1] == '*'); +} diff --git a/src/refspec.h b/src/refspec.h index 2db504910..a7a4dd834 100644 --- a/src/refspec.h +++ b/src/refspec.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -19,7 +19,15 @@ struct git_refspec { matching :1; }; +#define GIT_REFSPEC_TAGS "refs/tags/*:refs/tags/*" + int git_refspec_parse(struct git_refspec *refspec, const char *str); +int git_refspec__parse( + struct git_refspec *refspec, + const char *str, + bool is_fetch); + +void git_refspec__free(git_refspec *refspec); /** * Transform a reference to its target following the refspec's rules, @@ -32,4 +40,25 @@ int git_refspec_parse(struct git_refspec *refspec, const char *str); */ int git_refspec_transform_r(git_buf *out, const git_refspec *spec, const char *name); +/** + * Transform a reference from its target following the refspec's rules, + * and writes the results into a git_buf. + * + * @param out where to store the source name + * @param spec the refspec + * @param name the name of the reference to transform + * @return 0 or error if buffer allocation fails + */ +int git_refspec_transform_l(git_buf *out, const git_refspec *spec, const char *name); + +int git_refspec__serialize(git_buf *out, const git_refspec *refspec); + +/** + * Determines if a refspec is a wildcard refspec. + * + * @param spec the refspec + * @return 1 if the refspec is a wildcard, 0 otherwise + */ +int git_refspec_is_wildcard(const git_refspec *spec); + #endif diff --git a/src/remote.c b/src/remote.c index 9740344f8..56853834b 100644 --- a/src/remote.c +++ b/src/remote.c @@ -1,74 +1,94 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ -#include "git2/remote.h" #include "git2/config.h" #include "git2/types.h" +#include "git2/oid.h" #include "config.h" #include "repository.h" #include "remote.h" #include "fetch.h" #include "refs.h" +#include "refspec.h" +#include "fetchhead.h" #include <regex.h> -static int refspec_parse(git_refspec *refspec, const char *str) +static int parse_remote_refspec(git_config *cfg, git_refspec *refspec, const char *var, bool is_fetch) { - char *delim; + int error; + const char *val; - memset(refspec, 0x0, sizeof(git_refspec)); + if ((error = git_config_get_string(&val, cfg, var)) < 0) + return error; - if (*str == '+') { - refspec->force = 1; - str++; - } + return git_refspec__parse(refspec, val, is_fetch); +} - delim = strchr(str, ':'); - if (delim == NULL) { - giterr_set(GITERR_NET, "Invalid refspec, missing ':'"); +static int download_tags_value(git_remote *remote, git_config *cfg) +{ + const char *val; + 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; + if (git_buf_printf(&buf, "remote.%s.tagopt", remote->name) < 0) return -1; - } - refspec->src = git__strndup(str, delim - str); - GITERR_CHECK_ALLOC(refspec->src); + error = git_config_get_string(&val, cfg, git_buf_cstr(&buf)); + git_buf_free(&buf); + if (!error && !strcmp(val, "--no-tags")) + remote->download_tags = GIT_REMOTE_DOWNLOAD_TAGS_NONE; + else if (!error && !strcmp(val, "--tags")) + remote->download_tags = GIT_REMOTE_DOWNLOAD_TAGS_ALL; - refspec->dst = git__strdup(delim + 1); - GITERR_CHECK_ALLOC(refspec->dst); + if (error == GIT_ENOTFOUND) + error = 0; - return 0; + return error; } -static int parse_remote_refspec(git_config *cfg, git_refspec *refspec, const char *var) +static int ensure_remote_name_is_valid(const char *name) { - int error; - const char *val; + int error = 0; - if ((error = git_config_get_string(&val, cfg, var)) < 0) - return error; + if (!git_remote_is_valid_name(name)) { + giterr_set( + GITERR_CONFIG, + "'%s' is not a valid remote name.", name); + error = GIT_EINVALIDSPEC; + } - return refspec_parse(refspec, val); + return error; } -int git_remote_new(git_remote **out, git_repository *repo, const char *name, const char *url, const char *fetch) +static int create_internal(git_remote **out, git_repository *repo, const char *name, const char *url, const char *fetch) { git_remote *remote; + git_buf fetchbuf = GIT_BUF_INIT; + int error = -1; /* name is optional */ assert(out && repo && url); - remote = git__malloc(sizeof(git_remote)); + remote = git__calloc(1, sizeof(git_remote)); GITERR_CHECK_ALLOC(remote); - memset(remote, 0x0, sizeof(git_remote)); remote->repo = repo; + remote->check_cert = 1; + remote->update_fetchhead = 1; if (git_vector_init(&remote->refs, 32, NULL) < 0) - return -1; + goto on_error; remote->url = git__strdup(url); GITERR_CHECK_ALLOC(remote->url); @@ -79,18 +99,93 @@ int git_remote_new(git_remote **out, git_repository *repo, const char *name, con } if (fetch != NULL) { - if (refspec_parse(&remote->fetch, fetch) < 0) + if (git_refspec__parse(&remote->fetch, fetch, true) < 0) goto on_error; } + /* A remote without a name doesn't download tags */ + if (!name) { + remote->download_tags = GIT_REMOTE_DOWNLOAD_TAGS_NONE; + } + + *out = remote; + git_buf_free(&fetchbuf); + return 0; + +on_error: + git_remote_free(remote); + git_buf_free(&fetchbuf); + return error; +} + +static int ensure_remote_doesnot_exist(git_repository *repo, const char *name) +{ + int error; + git_remote *remote; + + error = git_remote_load(&remote, repo, name); + + if (error == GIT_ENOTFOUND) + return 0; + + if (error < 0) + return error; + + git_remote_free(remote); + + giterr_set( + GITERR_CONFIG, + "Remote '%s' already exists.", name); + + return GIT_EEXISTS; +} + + +int git_remote_create(git_remote **out, git_repository *repo, const char *name, const char *url) +{ + git_buf buf = GIT_BUF_INIT; + git_remote *remote = NULL; + int error; + + if ((error = ensure_remote_name_is_valid(name)) < 0) + return error; + + if ((error = ensure_remote_doesnot_exist(repo, name)) < 0) + return error; + + if (git_buf_printf(&buf, "+refs/heads/*:refs/remotes/%s/*", name) < 0) + return -1; + + if (create_internal(&remote, repo, name, url, git_buf_cstr(&buf)) < 0) + goto on_error; + + git_buf_free(&buf); + + if (git_remote_save(remote) < 0) + goto on_error; + *out = remote; + return 0; on_error: + git_buf_free(&buf); git_remote_free(remote); return -1; } +int git_remote_create_inmemory(git_remote **out, git_repository *repo, const char *fetch, const char *url) +{ + int error; + git_remote *remote; + + if ((error = create_internal(&remote, repo, NULL, url, fetch)) < 0) + return error; + + *out = remote; + return 0; +} + int git_remote_load(git_remote **out, git_repository *repo, const char *name) { git_remote *remote; @@ -101,6 +196,9 @@ int git_remote_load(git_remote **out, git_repository *repo, const char *name) assert(out && repo && name); + if ((error = ensure_remote_name_is_valid(name)) < 0) + return error; + if (git_repository_config__weakptr(&config, repo) < 0) return -1; @@ -108,6 +206,8 @@ int git_remote_load(git_remote **out, git_repository *repo, const char *name) GITERR_CHECK_ALLOC(remote); memset(remote, 0x0, sizeof(git_remote)); + remote->check_cert = 1; + remote->update_fetchhead = 1; remote->name = git__strdup(name); GITERR_CHECK_ALLOC(remote->name); @@ -123,18 +223,46 @@ 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; + goto cleanup; + } remote->repo = repo; remote->url = git__strdup(val); GITERR_CHECK_ALLOC(remote->url); 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; + } + + if (error < 0) { + error = -1; + goto cleanup; + } + + if (val) { + remote->pushurl = git__strdup(val); + GITERR_CHECK_ALLOC(remote->pushurl); + } + + git_buf_clear(&buf); if (git_buf_printf(&buf, "remote.%s.fetch", name) < 0) { error = -1; goto cleanup; } - error = parse_remote_refspec(config, &remote->fetch, git_buf_cstr(&buf)); + error = parse_remote_refspec(config, &remote->fetch, git_buf_cstr(&buf), true); if (error == GIT_ENOTFOUND) error = 0; @@ -149,7 +277,7 @@ int git_remote_load(git_remote **out, git_repository *repo, const char *name) goto cleanup; } - error = parse_remote_refspec(config, &remote->push, git_buf_cstr(&buf)); + error = parse_remote_refspec(config, &remote->push, git_buf_cstr(&buf), false); if (error == GIT_ENOTFOUND) error = 0; @@ -158,6 +286,9 @@ int git_remote_load(git_remote **out, git_repository *repo, const char *name) goto cleanup; } + if (download_tags_value(remote, config) < 0) + goto cleanup; + *out = remote; cleanup: @@ -169,15 +300,61 @@ cleanup: return error; } +static int update_config_refspec( + git_config *config, + const char *remote_name, + const git_refspec *refspec, + int git_direction) +{ + git_buf name = GIT_BUF_INIT, value = GIT_BUF_INIT; + int error = -1; + + if (refspec->src == NULL || refspec->dst == NULL) + return 0; + + if (git_buf_printf( + &name, + "remote.%s.%s", + remote_name, + git_direction == GIT_DIRECTION_FETCH ? "fetch" : "push") < 0) + goto cleanup; + + if (git_refspec__serialize(&value, refspec) < 0) + goto cleanup; + + error = git_config_set_string( + config, + git_buf_cstr(&name), + git_buf_cstr(&value)); + +cleanup: + git_buf_free(&name); + git_buf_free(&value); + + return error; +} + int git_remote_save(const git_remote *remote) { + int error; git_config *config; - git_buf buf = GIT_BUF_INIT, value = GIT_BUF_INIT; + const char *tagopt = NULL; + git_buf buf = GIT_BUF_INIT; + + assert(remote); + + if (!remote->name) { + giterr_set(GITERR_INVALID, "Can't save an in-memory remote."); + return GIT_EINVALIDSPEC; + } + + if ((error = ensure_remote_name_is_valid(remote->name)) < 0) + return error; if (git_repository_config__weakptr(&config, remote->repo) < 0) return -1; - if (git_buf_printf(&buf, "remote.%s.%s", remote->name, "url") < 0) + if (git_buf_printf(&buf, "remote.%s.url", remote->name) < 0) return -1; if (git_config_set_string(config, git_buf_cstr(&buf), remote->url) < 0) { @@ -185,71 +362,142 @@ int git_remote_save(const git_remote *remote) return -1; } - if (remote->fetch.src != NULL && remote->fetch.dst != NULL) { - git_buf_clear(&buf); - git_buf_clear(&value); - git_buf_printf(&buf, "remote.%s.fetch", remote->name); - git_buf_printf(&value, "%s:%s", remote->fetch.src, remote->fetch.dst); - if (git_buf_oom(&buf) || git_buf_oom(&value)) + git_buf_clear(&buf); + if (git_buf_printf(&buf, "remote.%s.pushurl", remote->name) < 0) + return -1; + + if (remote->pushurl) { + if (git_config_set_string(config, git_buf_cstr(&buf), remote->pushurl) < 0) { + git_buf_free(&buf); + return -1; + } + } else { + int error = git_config_delete_entry(config, git_buf_cstr(&buf)); + if (error == GIT_ENOTFOUND) { + error = 0; + giterr_clear(); + } + if (error < 0) { + git_buf_free(&buf); return -1; + } + } - if (git_config_set_string(config, git_buf_cstr(&buf), git_buf_cstr(&value)) < 0) + if (update_config_refspec( + config, + remote->name, + &remote->fetch, + GIT_DIRECTION_FETCH) < 0) goto on_error; - } - if (remote->push.src != NULL && remote->push.dst != NULL) { - git_buf_clear(&buf); - git_buf_clear(&value); - git_buf_printf(&buf, "remote.%s.push", remote->name); - git_buf_printf(&value, "%s:%s", remote->push.src, remote->push.dst); - if (git_buf_oom(&buf) || git_buf_oom(&value)) - return -1; + if (update_config_refspec( + config, + remote->name, + &remote->push, + GIT_DIRECTION_PUSH) < 0) + goto on_error; + + /* + * What action to take depends on the old and new values. This + * is describes by the table below. tagopt means whether the + * is already a value set in the config + * + * AUTO ALL or NONE + * +-----------------------+ + * tagopt | remove | set | + * +---------+-------------| + * !tagopt | nothing | set | + * +---------+-------------+ + */ - if (git_config_set_string(config, git_buf_cstr(&buf), git_buf_cstr(&value)) < 0) + git_buf_clear(&buf); + if (git_buf_printf(&buf, "remote.%s.tagopt", remote->name) < 0) + goto on_error; + + error = git_config_get_string(&tagopt, config, git_buf_cstr(&buf)); + if (error < 0 && error != GIT_ENOTFOUND) + goto on_error; + + if (remote->download_tags == GIT_REMOTE_DOWNLOAD_TAGS_ALL) { + if (git_config_set_string(config, git_buf_cstr(&buf), "--tags") < 0) + goto on_error; + } else if (remote->download_tags == GIT_REMOTE_DOWNLOAD_TAGS_NONE) { + if (git_config_set_string(config, git_buf_cstr(&buf), "--no-tags") < 0) + goto on_error; + } else if (tagopt) { + if (git_config_delete_entry(config, git_buf_cstr(&buf)) < 0) goto on_error; } git_buf_free(&buf); - git_buf_free(&value); return 0; on_error: git_buf_free(&buf); - git_buf_free(&value); return -1; } -const char *git_remote_name(git_remote *remote) +const char *git_remote_name(const git_remote *remote) { assert(remote); return remote->name; } -const char *git_remote_url(git_remote *remote) +const char *git_remote_url(const git_remote *remote) { assert(remote); return remote->url; } +int git_remote_set_url(git_remote *remote, const char* url) +{ + assert(remote); + assert(url); + + git__free(remote->url); + remote->url = git__strdup(url); + GITERR_CHECK_ALLOC(remote->url); + + return 0; +} + +const char *git_remote_pushurl(const git_remote *remote) +{ + assert(remote); + return remote->pushurl; +} + +int git_remote_set_pushurl(git_remote *remote, const char* url) +{ + assert(remote); + + git__free(remote->pushurl); + if (url) { + remote->pushurl = git__strdup(url); + GITERR_CHECK_ALLOC(remote->pushurl); + } else { + remote->pushurl = NULL; + } + return 0; +} + int git_remote_set_fetchspec(git_remote *remote, const char *spec) { git_refspec refspec; assert(remote && spec); - if (refspec_parse(&refspec, spec) < 0) + if (git_refspec__parse(&refspec, spec, true) < 0) return -1; - git__free(remote->fetch.src); - git__free(remote->fetch.dst); - remote->fetch.src = refspec.src; - remote->fetch.dst = refspec.dst; + git_refspec__free(&remote->fetch); + memcpy(&remote->fetch, &refspec, sizeof(git_refspec)); return 0; } -const git_refspec *git_remote_fetchspec(git_remote *remote) +const git_refspec *git_remote_fetchspec(const git_remote *remote) { assert(remote); return &remote->fetch; @@ -261,35 +509,65 @@ int git_remote_set_pushspec(git_remote *remote, const char *spec) assert(remote && spec); - if (refspec_parse(&refspec, spec) < 0) + if (git_refspec__parse(&refspec, spec, false) < 0) return -1; - git__free(remote->push.src); - git__free(remote->push.dst); + git_refspec__free(&remote->push); remote->push.src = refspec.src; remote->push.dst = refspec.dst; return 0; } -const git_refspec *git_remote_pushspec(git_remote *remote) +const git_refspec *git_remote_pushspec(const git_remote *remote) { assert(remote); return &remote->push; } -int git_remote_connect(git_remote *remote, int direction) +const char* git_remote__urlfordirection(git_remote *remote, int direction) +{ + assert(remote); + + if (direction == GIT_DIRECTION_FETCH) { + return remote->url; + } + + if (direction == GIT_DIRECTION_PUSH) { + return remote->pushurl ? remote->pushurl : remote->url; + } + + return NULL; +} + +int git_remote_connect(git_remote *remote, git_direction direction) { git_transport *t; + const char *url; + int flags = GIT_TRANSPORTFLAGS_NONE; assert(remote); - if (git_transport_new(&t, remote->url) < 0) + t = remote->transport; + + url = git_remote__urlfordirection(remote, direction); + if (url == NULL ) return -1; - if (t->connect(t, direction) < 0) { + /* A transport could have been supplied in advance with + * git_remote_set_transport */ + if (!t && git_transport_new(&t, remote, url) < 0) + return -1; + + if (t->set_callbacks && + t->set_callbacks(t, remote->callbacks.progress, NULL, remote->callbacks.payload) < 0) + goto on_error; + + if (!remote->check_cert) + flags |= GIT_TRANSPORTFLAGS_NO_CHECK_CERT; + + if (t->connect(t, url, remote->cred_acquire_cb, remote->cred_acquire_payload, direction, flags) < 0) goto on_error; - } remote->transport = t; @@ -297,6 +575,10 @@ int git_remote_connect(git_remote *remote, int direction) on_error: t->free(t); + + if (t == remote->transport) + remote->transport = NULL; + return -1; } @@ -304,59 +586,283 @@ int git_remote_ls(git_remote *remote, git_headlist_cb list_cb, void *payload) { assert(remote); - if (!remote->transport || !remote->transport->connected) { - giterr_set(GITERR_NET, "The remote is not connected"); + return remote->transport->ls(remote->transport, list_cb, payload); +} + +int git_remote__get_http_proxy(git_remote *remote, bool use_ssl, char **proxy_url) +{ + git_config *cfg; + const char *val; + + assert(remote); + + if (!proxy_url || !remote->repo) return -1; + + *proxy_url = NULL; + + if (git_repository_config__weakptr(&cfg, remote->repo) < 0) + return -1; + + /* Go through the possible sources for proxy configuration, from most specific + * to least specific. */ + + /* remote.<name>.proxy config setting */ + if (remote->name && 0 != *(remote->name)) { + git_buf buf = GIT_BUF_INIT; + + if (git_buf_printf(&buf, "remote.%s.proxy", remote->name) < 0) + return -1; + + if (!git_config_get_string(&val, cfg, git_buf_cstr(&buf)) && + val && ('\0' != *val)) { + git_buf_free(&buf); + + *proxy_url = git__strdup(val); + GITERR_CHECK_ALLOC(*proxy_url); + return 0; + } + + git_buf_free(&buf); } - return remote->transport->ls(remote->transport, list_cb, payload); + /* http.proxy config setting */ + if (!git_config_get_string(&val, cfg, "http.proxy") && + val && ('\0' != *val)) { + *proxy_url = git__strdup(val); + GITERR_CHECK_ALLOC(*proxy_url); + return 0; + } + + /* HTTP_PROXY / HTTPS_PROXY environment variables */ + val = use_ssl ? getenv("HTTPS_PROXY") : getenv("HTTP_PROXY"); + + if (val && ('\0' != *val)) { + *proxy_url = git__strdup(val); + GITERR_CHECK_ALLOC(*proxy_url); + return 0; + } + + return 0; } -int git_remote_download(git_remote *remote, git_off_t *bytes, git_indexer_stats *stats) +int git_remote_download( + git_remote *remote, + git_transfer_progress_callback progress_cb, + void *progress_payload) { int error; - assert(remote && bytes && stats); + assert(remote); if ((error = git_fetch_negotiate(remote)) < 0) return error; - return git_fetch_download_pack(remote, bytes, stats); + 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; + git_remote_head *remote_ref; + + assert(update_heads && fetchspec_src); + + *out = NULL; + + git_vector_foreach(update_heads, i, remote_ref) { + if (strcmp(remote_ref->name, fetchspec_src) == 0) { + *out = remote_ref; + break; + } + } + + return 0; } -int git_remote_update_tips(git_remote *remote, int (*cb)(const char *refname, const git_oid *a, const git_oid *b)) +static int remote_head_for_ref(git_remote_head **out, git_remote *remote, 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); + + *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) { + /* Not an error if HEAD is orphaned or no tracking branch */ + if (error == GIT_ENOTFOUND) + error = 0; + + goto cleanup; + } + + error = remote_head_for_fetchspec_src(out, update_heads, git_buf_cstr(&remote_name)); + +cleanup: + git_reference_free(tracking_ref); + git_reference_free(resolved_ref); + git_buf_free(&remote_name); + return error; +} + +static int git_remote_write_fetchhead(git_remote *remote, 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; + git_vector fetchhead_refs; + bool include_all_fetchheads; + unsigned int i = 0; + int error = 0; + + assert(remote); + + /* no heads, nothing to do */ + if (update_heads->length == 0) + return 0; + + spec = &remote->fetch; + + if (git_vector_init(&fetchhead_refs, update_heads->length, git_fetchhead_ref_cmp) < 0) + return -1; + + /* Iff refspec is * (but not subdir slash star), include tags */ + include_all_fetchheads = (strcmp(GIT_REFS_HEADS_DIR "*", git_refspec_src(spec)) == 0); + + /* 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) + goto cleanup; + } else { + /* If we're fetching a single refspec, that's the only thing that should be in FETCH_HEAD. */ + if ((error = remote_head_for_fetchspec_src(&merge_remote_ref, update_heads, git_refspec_src(spec))) < 0) + goto cleanup; + } + + /* Create the FETCH_HEAD file */ + git_vector_foreach(update_heads, i, remote_ref) { + int merge_this_fetchhead = (merge_remote_ref == remote_ref); + + if (!include_all_fetchheads && + !git_refspec_src_matches(spec, remote_ref->name) && + !merge_this_fetchhead) + continue; + + if (git_fetchhead_ref_create(&fetchhead_ref, + &remote_ref->oid, + merge_this_fetchhead, + remote_ref->name, + git_remote_url(remote)) < 0) + goto cleanup; + + if (git_vector_insert(&fetchhead_refs, fetchhead_ref) < 0) + goto cleanup; + } + + git_fetchhead_write(remote->repo, &fetchhead_refs); + +cleanup: + for (i = 0; i < fetchhead_refs.length; ++i) + git_fetchhead_ref_free(fetchhead_refs.contents[i]); + + git_vector_free(&fetchhead_refs); + git_reference_free(head_ref); + + return error; +} + +int git_remote_update_tips(git_remote *remote) +{ + int error = 0, autotag; unsigned int i = 0; git_buf refname = GIT_BUF_INIT; git_oid old; - git_vector *refs = &remote->refs; + git_odb *odb; git_remote_head *head; git_reference *ref; - struct git_refspec *spec = &remote->fetch; + struct git_refspec *spec; + git_refspec tagspec; + git_vector refs, update_heads; assert(remote); - if (refs->length == 0) - return 0; + spec = &remote->fetch; + + if (git_repository_odb__weakptr(&odb, remote->repo) < 0) + return -1; - /* HEAD is only allowed to be the first in the list */ - head = refs->contents[0]; - if (!strcmp(head->name, GIT_HEAD_FILE)) { - if (git_reference_create_oid(&ref, remote->repo, GIT_FETCH_HEAD_FILE, &head->oid, 1) < 0) - return -1; + if (git_refspec__parse(&tagspec, GIT_REFSPEC_TAGS, true) < 0) + return -1; - i = 1; - git_reference_free(ref); + /* Make a copy of the transport's refs */ + if (git_vector_init(&refs, 16, NULL) < 0 || + 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 = refs->contents[i]; + for (; i < refs.length; ++i) { + head = (git_remote_head *)refs.contents[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_transform_r(&refname, spec, head->name) < 0) + goto on_error; + } else if (remote->download_tags != GIT_REMOTE_DOWNLOAD_TAGS_NONE) { + + if (remote->download_tags != GIT_REMOTE_DOWNLOAD_TAGS_ALL) + autotag = 1; - if (git_refspec_transform_r(&refname, spec, head->name) < 0) + if (!git_refspec_src_matches(&tagspec, head->name)) + continue; + + git_buf_clear(&refname); + if (git_buf_puts(&refname, head->name) < 0) + goto on_error; + } else { + continue; + } + + if (autotag && !git_odb_exists(odb, &head->oid)) + continue; + + if (git_vector_insert(&update_heads, head) < 0) goto on_error; - error = git_reference_name_to_oid(&old, remote->repo, refname.ptr); + error = git_reference_name_to_id(&old, remote->repo, refname.ptr); if (error < 0 && error != GIT_ENOTFOUND) goto on_error; @@ -366,21 +872,33 @@ int git_remote_update_tips(git_remote *remote, int (*cb)(const char *refname, co if (!git_oid_cmp(&old, &head->oid)) continue; - if (git_reference_create_oid(&ref, remote->repo, refname.ptr, &head->oid, 1) < 0) - break; + /* In autotag mode, don't overwrite any locally-existing tags */ + error = git_reference_create(&ref, remote->repo, refname.ptr, &head->oid, !autotag); + if (error < 0 && error != GIT_EEXISTS) + goto on_error; git_reference_free(ref); - if (cb != NULL) { - if (cb(refname.ptr, &old, &head->oid) < 0) + if (remote->callbacks.update_tips != NULL) { + if (remote->callbacks.update_tips(refname.ptr, &old, &head->oid, remote->callbacks.payload) < 0) goto on_error; } } + if (git_remote_update_fetchhead(remote) && + (error = git_remote_write_fetchhead(remote, &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); return -1; @@ -389,15 +907,28 @@ on_error: int git_remote_connected(git_remote *remote) { assert(remote); - return remote->transport == NULL ? 0 : remote->transport->connected; + + if (!remote->transport || !remote->transport->is_connected) + return 0; + + /* Ask the transport if it's connected. */ + return remote->transport->is_connected(remote->transport); +} + +void git_remote_stop(git_remote *remote) +{ + assert(remote); + + if (remote->transport && remote->transport->cancel) + remote->transport->cancel(remote->transport); } void git_remote_disconnect(git_remote *remote) { assert(remote); - if (remote->transport != NULL && remote->transport->connected) - remote->transport->close(remote->transport); + if (git_remote_connected(remote)) + remote->transport->close(remote->transport); } void git_remote_free(git_remote *remote) @@ -414,11 +945,10 @@ void git_remote_free(git_remote *remote) git_vector_free(&remote->refs); - git__free(remote->fetch.src); - git__free(remote->fetch.dst); - git__free(remote->push.src); - git__free(remote->push.dst); + git_refspec__free(&remote->fetch); + git_refspec__free(&remote->push); git__free(remote->url); + git__free(remote->pushurl); git__free(remote->name); git__free(remote); } @@ -428,12 +958,12 @@ struct cb_data { regex_t *preg; }; -static int remote_list_cb(const char *name, const char *value, void *data_) +static int remote_list_cb(const git_config_entry *entry, void *data_) { struct cb_data *data = (struct cb_data *)data_; size_t nmatch = 2; regmatch_t pmatch[2]; - GIT_UNUSED(value); + const char *name = entry->name; if (!regexec(data->preg, name, nmatch, pmatch, 0)) { char *remote_name = git__strndup(&name[pmatch[1].rm_so], pmatch[1].rm_eo - pmatch[1].rm_so); @@ -477,6 +1007,11 @@ int git_remote_list(git_strarray *remotes_list, git_repository *repo) } git_vector_free(&list); + + /* cb error is converted to GIT_EUSER by git_config_foreach */ + if (error == GIT_EUSER) + error = -1; + return error; } @@ -486,25 +1021,371 @@ int git_remote_list(git_strarray *remotes_list, git_repository *repo) return 0; } -int git_remote_add(git_remote **out, git_repository *repo, const char *name, const char *url) +void git_remote_check_cert(git_remote *remote, int check) { - git_buf buf = GIT_BUF_INIT; + assert(remote); + + remote->check_cert = check; +} + +int git_remote_set_callbacks(git_remote *remote, git_remote_callbacks *callbacks) +{ + assert(remote && callbacks); + + GITERR_CHECK_VERSION(callbacks, GIT_REMOTE_CALLBACKS_VERSION, "git_remote_callbacks"); + + memcpy(&remote->callbacks, callbacks, sizeof(git_remote_callbacks)); + + if (remote->transport && remote->transport->set_callbacks) + remote->transport->set_callbacks(remote->transport, + remote->callbacks.progress, + NULL, + remote->callbacks.payload); - if (git_buf_printf(&buf, "refs/heads/*:refs/remotes/%s/*", name) < 0) + return 0; +} + +void git_remote_set_cred_acquire_cb( + git_remote *remote, + git_cred_acquire_cb cred_acquire_cb, + void *payload) +{ + assert(remote); + + remote->cred_acquire_cb = cred_acquire_cb; + remote->cred_acquire_payload = payload; +} + +int git_remote_set_transport(git_remote *remote, git_transport *transport) +{ + assert(remote && transport); + + GITERR_CHECK_VERSION(transport, GIT_TRANSPORT_VERSION, "git_transport"); + + if (remote->transport) { + giterr_set(GITERR_NET, "A transport is already bound to this remote"); return -1; + } - if (git_remote_new(out, repo, name, url, git_buf_cstr(&buf)) < 0) - goto on_error; + remote->transport = transport; + return 0; +} - git_buf_free(&buf); +const git_transfer_progress* git_remote_stats(git_remote *remote) +{ + assert(remote); + return &remote->stats; +} - if (git_remote_save(*out) < 0) - goto on_error; +git_remote_autotag_option_t git_remote_autotag(git_remote *remote) +{ + return remote->download_tags; +} + +void git_remote_set_autotag(git_remote *remote, git_remote_autotag_option_t value) +{ + remote->download_tags = value; +} + +static int rename_remote_config_section( + git_repository *repo, + const char *old_name, + const char *new_name) +{ + git_buf old_section_name = GIT_BUF_INIT, + new_section_name = GIT_BUF_INIT; + int error = -1; + + if (git_buf_printf(&old_section_name, "remote.%s", old_name) < 0) + goto cleanup; + + if (git_buf_printf(&new_section_name, "remote.%s", new_name) < 0) + goto cleanup; + + error = git_config_rename_section( + repo, + git_buf_cstr(&old_section_name), + git_buf_cstr(&new_section_name)); + +cleanup: + git_buf_free(&old_section_name); + git_buf_free(&new_section_name); + + return error; +} + +struct update_data +{ + git_config *config; + const char *old_remote_name; + const char *new_remote_name; +}; + +static int update_config_entries_cb( + const git_config_entry *entry, + void *payload) +{ + struct update_data *data = (struct update_data *)payload; + + if (strcmp(entry->value, data->old_remote_name)) + return 0; + + return git_config_set_string( + data->config, + entry->name, + data->new_remote_name); +} + +static int update_branch_remote_config_entry( + git_repository *repo, + const char *old_name, + const char *new_name) +{ + git_config *config; + struct update_data data; + + if (git_repository_config__weakptr(&config, repo) < 0) + return -1; + + data.config = config; + data.old_remote_name = old_name; + data.new_remote_name = new_name; + + return git_config_foreach_match( + config, + "branch\\..+\\.remote", + 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, + 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) + 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); + git_reference_free(reference); + +cleanup: + git_reference_free(newref); + git_buf_free(&new_name); + return error; +} + +static int rename_remote_references( + git_repository *repo, + const char *old_name, + const char *new_name) +{ + git_vector refnames; + int error = -1; + unsigned int i; + char *name; + + if (git_vector_init(&refnames, 8, NULL) < 0) + goto cleanup; + + if (git_reference_foreach( + repo, + GIT_REF_LISTALL, + rename_cb, + &refnames) < 0) + goto cleanup; + + git_vector_foreach(&refnames, i, name) { + if ((error = rename_one_remote_reference(repo, name, old_name, new_name)) < 0) + goto cleanup; + } + + error = 0; +cleanup: + git_vector_foreach(&refnames, i, name) { + git__free(name); + } + + git_vector_free(&refnames); + return error; +} + +static int rename_fetch_refspecs( + git_remote *remote, + const char *new_name, + int (*callback)(const char *problematic_refspec, void *payload), + void *payload) +{ + git_config *config; + const git_refspec *fetch_refspec; + git_buf dst_prefix = GIT_BUF_INIT, serialized = GIT_BUF_INIT; + const char* pos; + 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) + goto cleanup; + + /* Is it an in-memory remote? */ + if (!remote->name) { + error = (callback(git_buf_cstr(&serialized), payload) < 0) ? GIT_EUSER : 0; + goto cleanup; + } + + if (git_buf_printf(&dst_prefix, ":refs/remotes/%s/", remote->name) < 0) + goto cleanup; + + pos = strstr(git_buf_cstr(&serialized), git_buf_cstr(&dst_prefix)); + + /* 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; + } + + if (git_buf_splice( + &serialized, + pos - git_buf_cstr(&serialized) + strlen(":refs/remotes/"), + strlen(remote->name), new_name, + strlen(new_name)) < 0) + goto cleanup; + + git_refspec__free(&remote->fetch); + + 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; + + error = update_config_refspec(config, new_name, &remote->fetch, GIT_DIRECTION_FETCH); + +cleanup: + git_buf_free(&serialized); + git_buf_free(&dst_prefix); + return error; +} + +int git_remote_rename( + git_remote *remote, + const char *new_name, + git_remote_rename_problem_cb callback, + void *payload) +{ + int error; + + assert(remote && new_name); + + if (!remote->name) { + giterr_set(GITERR_INVALID, "Can't rename an in-memory remote."); + return GIT_EINVALIDSPEC; + } + + if ((error = ensure_remote_name_is_valid(new_name)) < 0) + return error; + + if (remote->repo) { + if ((error = ensure_remote_doesnot_exist(remote->repo, new_name)) < 0) + return error; + + if (!remote->name) { + if ((error = rename_fetch_refspecs( + remote, + new_name, + callback, + payload)) < 0) + return error; + + remote->name = git__strdup(new_name); + + if (!remote->name) return 0; + return git_remote_save(remote); + } + + if ((error = rename_remote_config_section( + remote->repo, + remote->name, + new_name)) < 0) + return error; + + if ((error = update_branch_remote_config_entry( + remote->repo, + remote->name, + new_name)) < 0) + return error; + + if ((error = rename_remote_references( + remote->repo, + remote->name, + new_name)) < 0) + return error; + + if ((error = rename_fetch_refspecs( + remote, + new_name, + callback, + payload)) < 0) + return error; + } + + git__free(remote->name); + remote->name = git__strdup(new_name); return 0; +} + +int git_remote_update_fetchhead(git_remote *remote) +{ + return remote->update_fetchhead; +} + +void git_remote_set_update_fetchhead(git_remote *remote, int value) +{ + remote->update_fetchhead = value; +} + +int git_remote_is_valid_name( + const char *remote_name) +{ + git_buf buf = GIT_BUF_INIT; + git_refspec refspec; + int error = -1; + + if (!remote_name || *remote_name == '\0') + return 0; + + git_buf_printf(&buf, "refs/heads/test:refs/remotes/%s/test", remote_name); + error = git_refspec__parse(&refspec, git_buf_cstr(&buf), true); -on_error: git_buf_free(&buf); - git_remote_free(*out); - return -1; + git_refspec__free(&refspec); + + giterr_clear(); + return error == 0; } diff --git a/src/remote.h b/src/remote.h index 5a1625d05..4c1a18aa7 100644 --- a/src/remote.h +++ b/src/remote.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -7,19 +7,34 @@ #ifndef INCLUDE_remote_h__ #define INCLUDE_remote_h__ +#include "git2/remote.h" +#include "git2/transport.h" + #include "refspec.h" -#include "transport.h" #include "repository.h" +#define GIT_REMOTE_ORIGIN "origin" + struct git_remote { char *name; char *url; + char *pushurl; git_vector refs; struct git_refspec fetch; struct git_refspec push; + git_cred_acquire_cb cred_acquire_cb; + void *cred_acquire_payload; git_transport *transport; git_repository *repo; - unsigned int need_pack:1; + git_remote_callbacks callbacks; + git_transfer_progress stats; + unsigned int need_pack; + git_remote_autotag_option_t download_tags; + unsigned int check_cert; + unsigned int update_fetchhead; }; +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); + #endif diff --git a/src/repo_template.h b/src/repo_template.h new file mode 100644 index 000000000..099279aa7 --- /dev/null +++ b/src/repo_template.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_repo_template_h__ +#define INCLUDE_repo_template_h__ + +#define GIT_OBJECTS_INFO_DIR GIT_OBJECTS_DIR "info/" +#define GIT_OBJECTS_PACK_DIR GIT_OBJECTS_DIR "pack/" + +#define GIT_HOOKS_DIR "hooks/" +#define GIT_HOOKS_DIR_MODE 0777 + +#define GIT_HOOKS_README_FILE GIT_HOOKS_DIR "README.sample" +#define GIT_HOOKS_README_MODE 0777 +#define GIT_HOOKS_README_CONTENT \ +"#!/bin/sh\n"\ +"#\n"\ +"# Place appropriately named executable hook scripts into this directory\n"\ +"# to intercept various actions that git takes. See `git help hooks` for\n"\ +"# more information.\n" + +#define GIT_INFO_DIR "info/" +#define GIT_INFO_DIR_MODE 0777 + +#define GIT_INFO_EXCLUDE_FILE GIT_INFO_DIR "exclude" +#define GIT_INFO_EXCLUDE_MODE 0666 +#define GIT_INFO_EXCLUDE_CONTENT \ +"# File patterns to ignore; see `git help ignore` for more information.\n"\ +"# Lines that start with '#' are comments.\n" + +#define GIT_DESC_FILE "description" +#define GIT_DESC_MODE 0666 +#define GIT_DESC_CONTENT \ +"Unnamed repository; edit this file 'description' to name the repository.\n" + +typedef struct { + const char *path; + mode_t mode; + const char *content; +} repo_template_item; + +static repo_template_item repo_template[] = { + { GIT_OBJECTS_INFO_DIR, GIT_OBJECT_DIR_MODE, NULL }, /* '/objects/info/' */ + { GIT_OBJECTS_PACK_DIR, GIT_OBJECT_DIR_MODE, NULL }, /* '/objects/pack/' */ + { GIT_REFS_HEADS_DIR, GIT_REFS_DIR_MODE, NULL }, /* '/refs/heads/' */ + { GIT_REFS_TAGS_DIR, GIT_REFS_DIR_MODE, NULL }, /* '/refs/tags/' */ + { GIT_HOOKS_DIR, GIT_HOOKS_DIR_MODE, NULL }, /* '/hooks/' */ + { GIT_INFO_DIR, GIT_INFO_DIR_MODE, NULL }, /* '/info/' */ + { GIT_DESC_FILE, GIT_DESC_MODE, GIT_DESC_CONTENT }, + { GIT_HOOKS_README_FILE, GIT_HOOKS_README_MODE, GIT_HOOKS_README_CONTENT }, + { GIT_INFO_EXCLUDE_FILE, GIT_INFO_EXCLUDE_MODE, GIT_INFO_EXCLUDE_CONTENT }, + { NULL, 0, NULL } +}; + +#endif diff --git a/src/repository.c b/src/repository.c index 6ce3a560f..0ad7449ba 100644 --- a/src/repository.c +++ b/src/repository.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -8,6 +8,7 @@ #include <ctype.h> #include "git2/object.h" +#include "git2/refdb.h" #include "common.h" #include "repository.h" @@ -17,9 +18,10 @@ #include "fileops.h" #include "config.h" #include "refs.h" - -#define GIT_OBJECTS_INFO_DIR GIT_OBJECTS_DIR "info/" -#define GIT_OBJECTS_PACK_DIR GIT_OBJECTS_DIR "pack/" +#include "filter.h" +#include "odb.h" +#include "remote.h" +#include "merge.h" #define GIT_FILE_CONTENT_PREFIX "gitdir:" @@ -27,6 +29,8 @@ #define GIT_REPO_VERSION 0 +#define GIT_TEMPLATE_DIR "/usr/share/git-core/templates" + static void drop_odb(git_repository *repo) { if (repo->_odb != NULL) { @@ -36,6 +40,15 @@ static void drop_odb(git_repository *repo) } } +static void drop_refdb(git_repository *repo) +{ + if (repo->_refdb != NULL) { + GIT_REFCOUNT_OWN(repo->_refdb, NULL); + git_refdb_free(repo->_refdb); + repo->_refdb = NULL; + } +} + static void drop_config(git_repository *repo) { if (repo->_config != NULL) { @@ -62,7 +75,6 @@ void git_repository_free(git_repository *repo) return; git_cache_free(&repo->objects); - git_repository__refcache_free(&repo->references); git_attr_cache_flush(repo); git_submodule_config_free(repo); @@ -72,6 +84,7 @@ void git_repository_free(git_repository *repo) drop_config(repo); drop_index(repo); drop_odb(repo); + drop_refdb(repo); git__free(repo); } @@ -124,11 +137,12 @@ static int load_config_data(git_repository *repo) if (git_repository_config__weakptr(&config, repo) < 0) return -1; + /* Try to figure out if it's bare, default to non-bare if it's not set */ if (git_config_get_bool(&is_bare, config, "core.bare") < 0) - return -1; /* FIXME: We assume that a missing core.bare - variable is an error. Is this right? */ + repo->is_bare = 0; + else + repo->is_bare = is_bare; - repo->is_bare = is_bare; return 0; } @@ -146,8 +160,13 @@ static int load_workdir(git_repository *repo, git_buf *parent_path) return -1; error = git_config_get_string(&worktree, config, "core.worktree"); - if (!error && worktree != NULL) - repo->workdir = git__strdup(worktree); + if (!error && worktree != NULL) { + error = git_path_prettify_dir( + &worktree_buf, worktree, repo->path_repository); + if (error < 0) + return error; + repo->workdir = git_buf_detach(&worktree_buf); + } else if (error != GIT_ENOTFOUND) return error; else { @@ -237,16 +256,17 @@ static int read_gitfile(git_buf *path_out, const char *file_path) git_buf_rtrim(&file); - if (file.size <= prefix_len || - memcmp(file.ptr, GIT_FILE_CONTENT_PREFIX, prefix_len) != 0) + if (git_buf_len(&file) <= prefix_len || + memcmp(git_buf_cstr(&file), GIT_FILE_CONTENT_PREFIX, prefix_len) != 0) { giterr_set(GITERR_REPOSITORY, "The `.git` file at '%s' is malformed", file_path); error = -1; } else if ((error = git_path_dirname_r(path_out, file_path)) >= 0) { - const char *gitlink = ((const char *)file.ptr) + prefix_len; + const char *gitlink = git_buf_cstr(&file) + prefix_len; while (*gitlink && git__isspace(*gitlink)) gitlink++; - error = git_path_prettify_dir(path_out, gitlink, path_out->ptr); + error = git_path_prettify_dir( + path_out, gitlink, git_buf_cstr(path_out)); } git_buf_free(&file); @@ -351,16 +371,18 @@ static int find_repo( int git_repository_open_ext( git_repository **repo_ptr, const char *start_path, - uint32_t flags, + unsigned int flags, const char *ceiling_dirs) { int error; git_buf path = GIT_BUF_INIT, parent = GIT_BUF_INIT; git_repository *repo; - *repo_ptr = NULL; + if (repo_ptr) + *repo_ptr = NULL; - if ((error = find_repo(&path, &parent, start_path, flags, ceiling_dirs)) < 0) + error = find_repo(&path, &parent, start_path, flags, ceiling_dirs); + if (error < 0 || !repo_ptr) return error; repo = repository_alloc(); @@ -387,6 +409,19 @@ int git_repository_open(git_repository **repo_out, const char *path) repo_out, path, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL); } +int git_repository_wrap_odb(git_repository **repo_out, git_odb *odb) +{ + git_repository *repo; + + repo = repository_alloc(); + GITERR_CHECK_ALLOC(repo); + + git_repository_set_odb(repo, odb); + *repo_out = repo; + + return 0; +} + int git_repository_discover( char *repository_path, size_t size, @@ -407,7 +442,7 @@ int git_repository_discover( if (size < (size_t)(path.size + 1)) { giterr_set(GITERR_REPOSITORY, - "The given buffer is too long to store the discovered path"); + "The given buffer is too small to store the discovered path"); git_buf_free(&path); return -1; } @@ -422,34 +457,49 @@ static int load_config( git_config **out, git_repository *repo, const char *global_config_path, + const char *xdg_config_path, const char *system_config_path) { + int error; git_buf config_path = GIT_BUF_INIT; git_config *cfg = NULL; assert(repo && out); - if (git_config_new(&cfg) < 0) - return -1; + if ((error = git_config_new(&cfg)) < 0) + return error; - if (git_buf_joinpath( - &config_path, repo->path_repository, GIT_CONFIG_FILENAME_INREPO) < 0) + error = git_buf_joinpath( + &config_path, repo->path_repository, GIT_CONFIG_FILENAME_INREPO); + if (error < 0) goto on_error; - if (git_config_add_file_ondisk(cfg, config_path.ptr, 3) < 0) + if ((error = git_config_add_file_ondisk( + cfg, config_path.ptr, GIT_CONFIG_LEVEL_LOCAL, 0)) < 0 && + error != GIT_ENOTFOUND) goto on_error; git_buf_free(&config_path); - if (global_config_path != NULL) { - if (git_config_add_file_ondisk(cfg, global_config_path, 2) < 0) - goto on_error; - } + if (global_config_path != NULL && + (error = git_config_add_file_ondisk( + cfg, global_config_path, GIT_CONFIG_LEVEL_GLOBAL, 0)) < 0 && + error != GIT_ENOTFOUND) + goto on_error; - if (system_config_path != NULL) { - if (git_config_add_file_ondisk(cfg, system_config_path, 1) < 0) - goto on_error; - } + if (xdg_config_path != NULL && + (error = git_config_add_file_ondisk( + cfg, xdg_config_path, GIT_CONFIG_LEVEL_XDG, 0)) < 0 && + error != GIT_ENOTFOUND) + goto on_error; + + if (system_config_path != NULL && + (error = git_config_add_file_ondisk( + cfg, system_config_path, GIT_CONFIG_LEVEL_SYSTEM, 0)) < 0 && + error != GIT_ENOTFOUND) + goto on_error; + + giterr_clear(); /* clear any lingering ENOTFOUND errors */ *out = cfg; return 0; @@ -458,27 +508,32 @@ on_error: git_buf_free(&config_path); git_config_free(cfg); *out = NULL; - return -1; + return error; } int git_repository_config__weakptr(git_config **out, git_repository *repo) { if (repo->_config == NULL) { - git_buf global_buf = GIT_BUF_INIT, system_buf = GIT_BUF_INIT; + 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; + if (git_config_find_xdg_r(&xdg_buf) == 0) + xdg_config_path = xdg_buf.ptr; + if (git_config_find_system_r(&system_buf) == 0) system_config_path = system_buf.ptr; - res = load_config(&repo->_config, repo, global_config_path, system_config_path); + res = load_config(&repo->_config, repo, global_config_path, xdg_config_path, system_config_path); git_buf_free(&global_buf); + git_buf_free(&xdg_buf); git_buf_free(&system_buf); if (res < 0) @@ -508,6 +563,7 @@ void git_repository_set_config(git_repository *repo, git_config *config) repo->_config = config; GIT_REFCOUNT_OWN(repo->_config, repo); + GIT_REFCOUNT_INC(repo->_config); } int git_repository_odb__weakptr(git_odb **out, git_repository *repo) @@ -554,6 +610,45 @@ void git_repository_set_odb(git_repository *repo, git_odb *odb) GIT_REFCOUNT_INC(odb); } +int git_repository_refdb__weakptr(git_refdb **out, git_repository *repo) +{ + assert(out && repo); + + if (repo->_refdb == NULL) { + int res; + + res = git_refdb_open(&repo->_refdb, repo); + + if (res < 0) + return -1; + + GIT_REFCOUNT_OWN(repo->_refdb, repo); + } + + *out = repo->_refdb; + return 0; +} + +int git_repository_refdb(git_refdb **out, git_repository *repo) +{ + if (git_repository_refdb__weakptr(out, repo) < 0) + return -1; + + GIT_REFCOUNT_INC(*out); + return 0; +} + +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); +} + int git_repository_index__weakptr(git_index **out, git_repository *repo) { assert(out && repo); @@ -572,6 +667,9 @@ int git_repository_index__weakptr(git_index **out, git_repository *repo) return -1; GIT_REFCOUNT_OWN(repo->_index, repo); + + if (git_index_set_caps(repo->_index, GIT_INDEXCAP_FROM_OWNER) < 0) + return -1; } *out = repo->_index; @@ -598,14 +696,10 @@ void git_repository_set_index(git_repository *repo, git_index *index) GIT_REFCOUNT_INC(index); } -static int check_repositoryformatversion(git_repository *repo) +static int check_repositoryformatversion(git_config *config) { - git_config *config; int version; - if (git_repository_config__weakptr(&config, repo) < 0) - return -1; - if (git_config_get_int32(&version, config, "core.repositoryformatversion") < 0) return -1; @@ -619,105 +713,215 @@ static int check_repositoryformatversion(git_repository *repo) return 0; } -static int repo_init_reinit(git_repository **repo_out, const char *repository_path, int is_bare) +static int repo_init_create_head(const char *git_dir, const char *ref_name) { - git_repository *repo = NULL; + git_buf ref_path = GIT_BUF_INIT; + git_filebuf ref = GIT_FILEBUF_INIT; + const char *fmt; - GIT_UNUSED(is_bare); + if (git_buf_joinpath(&ref_path, git_dir, GIT_HEAD_FILE) < 0 || + git_filebuf_open(&ref, ref_path.ptr, 0) < 0) + goto fail; - if (git_repository_open(&repo, repository_path) < 0) - return -1; + if (!ref_name) + ref_name = GIT_BRANCH_MASTER; - if (check_repositoryformatversion(repo) < 0) { - git_repository_free(repo); - return -1; - } + if (git__prefixcmp(ref_name, GIT_REFS_DIR) == 0) + fmt = "ref: %s\n"; + else + fmt = "ref: " GIT_REFS_HEADS_DIR "%s\n"; - /* TODO: reinitialize the templates */ + if (git_filebuf_printf(&ref, fmt, ref_name) < 0 || + git_filebuf_commit(&ref, GIT_REFS_FILE_MODE) < 0) + goto fail; - *repo_out = repo; + git_buf_free(&ref_path); return 0; + +fail: + git_buf_free(&ref_path); + git_filebuf_cleanup(&ref); + return -1; } -static int repo_init_createhead(const char *git_dir) +static bool is_chmod_supported(const char *file_path) { - git_buf ref_path = GIT_BUF_INIT; - git_filebuf ref = GIT_FILEBUF_INIT; + struct stat st1, st2; + static int _is_supported = -1; - if (git_buf_joinpath(&ref_path, git_dir, GIT_HEAD_FILE) < 0 || - git_filebuf_open(&ref, ref_path.ptr, 0) < 0 || - git_filebuf_printf(&ref, "ref: refs/heads/master\n") < 0 || - git_filebuf_commit(&ref, GIT_REFS_FILE_MODE) < 0) + if (_is_supported > -1) + return _is_supported; + + if (p_stat(file_path, &st1) < 0) + return false; + + if (p_chmod(file_path, st1.st_mode ^ S_IXUSR) < 0) + return false; + + if (p_stat(file_path, &st2) < 0) + return false; + + _is_supported = (st1.st_mode != st2.st_mode); + + return _is_supported; +} + +static bool is_filesystem_case_insensitive(const char *gitdir_path) +{ + git_buf path = GIT_BUF_INIT; + static int _is_insensitive = -1; + + if (_is_insensitive > -1) + return _is_insensitive; + + if (git_buf_joinpath(&path, gitdir_path, "CoNfIg") < 0) + goto cleanup; + + _is_insensitive = git_path_exists(git_buf_cstr(&path)); + +cleanup: + git_buf_free(&path); + return _is_insensitive; +} + +static bool are_symlinks_supported(const char *wd_path) +{ + git_buf path = GIT_BUF_INIT; + int fd; + struct stat st; + static int _symlinks_supported = -1; + + if (_symlinks_supported > -1) + return _symlinks_supported; + + if ((fd = git_futils_mktmp(&path, wd_path)) < 0 || + p_close(fd) < 0 || + p_unlink(path.ptr) < 0 || + p_symlink("testing", path.ptr) < 0 || + p_lstat(path.ptr, &st) < 0) + _symlinks_supported = false; + else + _symlinks_supported = (S_ISLNK(st.st_mode) != 0); + + (void)p_unlink(path.ptr); + git_buf_free(&path); + + return _symlinks_supported; +} + +static int create_empty_file(const char *path, mode_t mode) +{ + int fd; + + if ((fd = p_creat(path, mode)) < 0) { + giterr_set(GITERR_OS, "Error while creating '%s'", path); return -1; + } + + if (p_close(fd) < 0) { + giterr_set(GITERR_OS, "Error while closing '%s'", path); + return -1; + } - git_buf_free(&ref_path); return 0; } -static int repo_init_config(const char *git_dir, int is_bare) +static int repo_init_config( + const char *repo_dir, + const char *work_dir, + git_repository_init_options *opts) { + int error = 0; git_buf cfg_path = GIT_BUF_INIT; git_config *config = NULL; -#define SET_REPO_CONFIG(type, name, val) {\ - if (git_config_set_##type(config, name, val) < 0) { \ - git_buf_free(&cfg_path); \ - git_config_free(config); \ - return -1; } \ -} +#define SET_REPO_CONFIG(TYPE, NAME, VAL) do {\ + if ((error = git_config_set_##TYPE(config, NAME, VAL)) < 0) \ + goto cleanup; } while (0) - if (git_buf_joinpath(&cfg_path, git_dir, GIT_CONFIG_FILENAME_INREPO) < 0) + if (git_buf_joinpath(&cfg_path, repo_dir, GIT_CONFIG_FILENAME_INREPO) < 0) return -1; - if (git_config_open_ondisk(&config, cfg_path.ptr) < 0) { + if (!git_path_isfile(git_buf_cstr(&cfg_path)) && + create_empty_file(git_buf_cstr(&cfg_path), GIT_CONFIG_FILE_MODE) < 0) { + git_buf_free(&cfg_path); + return -1; + } + + if (git_config_open_ondisk(&config, git_buf_cstr(&cfg_path)) < 0) { git_buf_free(&cfg_path); return -1; } - SET_REPO_CONFIG(bool, "core.bare", is_bare); - SET_REPO_CONFIG(int32, "core.repositoryformatversion", GIT_REPO_VERSION); - /* TODO: what other defaults? */ + if ((opts->flags & GIT_REPOSITORY_INIT__IS_REINIT) != 0 && + (error = check_repositoryformatversion(config)) < 0) + goto cleanup; - git_buf_free(&cfg_path); - git_config_free(config); - return 0; -} + SET_REPO_CONFIG( + bool, "core.bare", (opts->flags & GIT_REPOSITORY_INIT_BARE) != 0); + SET_REPO_CONFIG( + int32, "core.repositoryformatversion", GIT_REPO_VERSION); + SET_REPO_CONFIG( + bool, "core.filemode", is_chmod_supported(git_buf_cstr(&cfg_path))); -#define GIT_HOOKS_DIR "hooks/" -#define GIT_HOOKS_DIR_MODE 0755 + if (!(opts->flags & GIT_REPOSITORY_INIT_BARE)) { + SET_REPO_CONFIG(bool, "core.logallrefupdates", true); -#define GIT_HOOKS_README_FILE GIT_HOOKS_DIR "README.sample" -#define GIT_HOOKS_README_MODE 0755 -#define GIT_HOOKS_README_CONTENT \ -"#!/bin/sh\n"\ -"#\n"\ -"# Place appropriately named executable hook scripts into this directory\n"\ -"# to intercept various actions that git takes. See `git help hooks` for\n"\ -"# more information.\n" + if (!are_symlinks_supported(work_dir)) + SET_REPO_CONFIG(bool, "core.symlinks", false); -#define GIT_INFO_DIR "info/" -#define GIT_INFO_DIR_MODE 0755 + if (!(opts->flags & GIT_REPOSITORY_INIT__NATURAL_WD)) { + SET_REPO_CONFIG(string, "core.worktree", work_dir); + } + else if ((opts->flags & GIT_REPOSITORY_INIT__IS_REINIT) != 0) { + if (git_config_delete_entry(config, "core.worktree") < 0) + giterr_clear(); + } + } else { + if (!are_symlinks_supported(repo_dir)) + SET_REPO_CONFIG(bool, "core.symlinks", false); + } -#define GIT_INFO_EXCLUDE_FILE GIT_INFO_DIR "exclude" -#define GIT_INFO_EXCLUDE_MODE 0644 -#define GIT_INFO_EXCLUDE_CONTENT \ -"# File patterns to ignore; see `git help ignore` for more information.\n"\ -"# Lines that start with '#' are comments.\n" + if (!(opts->flags & GIT_REPOSITORY_INIT__IS_REINIT) && + is_filesystem_case_insensitive(repo_dir)) + SET_REPO_CONFIG(bool, "core.ignorecase", true); -#define GIT_DESC_FILE "description" -#define GIT_DESC_MODE 0644 -#define GIT_DESC_CONTENT "Unnamed repository; edit this file 'description' to name the repository.\n" + if (opts->mode == GIT_REPOSITORY_INIT_SHARED_GROUP) { + SET_REPO_CONFIG(int32, "core.sharedrepository", 1); + SET_REPO_CONFIG(bool, "receive.denyNonFastforwards", true); + } + else if (opts->mode == GIT_REPOSITORY_INIT_SHARED_ALL) { + SET_REPO_CONFIG(int32, "core.sharedrepository", 2); + SET_REPO_CONFIG(bool, "receive.denyNonFastforwards", true); + } + +cleanup: + git_buf_free(&cfg_path); + git_config_free(config); + + return error; +} static int repo_write_template( - const char *git_dir, const char *file, mode_t mode, const char *content) + const char *git_dir, + bool allow_overwrite, + const char *file, + mode_t mode, + bool hidden, + const char *content) { git_buf path = GIT_BUF_INIT; - int fd, error = 0; + int fd, error = 0, flags; if (git_buf_joinpath(&path, git_dir, file) < 0) return -1; - fd = p_open(git_buf_cstr(&path), O_WRONLY | O_CREAT | O_EXCL, mode); + if (allow_overwrite) + flags = O_WRONLY | O_CREAT | O_TRUNC; + else + flags = O_WRONLY | O_CREAT | O_EXCL; + + fd = p_open(git_buf_cstr(&path), flags, mode); if (fd >= 0) { error = p_write(fd, content, strlen(content)); @@ -727,6 +931,15 @@ static int repo_write_template( else if (errno != EEXIST) error = fd; +#ifdef GIT_WIN32 + if (!error && hidden) { + if (p_hide_directory__w32(path.ptr) < 0) + error = -1; + } +#else + GIT_UNUSED(hidden); +#endif + git_buf_free(&path); if (error) @@ -736,83 +949,369 @@ static int repo_write_template( return error; } -static int repo_init_structure(const char *git_dir, int is_bare) -{ - int i; - struct { const char *dir; mode_t mode; } dirs[] = { - { GIT_OBJECTS_INFO_DIR, GIT_OBJECT_DIR_MODE }, /* '/objects/info/' */ - { GIT_OBJECTS_PACK_DIR, GIT_OBJECT_DIR_MODE }, /* '/objects/pack/' */ - { GIT_REFS_HEADS_DIR, GIT_REFS_DIR_MODE }, /* '/refs/heads/' */ - { GIT_REFS_TAGS_DIR, GIT_REFS_DIR_MODE }, /* '/refs/tags/' */ - { GIT_HOOKS_DIR, GIT_HOOKS_DIR_MODE }, /* '/hooks/' */ - { GIT_INFO_DIR, GIT_INFO_DIR_MODE }, /* '/info/' */ - { NULL, 0 } - }; - struct { const char *file; mode_t mode; const char *content; } tmpl[] = { - { GIT_DESC_FILE, GIT_DESC_MODE, GIT_DESC_CONTENT }, - { GIT_HOOKS_README_FILE, GIT_HOOKS_README_MODE, GIT_HOOKS_README_CONTENT }, - { GIT_INFO_EXCLUDE_FILE, GIT_INFO_EXCLUDE_MODE, GIT_INFO_EXCLUDE_CONTENT }, - { NULL, 0, NULL } - }; - - /* Make the base directory */ - if (git_futils_mkdir_r(git_dir, NULL, is_bare ? GIT_BARE_DIR_MODE : GIT_DIR_MODE) < 0) +static int repo_write_gitlink( + const char *in_dir, const char *to_repo) +{ + int error; + git_buf buf = GIT_BUF_INIT; + struct stat st; + + git_path_dirname_r(&buf, to_repo); + git_path_to_dir(&buf); + if (git_buf_oom(&buf)) return -1; - /* Hides the ".git" directory */ - if (!is_bare) { + /* don't write gitlink to natural workdir */ + if (git__suffixcmp(to_repo, "/" DOT_GIT "/") == 0 && + strcmp(in_dir, buf.ptr) == 0) + { + error = GIT_PASSTHROUGH; + goto cleanup; + } + + if ((error = git_buf_joinpath(&buf, in_dir, DOT_GIT)) < 0) + goto cleanup; + + if (!p_stat(buf.ptr, &st) && !S_ISREG(st.st_mode)) { + giterr_set(GITERR_REPOSITORY, + "Cannot overwrite gitlink file into path '%s'", in_dir); + error = GIT_EEXISTS; + goto cleanup; + } + + git_buf_clear(&buf); + + error = git_buf_printf(&buf, "%s %s", GIT_FILE_CONTENT_PREFIX, to_repo); + + if (!error) + error = repo_write_template(in_dir, true, DOT_GIT, 0666, true, buf.ptr); + +cleanup: + git_buf_free(&buf); + return error; +} + +static mode_t pick_dir_mode(git_repository_init_options *opts) +{ + if (opts->mode == GIT_REPOSITORY_INIT_SHARED_UMASK) + return 0777; + if (opts->mode == GIT_REPOSITORY_INIT_SHARED_GROUP) + return (0775 | S_ISGID); + if (opts->mode == GIT_REPOSITORY_INIT_SHARED_ALL) + return (0777 | S_ISGID); + return opts->mode; +} + +#include "repo_template.h" + +static int repo_init_structure( + const char *repo_dir, + const char *work_dir, + git_repository_init_options *opts) +{ + int error = 0; + repo_template_item *tpl; + bool external_tpl = + ((opts->flags & GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE) != 0); + mode_t dmode = pick_dir_mode(opts); + + /* Hide the ".git" directory */ #ifdef GIT_WIN32 - if (p_hide_directory__w32(git_dir) < 0) { + if ((opts->flags & GIT_REPOSITORY_INIT__HAS_DOTGIT) != 0) { + if (p_hide_directory__w32(repo_dir) < 0) { giterr_set(GITERR_REPOSITORY, "Failed to mark Git repository folder as hidden"); return -1; } -#endif } +#endif - /* Make subdirectories as needed */ - for (i = 0; dirs[i].dir != NULL; ++i) { - if (git_futils_mkdir_r(dirs[i].dir, git_dir, dirs[i].mode) < 0) + /* Create the .git gitlink if appropriate */ + if ((opts->flags & GIT_REPOSITORY_INIT_BARE) == 0 && + (opts->flags & GIT_REPOSITORY_INIT__NATURAL_WD) == 0) + { + if (repo_write_gitlink(work_dir, repo_dir) < 0) return -1; } - /* Make template files as needed */ - for (i = 0; tmpl[i].file != NULL; ++i) { - if (repo_write_template( - git_dir, tmpl[i].file, tmpl[i].mode, tmpl[i].content) < 0) - return -1; + /* Copy external template if requested */ + if (external_tpl) { + git_config *cfg; + const char *tdir; + + if (opts->template_path) + tdir = opts->template_path; + else if ((error = git_config_open_default(&cfg)) < 0) + return error; + else { + error = git_config_get_string(&tdir, cfg, "init.templatedir"); + + git_config_free(cfg); + + if (error && error != GIT_ENOTFOUND) + return error; + + giterr_clear(); + tdir = GIT_TEMPLATE_DIR; + } + + error = git_futils_cp_r(tdir, repo_dir, + GIT_CPDIR_COPY_SYMLINKS | GIT_CPDIR_CHMOD_DIRS | + GIT_CPDIR_SIMPLE_TO_MODE, dmode); + + if (error < 0) { + if (strcmp(tdir, GIT_TEMPLATE_DIR) != 0) + return error; + + /* if template was default, ignore error and use internal */ + giterr_clear(); + external_tpl = false; + error = 0; + } } - return 0; + /* Copy internal template + * - always ensure existence of dirs + * - only create files if no external template was specified + */ + for (tpl = repo_template; !error && tpl->path; ++tpl) { + if (!tpl->content) + error = git_futils_mkdir( + tpl->path, repo_dir, dmode, GIT_MKDIR_PATH | GIT_MKDIR_CHMOD); + else if (!external_tpl) { + const char *content = tpl->content; + + if (opts->description && strcmp(tpl->path, GIT_DESC_FILE) == 0) + content = opts->description; + + error = repo_write_template( + repo_dir, false, tpl->path, tpl->mode, false, content); + } + } + + return error; } -int git_repository_init(git_repository **repo_out, const char *path, unsigned is_bare) +static int mkdir_parent(git_buf *buf, uint32_t mode, bool skip2) { - git_buf repository_path = GIT_BUF_INIT; - - assert(repo_out && path); + /* When making parent directories during repository initialization + * don't try to set gid or grant world write access + */ + return git_futils_mkdir( + buf->ptr, NULL, mode & ~(S_ISGID | 0002), + GIT_MKDIR_PATH | GIT_MKDIR_VERIFY_DIR | + (skip2 ? GIT_MKDIR_SKIP_LAST2 : GIT_MKDIR_SKIP_LAST)); +} - if (git_buf_joinpath(&repository_path, path, is_bare ? "" : GIT_DIR) < 0) +static int repo_init_directories( + git_buf *repo_path, + git_buf *wd_path, + const char *given_repo, + git_repository_init_options *opts) +{ + int error = 0; + bool is_bare, add_dotgit, has_dotgit, natural_wd; + mode_t dirmode; + + /* There are three possible rules for what we are allowed to create: + * - MKPATH means anything we need + * - MKDIR means just the .git directory and its parent and the workdir + * - Neither means only the .git directory can be created + * + * There are 5 "segments" of path that we might need to deal with: + * 1. The .git directory + * 2. The parent of the .git directory + * 3. Everything above the parent of the .git directory + * 4. The working directory (often the same as #2) + * 5. Everything above the working directory (often the same as #3) + * + * For all directories created, we start with the init_mode value for + * permissions and then strip off bits in some cases: + * + * For MKPATH, we create #3 (and #5) paths without S_ISGID or S_IWOTH + * For MKPATH and MKDIR, we create #2 (and #4) without S_ISGID + * For all rules, we create #1 using the untouched init_mode + */ + + /* set up repo path */ + + is_bare = ((opts->flags & GIT_REPOSITORY_INIT_BARE) != 0); + + add_dotgit = + (opts->flags & GIT_REPOSITORY_INIT_NO_DOTGIT_DIR) == 0 && + !is_bare && + git__suffixcmp(given_repo, "/" DOT_GIT) != 0 && + git__suffixcmp(given_repo, "/" GIT_DIR) != 0; + + if (git_buf_joinpath(repo_path, given_repo, add_dotgit ? GIT_DIR : "") < 0) return -1; - if (git_path_isdir(repository_path.ptr) == true) { - if (valid_repository_path(&repository_path) == true) { - int res = repo_init_reinit(repo_out, repository_path.ptr, is_bare); - git_buf_free(&repository_path); - return res; + has_dotgit = (git__suffixcmp(repo_path->ptr, "/" GIT_DIR) == 0); + if (has_dotgit) + opts->flags |= GIT_REPOSITORY_INIT__HAS_DOTGIT; + + /* set up workdir path */ + + if (!is_bare) { + if (opts->workdir_path) { + if (git_path_join_unrooted( + wd_path, opts->workdir_path, repo_path->ptr, NULL) < 0) + return -1; + } else if (has_dotgit) { + if (git_path_dirname_r(wd_path, repo_path->ptr) < 0) + return -1; + } else { + giterr_set(GITERR_REPOSITORY, "Cannot pick working directory" + " for non-bare repository that isn't a '.git' directory"); + return -1; } + + if (git_path_to_dir(wd_path) < 0) + return -1; + } else { + git_buf_clear(wd_path); } - if (repo_init_structure(repository_path.ptr, is_bare) < 0 || - repo_init_config(repository_path.ptr, is_bare) < 0 || - repo_init_createhead(repository_path.ptr) < 0 || - git_repository_open(repo_out, repository_path.ptr) < 0) { - git_buf_free(&repository_path); - return -1; + natural_wd = + has_dotgit && + wd_path->size > 0 && + wd_path->size + strlen(GIT_DIR) == repo_path->size && + memcmp(repo_path->ptr, wd_path->ptr, wd_path->size) == 0; + if (natural_wd) + opts->flags |= GIT_REPOSITORY_INIT__NATURAL_WD; + + /* create directories as needed / requested */ + + dirmode = pick_dir_mode(opts); + + if ((opts->flags & GIT_REPOSITORY_INIT_MKPATH) != 0) { + /* create path #5 */ + if (wd_path->size > 0 && + (error = mkdir_parent(wd_path, dirmode, false)) < 0) + return error; + + /* create path #3 (if not the same as #5) */ + if (!natural_wd && + (error = mkdir_parent(repo_path, dirmode, has_dotgit)) < 0) + return error; } - git_buf_free(&repository_path); - return 0; + if ((opts->flags & GIT_REPOSITORY_INIT_MKDIR) != 0 || + (opts->flags & GIT_REPOSITORY_INIT_MKPATH) != 0) + { + /* create path #4 */ + if (wd_path->size > 0 && + (error = git_futils_mkdir( + wd_path->ptr, NULL, dirmode & ~S_ISGID, + GIT_MKDIR_VERIFY_DIR)) < 0) + return error; + + /* create path #2 (if not the same as #4) */ + if (!natural_wd && + (error = git_futils_mkdir( + repo_path->ptr, NULL, dirmode & ~S_ISGID, + GIT_MKDIR_VERIFY_DIR | GIT_MKDIR_SKIP_LAST)) < 0) + return error; + } + + if ((opts->flags & GIT_REPOSITORY_INIT_MKDIR) != 0 || + (opts->flags & GIT_REPOSITORY_INIT_MKPATH) != 0 || + has_dotgit) + { + /* create path #1 */ + error = git_futils_mkdir(repo_path->ptr, NULL, dirmode, + GIT_MKDIR_VERIFY_DIR | ((dirmode & S_ISGID) ? GIT_MKDIR_CHMOD : 0)); + } + + /* prettify both directories now that they are created */ + + if (!error) { + error = git_path_prettify_dir(repo_path, repo_path->ptr, NULL); + + if (!error && wd_path->size > 0) + error = git_path_prettify_dir(wd_path, wd_path->ptr, NULL); + } + + return error; +} + +static int repo_init_create_origin(git_repository *repo, const char *url) +{ + int error; + git_remote *remote; + + if (!(error = git_remote_create(&remote, repo, GIT_REMOTE_ORIGIN, url))) { + git_remote_free(remote); + } + + return error; +} + +int git_repository_init( + git_repository **repo_out, const char *path, unsigned is_bare) +{ + git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT; + + opts.flags = GIT_REPOSITORY_INIT_MKPATH; /* don't love this default */ + if (is_bare) + opts.flags |= GIT_REPOSITORY_INIT_BARE; + + return git_repository_init_ext(repo_out, path, &opts); +} + +int git_repository_init_ext( + git_repository **out, + const char *given_repo, + git_repository_init_options *opts) +{ + int error; + git_buf repo_path = GIT_BUF_INIT, wd_path = GIT_BUF_INIT; + + assert(out && given_repo && opts); + + GITERR_CHECK_VERSION(opts, GIT_REPOSITORY_INIT_OPTIONS_VERSION, "git_repository_init_options"); + + error = repo_init_directories(&repo_path, &wd_path, given_repo, opts); + if (error < 0) + goto cleanup; + + if (valid_repository_path(&repo_path)) { + + if ((opts->flags & GIT_REPOSITORY_INIT_NO_REINIT) != 0) { + giterr_set(GITERR_REPOSITORY, + "Attempt to reinitialize '%s'", given_repo); + error = GIT_EEXISTS; + goto cleanup; + } + + opts->flags |= GIT_REPOSITORY_INIT__IS_REINIT; + + error = repo_init_config( + git_buf_cstr(&repo_path), git_buf_cstr(&wd_path), opts); + + /* TODO: reinitialize the templates */ + } + else { + if (!(error = repo_init_structure( + git_buf_cstr(&repo_path), git_buf_cstr(&wd_path), opts)) && + !(error = repo_init_config( + git_buf_cstr(&repo_path), git_buf_cstr(&wd_path), opts))) + error = repo_init_create_head( + git_buf_cstr(&repo_path), opts->initial_head); + } + if (error < 0) + goto cleanup; + + error = git_repository_open(out, git_buf_cstr(&repo_path)); + + if (!error && opts->origin_url) + error = repo_init_create_origin(*out, opts->origin_url); + +cleanup: + git_buf_free(&repo_path); + git_buf_free(&wd_path); + + return error; } int git_repository_head_detached(git_repository *repo) @@ -832,7 +1331,7 @@ int git_repository_head_detached(git_repository *repo) return 0; } - exists = git_odb_exists(odb, git_reference_oid(ref)); + exists = git_odb_exists(odb, git_reference_target(ref)); git_reference_free(ref); return exists; @@ -840,7 +1339,21 @@ int git_repository_head_detached(git_repository *repo) int git_repository_head(git_reference **head_out, git_repository *repo) { - return git_reference_lookup_resolved(head_out, repo, GIT_HEAD_FILE, -1); + git_reference *head; + int error; + + if ((error = git_reference_lookup(&head, repo, GIT_HEAD_FILE)) < 0) + return error; + + if (git_reference_type(head) == GIT_REF_OID) { + *head_out = head; + return 0; + } + + error = git_reference_lookup_resolved(head_out, repo, git_reference_symbolic_target(head), -1); + git_reference_free(head); + + return error == GIT_ENOTFOUND ? GIT_EORPHANEDHEAD : error; } int git_repository_head_orphan(git_repository *repo) @@ -851,7 +1364,7 @@ int git_repository_head_orphan(git_repository *repo) error = git_repository_head(&ref, repo); git_reference_free(ref); - if (error == GIT_ENOTFOUND) + if (error == GIT_EORPHANEDHEAD) return 1; if (error < 0) @@ -860,36 +1373,47 @@ int git_repository_head_orphan(git_repository *repo) return 0; } -int git_repository_is_empty(git_repository *repo) +static int at_least_one_cb(const char *refname, void *payload) { - git_reference *head = NULL, *branch = NULL; - int error; + GIT_UNUSED(refname); + GIT_UNUSED(payload); - if (git_reference_lookup(&head, repo, "HEAD") < 0) - return -1; + return GIT_EUSER; +} - if (git_reference_type(head) != GIT_REF_SYMBOLIC) { - git_reference_free(head); - return 0; - } +static int repo_contains_no_reference(git_repository *repo) +{ + int error; + + error = git_reference_foreach(repo, GIT_REF_LISTALL, at_least_one_cb, NULL); - if (strcmp(git_reference_target(head), "refs/heads/master") != 0) { - git_reference_free(head); + if (error == GIT_EUSER) return 0; - } - error = git_reference_resolve(&branch, head); - - git_reference_free(head); - git_reference_free(branch); + return error == 0 ? 1 : error; +} - if (error == GIT_ENOTFOUND) - return 1; +int git_repository_is_empty(git_repository *repo) +{ + git_reference *head = NULL; + int error; - if (error < 0) + if (git_reference_lookup(&head, repo, GIT_HEAD_FILE) < 0) return -1; - return 0; + if (!(error = git_reference_type(head) == GIT_REF_SYMBOLIC)) + goto cleanup; + + if (!(error = strcmp( + git_reference_symbolic_target(head), + GIT_REFS_HEADS_DIR "master") == 0)) + goto cleanup; + + error = repo_contains_no_reference(repo); + +cleanup: + git_reference_free(head); + return error < 0 ? -1 : error; } const char *git_repository_path(git_repository *repo) @@ -908,8 +1432,10 @@ const char *git_repository_workdir(git_repository *repo) return repo->workdir; } -int git_repository_set_workdir(git_repository *repo, const char *workdir) +int git_repository_set_workdir( + git_repository *repo, const char *workdir, int update_gitlink) { + int error = 0; git_buf path = GIT_BUF_INIT; assert(repo && workdir); @@ -917,11 +1443,37 @@ int git_repository_set_workdir(git_repository *repo, const char *workdir) if (git_path_prettify_dir(&path, workdir, NULL) < 0) return -1; - git__free(repo->workdir); + if (repo->workdir && strcmp(repo->workdir, path.ptr) == 0) + return 0; - repo->workdir = git_buf_detach(&path); - repo->is_bare = 0; - return 0; + if (update_gitlink) { + git_config *config; + + if (git_repository_config__weakptr(&config, repo) < 0) + return -1; + + error = repo_write_gitlink(path.ptr, git_repository_path(repo)); + + /* passthrough error means gitlink is unnecessary */ + if (error == GIT_PASSTHROUGH) + error = git_config_delete_entry(config, "core.worktree"); + else if (!error) + error = git_config_set_string(config, "core.worktree", path.ptr); + + if (!error) + error = git_config_set_bool(config, "core.bare", false); + } + + if (!error) { + char *old_workdir = repo->workdir; + + repo->workdir = git_buf_detach(&path); + repo->is_bare = 0; + + git__free(old_workdir); + } + + return error; } int git_repository_is_bare(git_repository *repo) @@ -932,20 +1484,248 @@ int git_repository_is_bare(git_repository *repo) int git_repository_head_tree(git_tree **tree, git_repository *repo) { - git_oid head_oid; - git_object *obj = NULL; + git_reference *head; + git_object *obj; + int error; - if (git_reference_name_to_oid(&head_oid, repo, GIT_HEAD_FILE) < 0) { - /* cannot resolve HEAD - probably brand new repo */ - giterr_clear(); - *tree = NULL; - return 0; + if ((error = git_repository_head(&head, repo)) < 0) + return error; + + if ((error = git_reference_peel(&obj, head, GIT_OBJ_TREE)) < 0) + goto cleanup; + + *tree = (git_tree *)obj; + +cleanup: + git_reference_free(head); + return error; +} + +int git_repository_message(char *buffer, size_t len, git_repository *repo) +{ + git_buf buf = GIT_BUF_INIT, path = GIT_BUF_INIT; + struct stat st; + int error; + + 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; + } + else if (buffer != NULL) { + error = git_futils_readbuffer(&buf, git_buf_cstr(&path)); + git_buf_copy_cstr(buffer, len, &buf); } - if (git_object_lookup(&obj, repo, &head_oid, GIT_OBJ_ANY) < 0 || - git_object__resolve_to_type(&obj, GIT_OBJ_TREE) < 0) + git_buf_free(&path); + git_buf_free(&buf); + + if (!error) + error = (int)st.st_size + 1; /* add 1 for NUL byte */ + + return error; +} + +int git_repository_message_remove(git_repository *repo) +{ + git_buf path = GIT_BUF_INIT; + int error; + + if (git_buf_joinpath(&path, repo->path_repository, GIT_MERGE_MSG_FILE) < 0) return -1; - *tree = (git_tree *)obj; - return 0; + error = p_unlink(git_buf_cstr(&path)); + git_buf_free(&path); + + return error; +} + +int git_repository_hashfile( + git_oid *out, + git_repository *repo, + const char *path, + git_otype type, + const char *as_path) +{ + int error; + git_vector filters = GIT_VECTOR_INIT; + git_file fd = -1; + git_off_t len; + git_buf full_path = GIT_BUF_INIT; + + assert(out && path && repo); /* as_path can be NULL */ + + /* At some point, it would be nice if repo could be NULL to just + * apply filter rules defined in system and global files, but for + * now that is not possible because git_filters_load() needs it. + */ + + error = git_path_join_unrooted( + &full_path, path, repo ? git_repository_workdir(repo) : NULL, NULL); + if (error < 0) + return error; + + if (!as_path) + as_path = path; + + /* passing empty string for "as_path" indicated --no-filters */ + if (strlen(as_path) > 0) { + error = git_filters_load(&filters, repo, as_path, GIT_FILTER_TO_ODB); + if (error < 0) + return error; + } else { + error = 0; + } + + /* at this point, error is a count of the number of loaded filters */ + + fd = git_futils_open_ro(full_path.ptr); + if (fd < 0) { + error = fd; + goto cleanup; + } + + len = git_futils_filesize(fd); + if (len < 0) { + error = (int)len; + goto cleanup; + } + + if (!git__is_sizet(len)) { + giterr_set(GITERR_OS, "File size overflow for 32-bit systems"); + error = -1; + goto cleanup; + } + + error = git_odb__hashfd_filtered(out, fd, (size_t)len, type, &filters); + +cleanup: + if (fd >= 0) + p_close(fd); + git_filters_free(&filters); + git_buf_free(&full_path); + + return error; +} + +static bool looks_like_a_branch(const char *refname) +{ + return git__prefixcmp(refname, GIT_REFS_HEADS_DIR) == 0; +} + +int git_repository_set_head( + git_repository* repo, + const char* refname) +{ + git_reference *ref, + *new_head = NULL; + int error; + + assert(repo && refname); + + error = git_reference_lookup(&ref, repo, refname); + if (error < 0 && error != GIT_ENOTFOUND) + return error; + + if (!error) { + if (git_reference_is_branch(ref)) + error = git_reference_symbolic_create(&new_head, repo, GIT_HEAD_FILE, git_reference_name(ref), 1); + else + error = git_repository_set_head_detached(repo, git_reference_target(ref)); + } else if (looks_like_a_branch(refname)) + error = git_reference_symbolic_create(&new_head, repo, GIT_HEAD_FILE, refname, 1); + + git_reference_free(ref); + git_reference_free(new_head); + return error; +} + +int git_repository_set_head_detached( + git_repository* repo, + const git_oid* commitish) +{ + int error; + git_object *object, + *peeled = NULL; + git_reference *new_head = NULL; + + assert(repo && commitish); + + if ((error = git_object_lookup(&object, repo, commitish, GIT_OBJ_ANY)) < 0) + return error; + + if ((error = git_object_peel(&peeled, object, GIT_OBJ_COMMIT)) < 0) + goto cleanup; + + error = git_reference_create(&new_head, repo, GIT_HEAD_FILE, git_object_id(peeled), 1); + +cleanup: + git_object_free(object); + git_object_free(peeled); + git_reference_free(new_head); + return error; +} + +int git_repository_detach_head( + git_repository* repo) +{ + git_reference *old_head = NULL, + *new_head = NULL; + git_object *object = NULL; + int error; + + assert(repo); + + if ((error = git_repository_head(&old_head, repo)) < 0) + return error; + + if ((error = git_object_lookup(&object, repo, git_reference_target(old_head), GIT_OBJ_COMMIT)) < 0) + goto cleanup; + + error = git_reference_create(&new_head, repo, GIT_HEAD_FILE, git_reference_target(old_head), 1); + +cleanup: + git_object_free(object); + git_reference_free(old_head); + git_reference_free(new_head); + return error; +} + +/** + * Loosely ported from git.git + * https://github.com/git/git/blob/master/contrib/completion/git-prompt.sh#L198-289 + */ +int git_repository_state(git_repository *repo) +{ + git_buf repo_path = GIT_BUF_INIT; + int state = GIT_REPOSITORY_STATE_NONE; + + assert(repo); + + if (git_buf_puts(&repo_path, repo->path_repository) < 0) + return -1; + + if (git_path_contains_file(&repo_path, GIT_REBASE_MERGE_INTERACTIVE_FILE)) + state = GIT_REPOSITORY_STATE_REBASE_INTERACTIVE; + else if (git_path_contains_dir(&repo_path, GIT_REBASE_MERGE_DIR)) + state = GIT_REPOSITORY_STATE_REBASE_MERGE; + else if (git_path_contains_file(&repo_path, GIT_REBASE_APPLY_REBASING_FILE)) + state = GIT_REPOSITORY_STATE_REBASE; + else if (git_path_contains_file(&repo_path, GIT_REBASE_APPLY_APPLYING_FILE)) + state = GIT_REPOSITORY_STATE_APPLY_MAILBOX; + else if (git_path_contains_dir(&repo_path, GIT_REBASE_APPLY_DIR)) + state = GIT_REPOSITORY_STATE_APPLY_MAILBOX_OR_REBASE; + else if (git_path_contains_file(&repo_path, GIT_MERGE_HEAD_FILE)) + state = GIT_REPOSITORY_STATE_MERGE; + else if(git_path_contains_file(&repo_path, GIT_REVERT_HEAD_FILE)) + state = GIT_REPOSITORY_STATE_REVERT; + else if(git_path_contains_file(&repo_path, GIT_CHERRY_PICK_HEAD_FILE)) + state = GIT_REPOSITORY_STATE_CHERRY_PICK; + else if(git_path_contains_file(&repo_path, GIT_BISECT_LOG_FILE)) + state = GIT_REPOSITORY_STATE_BISECT; + + git_buf_free(&repo_path); + return state; } diff --git a/src/repository.h b/src/repository.h index 91c69a655..cc2f8c2b8 100644 --- a/src/repository.h +++ b/src/repository.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -18,8 +18,10 @@ #include "refs.h" #include "buffer.h" #include "odb.h" -#include "attr.h" +#include "object.h" +#include "attrcache.h" #include "strmap.h" +#include "refdb.h" #define DOT_GIT ".git" #define GIT_DIR DOT_GIT "/" @@ -68,20 +70,21 @@ typedef enum { GIT_EOL_DEFAULT = GIT_EOL_NATIVE } git_cvar_value; -/** Base git object for inheritance */ -struct git_object { - git_cached_obj cached; - git_repository *repo; - git_otype type; +/* internal repository init flags */ +enum { + GIT_REPOSITORY_INIT__HAS_DOTGIT = (1u << 16), + GIT_REPOSITORY_INIT__NATURAL_WD = (1u << 17), + GIT_REPOSITORY_INIT__IS_REINIT = (1u << 18), }; +/** Internal structure for repository object */ struct git_repository { git_odb *_odb; + git_refdb *_refdb; git_config *_config; git_index *_index; git_cache objects; - git_refcache references; git_attr_cache attrcache; git_strmap *submodules; @@ -94,15 +97,6 @@ struct git_repository { git_cvar_value cvar_cache[GIT_CVAR_CACHE_MAX]; }; -/* fully free the object; internal method, do not - * export */ -void git_object__free(void *object); - -int git_object__resolve_to_type(git_object **obj, git_otype type); - -int git_oid__parse(git_oid *oid, const char **buffer_out, const char *buffer_end, const char *header); -void git_oid__writebuf(git_buf *buf, const char *header, const git_oid *oid); - GIT_INLINE(git_attr_cache *) git_repository_attr_cache(git_repository *repo) { return &repo->attrcache; @@ -119,10 +113,11 @@ int git_repository_head_tree(git_tree **tree, git_repository *repo); */ int git_repository_config__weakptr(git_config **out, git_repository *repo); int git_repository_odb__weakptr(git_odb **out, git_repository *repo); +int git_repository_refdb__weakptr(git_refdb **out, git_repository *repo); int git_repository_index__weakptr(git_index **out, git_repository *repo); /* - * CVAR cache + * CVAR cache * * Efficient access to the most used config variables of a repository. * The cache is cleared everytime the config backend is replaced. @@ -135,4 +130,19 @@ void git_repository__cvar_cache_clear(git_repository *repo); */ extern void git_submodule_config_free(git_repository *repo); +GIT_INLINE(int) git_repository__ensure_not_bare( + git_repository *repo, + const char *operation_name) +{ + if (!git_repository_is_bare(repo)) + return 0; + + giterr_set( + GITERR_REPOSITORY, + "Cannot %s. This operation is not allowed against bare repositories.", + operation_name); + + return GIT_EBAREREPO; +} + #endif diff --git a/src/reset.c b/src/reset.c new file mode 100644 index 000000000..c1e1f865e --- /dev/null +++ b/src/reset.c @@ -0,0 +1,163 @@ +/* + * 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 "commit.h" +#include "tag.h" +#include "merge.h" +#include "diff.h" +#include "git2/reset.h" +#include "git2/checkout.h" +#include "git2/merge.h" +#include "git2/refs.h" + +#define ERROR_MSG "Cannot perform reset" + +int git_reset_default( + git_repository *repo, + git_object *target, + git_strarray* pathspecs) +{ + git_object *commit = NULL; + git_tree *tree = NULL; + git_diff_list *diff = NULL; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + size_t i; + git_diff_delta *delta; + git_index_entry entry; + int error; + git_index *index = NULL; + + assert(pathspecs != NULL && pathspecs->count > 0); + + memset(&entry, 0, sizeof(git_index_entry)); + + if ((error = git_repository_index(&index, repo)) < 0) + goto cleanup; + + if (target) { + if (git_object_owner(target) != repo) { + giterr_set(GITERR_OBJECT, + "%s_default - The given target does not belong to this repository.", ERROR_MSG); + return -1; + } + + if ((error = git_object_peel(&commit, target, GIT_OBJ_COMMIT)) < 0 || + (error = git_commit_tree(&tree, (git_commit *)commit)) < 0) + goto cleanup; + } + + opts.pathspec = *pathspecs; + opts.flags = GIT_DIFF_REVERSE; + + if ((error = git_diff_tree_to_index( + &diff, repo, tree, index, &opts)) < 0) + goto cleanup; + + git_vector_foreach(&diff->deltas, i, delta) { + if ((error = git_index_conflict_remove(index, delta->old_file.path)) < 0) + goto cleanup; + + assert(delta->status == GIT_DELTA_ADDED || + delta->status == GIT_DELTA_MODIFIED || + delta->status == GIT_DELTA_DELETED); + + if (delta->status == GIT_DELTA_DELETED) { + if ((error = git_index_remove(index, delta->old_file.path, 0)) < 0) + goto cleanup; + } else { + entry.mode = delta->new_file.mode; + git_oid_cpy(&entry.oid, &delta->new_file.oid); + entry.path = (char *)delta->new_file.path; + + if ((error = git_index_add(index, &entry)) < 0) + goto cleanup; + } + } + + error = git_index_write(index); + +cleanup: + git_object_free(commit); + git_tree_free(tree); + git_index_free(index); + git_diff_list_free(diff); + + return error; +} + +int git_reset( + git_repository *repo, + git_object *target, + git_reset_t reset_type) +{ + git_object *commit = NULL; + git_index *index = NULL; + git_tree *tree = NULL; + int error = 0; + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + + assert(repo && target); + + if (git_object_owner(target) != repo) { + giterr_set(GITERR_OBJECT, + "%s - The given target does not belong to this repository.", ERROR_MSG); + return -1; + } + + if (reset_type != GIT_RESET_SOFT && + (error = git_repository__ensure_not_bare(repo, + reset_type == GIT_RESET_MIXED ? "reset mixed" : "reset hard")) < 0) + return error; + + if ((error = git_object_peel(&commit, target, GIT_OBJ_COMMIT)) < 0 || + (error = git_repository_index(&index, repo)) < 0 || + (error = git_commit_tree(&tree, (git_commit *)commit)) < 0) + goto cleanup; + + if (reset_type == GIT_RESET_SOFT && + (git_repository_state(repo) == GIT_REPOSITORY_STATE_MERGE || + git_index_has_conflicts(index))) + { + giterr_set(GITERR_OBJECT, "%s (soft) in the middle of a merge.", ERROR_MSG); + error = GIT_EUNMERGED; + goto cleanup; + } + + /* move HEAD to the new target */ + if ((error = git_reference__update_terminal(repo, GIT_HEAD_FILE, + git_object_id(commit))) < 0) + goto cleanup; + + if (reset_type == GIT_RESET_HARD) { + /* overwrite working directory with HEAD */ + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + + if ((error = git_checkout_tree(repo, (git_object *)tree, &opts)) < 0) + goto cleanup; + } + + if (reset_type > GIT_RESET_SOFT) { + /* reset index to the target content */ + + if ((error = git_index_read_tree(index, tree)) < 0 || + (error = git_index_write(index)) < 0) + goto cleanup; + + if ((error = git_repository_merge_cleanup(repo)) < 0) { + giterr_set(GITERR_INDEX, "%s - failed to clean up merge data", ERROR_MSG); + goto cleanup; + } + } + +cleanup: + git_object_free(commit); + git_index_free(index); + git_tree_free(tree); + + return error; +} diff --git a/src/revparse.c b/src/revparse.c new file mode 100644 index 000000000..74635ed04 --- /dev/null +++ b/src/revparse.c @@ -0,0 +1,912 @@ +/* + * 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 <assert.h> + +#include "common.h" +#include "buffer.h" +#include "tree.h" +#include "refdb.h" + +#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; + + if (git_oid_fromstrn(&oid, spec, speclen) < 0) + return GIT_ENOTFOUND; + + return git_object_lookup_prefix(out, repo, &oid, speclen, GIT_OBJ_ANY); +} + +static int maybe_sha(git_object** out, git_repository *repo, const char *spec) +{ + size_t speclen = strlen(spec); + + if (speclen != GIT_OID_HEXSZ) + return GIT_ENOTFOUND; + + return maybe_sha_or_abbrev(out, repo, spec, speclen); +} + +static int maybe_abbrev(git_object** out, git_repository *repo, const char *spec) +{ + size_t speclen = strlen(spec); + + return maybe_sha_or_abbrev(out, repo, spec, speclen); +} + +static int build_regex(regex_t *regex, const char *pattern) +{ + int error; + + if (*pattern == '\0') { + giterr_set(GITERR_REGEX, "Empty pattern"); + return GIT_EINVALIDSPEC; + } + + error = regcomp(regex, pattern, REG_EXTENDED); + if (!error) + return 0; + + error = giterr_set_regex(regex, error); + + regfree(regex); + + return error; +} + +static int maybe_describe(git_object**out, git_repository *repo, const char *spec) +{ + const char *substr; + int error; + regex_t regex; + + substr = strstr(spec, "-g"); + + if (substr == NULL) + return GIT_ENOTFOUND; + + if (build_regex(®ex, ".+-[0-9]+-g[0-9a-fA-F]+") < 0) + return -1; + + error = regexec(®ex, spec, 0, NULL, 0); + regfree(®ex); + + if (error) + return GIT_ENOTFOUND; + + return maybe_abbrev(out, repo, substr+2); +} + +static int revparse_lookup_object(git_object **out, git_repository *repo, const char *spec) +{ + int error; + git_reference *ref; + + error = maybe_sha(out, repo, spec); + if (!error) + return 0; + + if (error < 0 && error != GIT_ENOTFOUND) + return error; + + error = disambiguate_refname(&ref, repo, spec); + if (!error) { + error = git_object_lookup(out, repo, git_reference_target(ref), GIT_OBJ_ANY); + git_reference_free(ref); + return error; + } + + if (error < 0 && error != GIT_ENOTFOUND) + return error; + + error = maybe_abbrev(out, repo, spec); + if (!error) + return 0; + + if (error < 0 && error != GIT_ENOTFOUND) + return error; + + error = maybe_describe(out, repo, spec); + if (!error) + return 0; + + if (error < 0 && error != GIT_ENOTFOUND) + return error; + + giterr_set(GITERR_REFERENCE, "Refspec '%s' not found.", spec); + return GIT_ENOTFOUND; +} + +static int try_parse_numeric(int *n, const char *curly_braces_content) +{ + int32_t content; + const char *end_ptr; + + if (git__strtol32(&content, curly_braces_content, &end_ptr, 10) < 0) + return -1; + + if (*end_ptr != '\0') + return -1; + + *n = (int)content; + return 0; +} + +static int retrieve_previously_checked_out_branch_or_revision(git_object **out, git_reference **base_ref, git_repository *repo, const char *identifier, size_t position) +{ + git_reference *ref = NULL; + git_reflog *reflog = NULL; + regex_t preg; + int error = -1; + size_t i, numentries, cur; + const git_reflog_entry *entry; + const char *msg; + regmatch_t regexmatches[2]; + git_buf buf = GIT_BUF_INIT; + + cur = position; + + if (*identifier != '\0' || *base_ref != NULL) + return GIT_EINVALIDSPEC; + + if (build_regex(&preg, "checkout: moving from (.*) to .*") < 0) + return -1; + + if (git_reference_lookup(&ref, repo, GIT_HEAD_FILE) < 0) + goto cleanup; + + if (git_reflog_read(&reflog, ref) < 0) + goto cleanup; + + numentries = git_reflog_entrycount(reflog); + + for (i = 0; i < numentries; i++) { + entry = git_reflog_entry_byindex(reflog, i); + msg = git_reflog_entry_message(entry); + + if (regexec(&preg, msg, 2, regexmatches, 0)) + continue; + + cur--; + + if (cur > 0) + continue; + + 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) + goto cleanup; + + if (error < 0 && error != GIT_ENOTFOUND) + goto cleanup; + + error = maybe_abbrev(out, repo, git_buf_cstr(&buf)); + + goto cleanup; + } + + error = GIT_ENOTFOUND; + +cleanup: + git_reference_free(ref); + git_buf_free(&buf); + regfree(&preg); + git_reflog_free(reflog); + return error; +} + +static int retrieve_oid_from_reflog(git_oid *oid, git_reference *ref, size_t identifier) +{ + git_reflog *reflog; + int error = -1; + size_t numentries; + const git_reflog_entry *entry; + bool search_by_pos = (identifier <= 100000000); + + if (git_reflog_read(&reflog, ref) < 0) + return -1; + + numentries = git_reflog_entrycount(reflog); + + if (search_by_pos) { + if (numentries < identifier + 1) { + giterr_set( + GITERR_REFERENCE, + "Reflog for '%s' has only "PRIuZ" entries, asked for "PRIuZ, + git_reference_name(ref), numentries, identifier); + + error = GIT_ENOTFOUND; + goto cleanup; + } + + entry = git_reflog_entry_byindex(reflog, identifier); + git_oid_cpy(oid, git_reflog_entry_id_new(entry)); + error = 0; + goto cleanup; + + } else { + size_t i; + git_time commit_time; + + for (i = 0; i < numentries; i++) { + entry = git_reflog_entry_byindex(reflog, i); + commit_time = git_reflog_entry_committer(entry)->when; + + if (commit_time.time > (git_time_t)identifier) + continue; + + git_oid_cpy(oid, git_reflog_entry_id_new(entry)); + error = 0; + goto cleanup; + } + + error = GIT_ENOTFOUND; + } + +cleanup: + git_reflog_free(reflog); + return error; +} + +static int retrieve_revobject_from_reflog(git_object **out, git_reference **base_ref, git_repository *repo, const char *identifier, size_t position) +{ + git_reference *ref; + git_oid oid; + int error = -1; + + if (*base_ref == NULL) { + if ((error = disambiguate_refname(&ref, repo, identifier)) < 0) + return error; + } else { + ref = *base_ref; + *base_ref = NULL; + } + + if (position == 0) { + error = git_object_lookup(out, repo, git_reference_target(ref), GIT_OBJ_ANY); + goto cleanup; + } + + if ((error = retrieve_oid_from_reflog(&oid, ref, position)) < 0) + goto cleanup; + + error = git_object_lookup(out, repo, &oid, GIT_OBJ_ANY); + +cleanup: + git_reference_free(ref); + return error; +} + +static int retrieve_remote_tracking_reference(git_reference **base_ref, const char *identifier, git_repository *repo) +{ + git_reference *tracking, *ref; + int error = -1; + + if (*base_ref == NULL) { + if ((error = disambiguate_refname(&ref, repo, identifier)) < 0) + return error; + } else { + ref = *base_ref; + *base_ref = NULL; + } + + if (!git_reference_is_branch(ref)) { + error = GIT_EINVALIDSPEC; + goto cleanup; + } + + if ((error = git_branch_upstream(&tracking, ref)) < 0) + goto cleanup; + + *base_ref = tracking; + +cleanup: + git_reference_free(ref); + return error; +} + +static int handle_at_syntax(git_object **out, git_reference **ref, const char *spec, size_t identifier_len, git_repository* repo, const char *curly_braces_content) +{ + bool is_numeric; + int parsed = 0, error = -1; + git_buf identifier = GIT_BUF_INIT; + git_time_t timestamp; + + assert(*out == NULL); + + if (git_buf_put(&identifier, spec, identifier_len) < 0) + return -1; + + is_numeric = !try_parse_numeric(&parsed, curly_braces_content); + + if (*curly_braces_content == '-' && (!is_numeric || parsed == 0)) { + error = GIT_EINVALIDSPEC; + goto cleanup; + } + + if (is_numeric) { + if (parsed < 0) + error = retrieve_previously_checked_out_branch_or_revision(out, ref, repo, git_buf_cstr(&identifier), -parsed); + else + error = retrieve_revobject_from_reflog(out, ref, repo, git_buf_cstr(&identifier), parsed); + + goto cleanup; + } + + if (!strcmp(curly_braces_content, "u") || !strcmp(curly_braces_content, "upstream")) { + error = retrieve_remote_tracking_reference(ref, git_buf_cstr(&identifier), repo); + + goto cleanup; + } + + if (git__date_parse(×tamp, curly_braces_content) < 0) + goto cleanup; + + error = retrieve_revobject_from_reflog(out, ref, repo, git_buf_cstr(&identifier), (size_t)timestamp); + +cleanup: + git_buf_free(&identifier); + return error; +} + +static git_otype parse_obj_type(const char *str) +{ + if (!strcmp(str, "commit")) + return GIT_OBJ_COMMIT; + + if (!strcmp(str, "tree")) + return GIT_OBJ_TREE; + + if (!strcmp(str, "blob")) + return GIT_OBJ_BLOB; + + if (!strcmp(str, "tag")) + return GIT_OBJ_TAG; + + return GIT_OBJ_BAD; +} + +static int dereference_to_non_tag(git_object **out, git_object *obj) +{ + if (git_object_type(obj) == GIT_OBJ_TAG) + return git_tag_peel(out, (git_tag *)obj); + + return git_object_dup(out, obj); +} + +static int handle_caret_parent_syntax(git_object **out, git_object *obj, int n) +{ + git_object *temp_commit = NULL; + int error; + + if ((error = git_object_peel(&temp_commit, obj, GIT_OBJ_COMMIT)) < 0) + return (error == GIT_EAMBIGUOUS || error == GIT_ENOTFOUND) ? + GIT_EINVALIDSPEC : error; + + if (n == 0) { + *out = temp_commit; + return 0; + } + + error = git_commit_parent((git_commit **)out, (git_commit*)temp_commit, n - 1); + + git_object_free(temp_commit); + return error; +} + +static int handle_linear_syntax(git_object **out, git_object *obj, int n) +{ + git_object *temp_commit = NULL; + int error; + + if ((error = git_object_peel(&temp_commit, obj, GIT_OBJ_COMMIT)) < 0) + return (error == GIT_EAMBIGUOUS || error == GIT_ENOTFOUND) ? + GIT_EINVALIDSPEC : error; + + error = git_commit_nth_gen_ancestor((git_commit **)out, (git_commit*)temp_commit, n); + + git_object_free(temp_commit); + return error; +} + +static int handle_colon_syntax( + git_object **out, + git_object *obj, + const char *path) +{ + git_object *tree; + int error = -1; + git_tree_entry *entry = NULL; + + if ((error = git_object_peel(&tree, obj, GIT_OBJ_TREE)) < 0) + return error == GIT_ENOTFOUND ? GIT_EINVALIDSPEC : error; + + if (*path == '\0') { + *out = tree; + return 0; + } + + /* + * TODO: Handle the relative path syntax + * (:./relative/path and :../relative/path) + */ + if ((error = git_tree_entry_bypath(&entry, (git_tree *)tree, path)) < 0) + goto cleanup; + + error = git_tree_entry_to_object(out, git_object_owner(tree), entry); + +cleanup: + git_tree_entry_free(entry); + git_object_free(tree); + + return error; +} + +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); + if ((error < 0) && (error != GIT_ENOTFOUND)) + return -1; + + if (!regexec(regex, git_commit_message((git_commit*)obj), 0, NULL, 0)) { + *out = obj; + return 0; + } + + git_object_free(obj); + } + + if (error < 0 && error == GIT_ITEROVER) + error = GIT_ENOTFOUND; + + return error; +} + +static int handle_grep_syntax(git_object **out, git_repository *repo, const git_oid *spec_oid, const char *pattern) +{ + regex_t preg; + git_revwalk *walk = NULL; + int error; + + if ((error = build_regex(&preg, pattern)) < 0) + return error; + + if ((error = git_revwalk_new(&walk, repo)) < 0) + goto cleanup; + + git_revwalk_sorting(walk, GIT_SORT_TIME); + + if (spec_oid == NULL) { + // TODO: @carlosmn: The glob should be refs/* but this makes git_revwalk_next() fails + if ((error = git_revwalk_push_glob(walk, GIT_REFS_HEADS_DIR "*")) < 0) + goto cleanup; + } else if ((error = git_revwalk_push(walk, spec_oid)) < 0) + goto cleanup; + + error = walk_and_search(out, walk, &preg); + +cleanup: + regfree(&preg); + git_revwalk_free(walk); + + return error; +} + +static int handle_caret_curly_syntax(git_object **out, git_object *obj, const char *curly_braces_content) +{ + git_otype expected_type; + + if (*curly_braces_content == '\0') + return dereference_to_non_tag(out, obj); + + if (*curly_braces_content == '/') + return handle_grep_syntax(out, git_object_owner(obj), git_object_id(obj), curly_braces_content + 1); + + expected_type = parse_obj_type(curly_braces_content); + + if (expected_type == GIT_OBJ_BAD) + return GIT_EINVALIDSPEC; + + return git_object_peel(out, obj, expected_type); +} + +static int extract_curly_braces_content(git_buf *buf, const char *spec, size_t *pos) +{ + git_buf_clear(buf); + + assert(spec[*pos] == '^' || spec[*pos] == '@'); + + (*pos)++; + + if (spec[*pos] == '\0' || spec[*pos] != '{') + return GIT_EINVALIDSPEC; + + (*pos)++; + + while (spec[*pos] != '}') { + if (spec[*pos] == '\0') + return GIT_EINVALIDSPEC; + + git_buf_putc(buf, spec[(*pos)++]); + } + + (*pos)++; + + return 0; +} + +static int extract_path(git_buf *buf, const char *spec, size_t *pos) +{ + git_buf_clear(buf); + + assert(spec[*pos] == ':'); + + (*pos)++; + + if (git_buf_puts(buf, spec + *pos) < 0) + return -1; + + *pos += git_buf_len(buf); + + return 0; +} + +static int extract_how_many(int *n, const char *spec, size_t *pos) +{ + const char *end_ptr; + int parsed, accumulated; + char kind = spec[*pos]; + + assert(spec[*pos] == '^' || spec[*pos] == '~'); + + accumulated = 0; + + do { + do { + (*pos)++; + accumulated++; + } while (spec[(*pos)] == kind && kind == '~'); + + if (git__isdigit(spec[*pos])) { + if (git__strtol32(&parsed, spec + *pos, &end_ptr, 10) < 0) + return GIT_EINVALIDSPEC; + + accumulated += (parsed - 1); + *pos = end_ptr - spec; + } + + } while (spec[(*pos)] == kind && kind == '~'); + + *n = accumulated; + + return 0; +} + +static int object_from_reference(git_object **object, git_reference *reference) +{ + git_reference *resolved = NULL; + int error; + + if (git_reference_resolve(&resolved, reference) < 0) + return -1; + + error = git_object_lookup(object, reference->db->repo, git_reference_target(resolved), GIT_OBJ_ANY); + git_reference_free(resolved); + + return error; +} + +static int ensure_base_rev_loaded(git_object **object, git_reference **reference, const char *spec, size_t identifier_len, git_repository *repo, bool allow_empty_identifier) +{ + int error; + git_buf identifier = GIT_BUF_INIT; + + 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 (!allow_empty_identifier && identifier_len == 0) + return GIT_EINVALIDSPEC; + + if (git_buf_put(&identifier, spec, identifier_len) < 0) + return -1; + + error = revparse_lookup_object(object, repo, git_buf_cstr(&identifier)); + git_buf_free(&identifier); + + return error; +} + +static int ensure_base_rev_is_not_known_yet(git_object *object) +{ + if (object == NULL) + return 0; + + return GIT_EINVALIDSPEC; +} + +static bool any_left_hand_identifier(git_object *object, git_reference *reference, size_t identifier_len) +{ + if (object != NULL) + return true; + + if (reference != NULL) + return true; + + if (identifier_len > 0) + return true; + + return false; +} + +static int ensure_left_hand_identifier_is_not_known_yet(git_object *object, git_reference *reference) +{ + if (!ensure_base_rev_is_not_known_yet(object) && reference == NULL) + return 0; + + return GIT_EINVALIDSPEC; +} + +int git_revparse_single(git_object **out, git_repository *repo, const char *spec) +{ + size_t pos = 0, identifier_len = 0; + int error = -1, n; + git_buf buf = GIT_BUF_INIT; + + git_reference *reference = NULL; + git_object *base_rev = NULL; + + assert(out && repo && spec); + + *out = NULL; + + while (spec[pos]) { + switch (spec[pos]) { + case '^': + if ((error = ensure_base_rev_loaded(&base_rev, &reference, spec, identifier_len, repo, false)) < 0) + goto cleanup; + + if (spec[pos+1] == '{') { + git_object *temp_object = NULL; + + if ((error = extract_curly_braces_content(&buf, spec, &pos)) < 0) + goto cleanup; + + if ((error = handle_caret_curly_syntax(&temp_object, base_rev, git_buf_cstr(&buf))) < 0) + goto cleanup; + + git_object_free(base_rev); + base_rev = temp_object; + } else { + git_object *temp_object = NULL; + + if ((error = extract_how_many(&n, spec, &pos)) < 0) + goto cleanup; + + if ((error = handle_caret_parent_syntax(&temp_object, base_rev, n)) < 0) + goto cleanup; + + git_object_free(base_rev); + base_rev = temp_object; + } + break; + + case '~': + { + git_object *temp_object = NULL; + + if ((error = extract_how_many(&n, spec, &pos)) < 0) + goto cleanup; + + if ((error = ensure_base_rev_loaded(&base_rev, &reference, spec, identifier_len, repo, false)) < 0) + goto cleanup; + + if ((error = handle_linear_syntax(&temp_object, base_rev, n)) < 0) + goto cleanup; + + git_object_free(base_rev); + base_rev = temp_object; + break; + } + + case ':': + { + git_object *temp_object = NULL; + + if ((error = extract_path(&buf, spec, &pos)) < 0) + goto cleanup; + + if (any_left_hand_identifier(base_rev, reference, identifier_len)) { + if ((error = ensure_base_rev_loaded(&base_rev, &reference, spec, identifier_len, repo, true)) < 0) + goto cleanup; + + if ((error = handle_colon_syntax(&temp_object, base_rev, git_buf_cstr(&buf))) < 0) + goto cleanup; + } else { + if (*git_buf_cstr(&buf) == '/') { + if ((error = handle_grep_syntax(&temp_object, repo, NULL, git_buf_cstr(&buf) + 1)) < 0) + goto cleanup; + } else { + + /* + * TODO: support merge-stage path lookup (":2:Makefile") + * and plain index blob lookup (:i-am/a/blob) + */ + giterr_set(GITERR_INVALID, "Unimplemented"); + error = GIT_ERROR; + goto cleanup; + } + } + + git_object_free(base_rev); + base_rev = temp_object; + break; + } + + case '@': + { + if (spec[pos+1] == '{') { + git_object *temp_object = NULL; + + if ((error = extract_curly_braces_content(&buf, spec, &pos)) < 0) + goto cleanup; + + if ((error = ensure_base_rev_is_not_known_yet(base_rev)) < 0) + goto cleanup; + + if ((error = handle_at_syntax(&temp_object, &reference, spec, identifier_len, repo, git_buf_cstr(&buf))) < 0) + goto cleanup; + + if (temp_object != NULL) + base_rev = temp_object; + break; + } else { + /* Fall through */ + } + } + + default: + if ((error = ensure_left_hand_identifier_is_not_known_yet(base_rev, reference)) < 0) + goto cleanup; + + pos++; + identifier_len++; + } + } + + if ((error = ensure_base_rev_loaded(&base_rev, &reference, spec, identifier_len, repo, false)) < 0) + goto cleanup; + + *out = base_rev; + error = 0; + +cleanup: + if (error) { + if (error == GIT_EINVALIDSPEC) + giterr_set(GITERR_INVALID, + "Failed to parse revision specifier - Invalid pattern '%s'", spec); + + git_object_free(base_rev); + } + git_reference_free(reference); + git_buf_free(&buf); + return error; +} + + +int git_revparse( + git_revspec *revspec, + git_repository *repo, + const char *spec) +{ + const char *dotdot; + int error = 0; + + 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 { + revspec->flags = GIT_REVPARSE_SINGLE; + error = git_revparse_single(&revspec->from, repo, spec); + } + + return error; +} + diff --git a/src/revwalk.c b/src/revwalk.c index e64d93f20..16f06624d 100644 --- a/src/revwalk.c +++ b/src/revwalk.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -8,146 +8,18 @@ #include "common.h" #include "commit.h" #include "odb.h" -#include "pqueue.h" #include "pool.h" -#include "oidmap.h" -#include "git2/revwalk.h" -#include "git2/merge.h" +#include "revwalk.h" +#include "git2/revparse.h" +#include "merge.h" #include <regex.h> -GIT__USE_OIDMAP; - -#define PARENT1 (1 << 0) -#define PARENT2 (1 << 1) -#define RESULT (1 << 2) -#define STALE (1 << 3) - -typedef struct commit_object { - git_oid oid; - uint32_t time; - unsigned int seen:1, - uninteresting:1, - topo_delay:1, - parsed:1, - flags : 4; - - unsigned short in_degree; - unsigned short out_degree; - - struct commit_object **parents; -} commit_object; - -typedef struct commit_list { - commit_object *item; - struct commit_list *next; -} commit_list; - -struct git_revwalk { - git_repository *repo; - git_odb *odb; - - git_oidmap *commits; - git_pool commit_pool; - - commit_list *iterator_topo; - commit_list *iterator_rand; - commit_list *iterator_reverse; - git_pqueue iterator_time; - - int (*get_next)(commit_object **, git_revwalk *); - int (*enqueue)(git_revwalk *, commit_object *); - - unsigned walking:1; - unsigned int sorting; - - /* merge base calculation */ - commit_object *one; - git_vector twos; -}; - -static int commit_time_cmp(void *a, void *b) -{ - commit_object *commit_a = (commit_object *)a; - commit_object *commit_b = (commit_object *)b; - - return (commit_a->time < commit_b->time); -} - -static commit_list *commit_list_insert(commit_object *item, commit_list **list_p) -{ - commit_list *new_list = git__malloc(sizeof(commit_list)); - if (new_list != NULL) { - new_list->item = item; - new_list->next = *list_p; - } - *list_p = new_list; - return new_list; -} - -static commit_list *commit_list_insert_by_date(commit_object *item, commit_list **list_p) -{ - commit_list **pp = list_p; - commit_list *p; - - while ((p = *pp) != NULL) { - if (commit_time_cmp(p->item, item) < 0) - break; - - pp = &p->next; - } - - return commit_list_insert(item, pp); -} -static void commit_list_free(commit_list **list_p) -{ - commit_list *list = *list_p; - - while (list) { - commit_list *temp = list; - list = temp->next; - git__free(temp); - } - - *list_p = NULL; -} - -static commit_object *commit_list_pop(commit_list **stack) -{ - commit_list *top = *stack; - commit_object *item = top ? top->item : NULL; - - if (top) { - *stack = top->next; - git__free(top); - } - return item; -} - -#define PARENTS_PER_COMMIT 2 -#define COMMIT_ALLOC \ - (sizeof(commit_object) + PARENTS_PER_COMMIT * sizeof(commit_object *)) - -static commit_object *alloc_commit(git_revwalk *walk) -{ - return (commit_object *)git_pool_malloc(&walk->commit_pool, COMMIT_ALLOC); -} - -static commit_object **alloc_parents( - git_revwalk *walk, commit_object *commit, size_t n_parents) -{ - if (n_parents <= PARENTS_PER_COMMIT) - return (commit_object **)((char *)commit + sizeof(commit_object)); - - return (commit_object **)git_pool_malloc( - &walk->commit_pool, (uint32_t)(n_parents * sizeof(commit_object *))); -} - - -static commit_object *commit_lookup(git_revwalk *walk, const git_oid *oid) +git_commit_list_node *git_revwalk__commit_lookup( + git_revwalk *walk, const git_oid *oid) { - commit_object *commit; + git_commit_list_node *commit; khiter_t pos; int ret; @@ -156,7 +28,7 @@ static commit_object *commit_lookup(git_revwalk *walk, const git_oid *oid) if (pos != kh_end(walk->commits)) return kh_value(walk->commits, pos); - commit = alloc_commit(walk); + commit = git_commit_list_alloc_node(walk); if (commit == NULL) return NULL; @@ -169,227 +41,7 @@ static commit_object *commit_lookup(git_revwalk *walk, const git_oid *oid) return commit; } -static int commit_quick_parse(git_revwalk *walk, commit_object *commit, git_rawobj *raw) -{ - 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; - - int i, parents = 0; - int commit_time; - - buffer += strlen("tree ") + GIT_OID_HEXSZ + 1; - - parents_start = buffer; - while (buffer + parent_len < buffer_end && memcmp(buffer, "parent ", strlen("parent ")) == 0) { - parents++; - buffer += parent_len; - } - - commit->parents = alloc_parents(walk, commit, parents); - GITERR_CHECK_ALLOC(commit->parents); - - buffer = parents_start; - for (i = 0; i < parents; ++i) { - git_oid oid; - - if (git_oid_fromstr(&oid, (char *)buffer + strlen("parent ")) < 0) - return -1; - - commit->parents[i] = commit_lookup(walk, &oid); - if (commit->parents[i] == NULL) - return -1; - - buffer += parent_len; - } - - commit->out_degree = (unsigned short)parents; - - if ((buffer = memchr(buffer, '\n', buffer_end - buffer)) == NULL) { - giterr_set(GITERR_ODB, "Failed to parse commit. Object is corrupted"); - return -1; - } - - buffer = memchr(buffer, '>', buffer_end - buffer); - if (buffer == NULL) { - giterr_set(GITERR_ODB, "Failed to parse commit. Can't find author"); - return -1; - } - - if (git__strtol32(&commit_time, (char *)buffer + 2, NULL, 10) < 0) { - giterr_set(GITERR_ODB, "Failed to parse commit. Can't parse commit time"); - return -1; - } - - commit->time = (time_t)commit_time; - commit->parsed = 1; - return 0; -} - -static int commit_parse(git_revwalk *walk, commit_object *commit) -{ - git_odb_object *obj; - int error; - - if (commit->parsed) - return 0; - - if ((error = git_odb_read(&obj, walk->odb, &commit->oid)) < 0) - return error; - - if (obj->raw.type != GIT_OBJ_COMMIT) { - git_odb_object_free(obj); - giterr_set(GITERR_INVALID, "Failed to parse commit. Object is no commit object"); - return -1; - } - - error = commit_quick_parse(walk, commit, &obj->raw); - git_odb_object_free(obj); - return error; -} - -static int interesting(git_pqueue *list) -{ - unsigned int i; - for (i = 1; i < git_pqueue_size(list); i++) { - commit_object *commit = list->d[i]; - if ((commit->flags & STALE) == 0) - return 1; - } - - return 0; -} - -static int merge_bases_many(commit_list **out, git_revwalk *walk, commit_object *one, git_vector *twos) -{ - int error; - unsigned int i; - commit_object *two; - commit_list *result = NULL, *tmp = NULL; - git_pqueue list; - - /* if the commit is repeated, we have a our merge base already */ - git_vector_foreach(twos, i, two) { - if (one == two) - return commit_list_insert(one, out) ? 0 : -1; - } - - if (git_pqueue_init(&list, twos->length * 2, commit_time_cmp) < 0) - return -1; - - if (commit_parse(walk, one) < 0) - return -1; - - one->flags |= PARENT1; - if (git_pqueue_insert(&list, one) < 0) - return -1; - - git_vector_foreach(twos, i, two) { - commit_parse(walk, two); - two->flags |= PARENT2; - if (git_pqueue_insert(&list, two) < 0) - return -1; - } - - /* as long as there are non-STALE commits */ - while (interesting(&list)) { - commit_object *commit; - int flags; - - commit = git_pqueue_pop(&list); - - flags = commit->flags & (PARENT1 | PARENT2 | STALE); - if (flags == (PARENT1 | PARENT2)) { - if (!(commit->flags & RESULT)) { - commit->flags |= RESULT; - if (commit_list_insert(commit, &result) == NULL) - return -1; - } - /* we mark the parents of a merge stale */ - flags |= STALE; - } - - for (i = 0; i < commit->out_degree; i++) { - commit_object *p = commit->parents[i]; - if ((p->flags & flags) == flags) - continue; - - if ((error = commit_parse(walk, p)) < 0) - return error; - - p->flags |= flags; - if (git_pqueue_insert(&list, p) < 0) - return -1; - } - } - - git_pqueue_free(&list); - - /* filter out any stale commits in the results */ - tmp = result; - result = NULL; - - while (tmp) { - struct commit_list *next = tmp->next; - if (!(tmp->item->flags & STALE)) - if (commit_list_insert_by_date(tmp->item, &result) == NULL) - return -1; - - git__free(tmp); - tmp = next; - } - - *out = result; - return 0; -} - -int git_merge_base(git_oid *out, git_repository *repo, git_oid *one, git_oid *two) -{ - git_revwalk *walk; - git_vector list; - commit_list *result = NULL; - commit_object *commit; - void *contents[1]; - - if (git_revwalk_new(&walk, repo) < 0) - return -1; - - commit = commit_lookup(walk, two); - if (commit == NULL) - goto on_error; - - /* This is just one value, so we can do it on the stack */ - memset(&list, 0x0, sizeof(git_vector)); - contents[0] = commit; - list.length = 1; - list.contents = contents; - - commit = commit_lookup(walk, one); - if (commit == NULL) - goto on_error; - - if (merge_bases_many(&result, walk, commit, &list) < 0) - goto on_error; - - if (!result) { - git_revwalk_free(walk); - return GIT_ENOTFOUND; - } - - git_oid_cpy(out, &result->item->oid); - commit_list_free(&result); - git_revwalk_free(walk); - - return 0; - -on_error: - git_revwalk_free(walk); - return -1; -} - -static void mark_uninteresting(commit_object *commit) +static void mark_uninteresting(git_commit_list_node *commit) { unsigned short i; assert(commit); @@ -405,7 +57,7 @@ static void mark_uninteresting(commit_object *commit) mark_uninteresting(commit->parents[i]); } -static int process_commit(git_revwalk *walk, commit_object *commit, int hide) +static int process_commit(git_revwalk *walk, git_commit_list_node *commit, int hide) { int error; @@ -417,13 +69,13 @@ static int process_commit(git_revwalk *walk, commit_object *commit, int hide) commit->seen = 1; - if ((error = commit_parse(walk, commit)) < 0) + if ((error = git_commit_list_parse(walk, commit)) < 0) return error; return walk->enqueue(walk, commit); } -static int process_commit_parents(git_revwalk *walk, commit_object *commit) +static int process_commit_parents(git_revwalk *walk, git_commit_list_node *commit) { unsigned short i; int error = 0; @@ -436,9 +88,22 @@ static int process_commit_parents(git_revwalk *walk, commit_object *commit) static int push_commit(git_revwalk *walk, const git_oid *oid, int uninteresting) { - commit_object *commit; + git_object *obj; + git_otype type; + git_commit_list_node *commit; + + if (git_object_lookup(&obj, walk->repo, oid, GIT_OBJ_ANY) < 0) + return -1; + + type = git_object_type(obj); + git_object_free(obj); - commit = commit_lookup(walk, oid); + if (type != GIT_OBJ_COMMIT) { + giterr_set(GITERR_INVALID, "Object is no commit object"); + return -1; + } + + commit = git_revwalk__commit_lookup(walk, oid); if (commit == NULL) return -1; /* error already reported by failed lookup */ @@ -470,7 +135,7 @@ static int push_ref(git_revwalk *walk, const char *refname, int hide) { git_oid oid; - if (git_reference_name_to_oid(&oid, walk->repo, refname) < 0) + if (git_reference_name_to_id(&oid, walk->repo, refname) < 0) return -1; return push_commit(walk, &oid, hide); @@ -478,7 +143,6 @@ static int push_ref(git_revwalk *walk, const char *refname, int hide) struct push_cb_data { git_revwalk *walk; - const char *glob; int hide; }; @@ -486,10 +150,7 @@ static int push_glob_cb(const char *refname, void *data_) { struct push_cb_data *data = (struct push_cb_data *)data_; - if (!p_fnmatch(data->glob, refname, 0)) - return push_ref(data->walk, refname, data->hide); - - return 0; + return push_ref(data->walk, refname, data->hide); } static int push_glob(git_revwalk *walk, const char *glob, int hide) @@ -522,11 +183,10 @@ static int push_glob(git_revwalk *walk, const char *glob, int hide) goto on_error; data.walk = walk; - data.glob = git_buf_cstr(&buf); data.hide = hide; - if (git_reference_foreach( - walk->repo, GIT_REF_LISTALL, push_glob_cb, &data) < 0) + if (git_reference_foreach_glob( + walk->repo, git_buf_cstr(&buf), GIT_REF_LISTALL, push_glob_cb, &data) < 0) goto on_error; regfree(&preg); @@ -569,26 +229,51 @@ int git_revwalk_push_ref(git_revwalk *walk, const char *refname) return push_ref(walk, refname, 0); } +int git_revwalk_push_range(git_revwalk *walk, const char *range) +{ + git_revspec revspec; + int error = 0; + + if ((error = git_revparse(&revspec, walk->repo, range))) + return error; + + 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(revspec.from), 1))) + goto out; + + error = push_commit(walk, git_object_id(revspec.to), 0); + +out: + git_object_free(revspec.from); + git_object_free(revspec.to); + return error; +} + int git_revwalk_hide_ref(git_revwalk *walk, const char *refname) { assert(walk && refname); return push_ref(walk, refname, 1); } -static int revwalk_enqueue_timesort(git_revwalk *walk, commit_object *commit) +static int revwalk_enqueue_timesort(git_revwalk *walk, git_commit_list_node *commit) { return git_pqueue_insert(&walk->iterator_time, commit); } -static int revwalk_enqueue_unsorted(git_revwalk *walk, commit_object *commit) +static int revwalk_enqueue_unsorted(git_revwalk *walk, git_commit_list_node *commit) { - return commit_list_insert(commit, &walk->iterator_rand) ? 0 : -1; + return git_commit_list_insert(commit, &walk->iterator_rand) ? 0 : -1; } -static int revwalk_next_timesort(commit_object **object_out, git_revwalk *walk) +static int revwalk_next_timesort(git_commit_list_node **object_out, git_revwalk *walk) { int error; - commit_object *next; + git_commit_list_node *next; while ((next = git_pqueue_pop(&walk->iterator_time)) != NULL) { if ((error = process_commit_parents(walk, next)) < 0) @@ -600,15 +285,16 @@ static int revwalk_next_timesort(commit_object **object_out, git_revwalk *walk) } } - return GIT_REVWALKOVER; + giterr_clear(); + return GIT_ITEROVER; } -static int revwalk_next_unsorted(commit_object **object_out, git_revwalk *walk) +static int revwalk_next_unsorted(git_commit_list_node **object_out, git_revwalk *walk) { int error; - commit_object *next; + git_commit_list_node *next; - while ((next = commit_list_pop(&walk->iterator_rand)) != NULL) { + while ((next = git_commit_list_pop(&walk->iterator_rand)) != NULL) { if ((error = process_commit_parents(walk, next)) < 0) return error; @@ -618,18 +304,21 @@ static int revwalk_next_unsorted(commit_object **object_out, git_revwalk *walk) } } - return GIT_REVWALKOVER; + giterr_clear(); + return GIT_ITEROVER; } -static int revwalk_next_toposort(commit_object **object_out, git_revwalk *walk) +static int revwalk_next_toposort(git_commit_list_node **object_out, git_revwalk *walk) { - commit_object *next; + git_commit_list_node *next; unsigned short i; for (;;) { - next = commit_list_pop(&walk->iterator_topo); - if (next == NULL) - return GIT_REVWALKOVER; + next = git_commit_list_pop(&walk->iterator_topo); + if (next == NULL) { + giterr_clear(); + return GIT_ITEROVER; + } if (next->in_degree > 0) { next->topo_delay = 1; @@ -637,11 +326,11 @@ static int revwalk_next_toposort(commit_object **object_out, git_revwalk *walk) } for (i = 0; i < next->out_degree; ++i) { - commit_object *parent = next->parents[i]; + git_commit_list_node *parent = next->parents[i]; if (--parent->in_degree == 0 && parent->topo_delay) { parent->topo_delay = 0; - if (commit_list_insert(parent, &walk->iterator_topo) == NULL) + if (git_commit_list_insert(parent, &walk->iterator_topo) == NULL) return -1; } } @@ -651,10 +340,10 @@ static int revwalk_next_toposort(commit_object **object_out, git_revwalk *walk) } } -static int revwalk_next_reverse(commit_object **object_out, git_revwalk *walk) +static int revwalk_next_reverse(git_commit_list_node **object_out, git_revwalk *walk) { - *object_out = commit_list_pop(&walk->iterator_reverse); - return *object_out ? 0 : GIT_REVWALKOVER; + *object_out = git_commit_list_pop(&walk->iterator_reverse); + return *object_out ? 0 : GIT_ITEROVER; } @@ -662,21 +351,23 @@ static int prepare_walk(git_revwalk *walk) { int error; unsigned int i; - commit_object *next, *two; - commit_list *bases = NULL; + git_commit_list_node *next, *two; + git_commit_list *bases = NULL; /* * If walk->one is NULL, there were no positive references, * so we know that the walk is already over. */ - if (walk->one == NULL) - return GIT_REVWALKOVER; + if (walk->one == NULL) { + giterr_clear(); + return GIT_ITEROVER; + } /* first figure out what the merge bases are */ - if (merge_bases_many(&bases, walk, walk->one, &walk->twos) < 0) + if (git_merge__bases_many(&bases, walk, walk->one, &walk->twos) < 0) return -1; - commit_list_free(&bases); + git_commit_list_free(&bases); if (process_commit(walk, walk->one, walk->one->uninteresting) < 0) return -1; @@ -690,15 +381,15 @@ static int prepare_walk(git_revwalk *walk) while ((error = walk->get_next(&next, walk)) == 0) { for (i = 0; i < next->out_degree; ++i) { - commit_object *parent = next->parents[i]; + git_commit_list_node *parent = next->parents[i]; parent->in_degree++; } - if (commit_list_insert(next, &walk->iterator_topo) == NULL) + if (git_commit_list_insert(next, &walk->iterator_topo) == NULL) return -1; } - if (error != GIT_REVWALKOVER) + if (error != GIT_ITEROVER) return error; walk->get_next = &revwalk_next_toposort; @@ -707,10 +398,10 @@ static int prepare_walk(git_revwalk *walk) if (walk->sorting & GIT_SORT_REVERSE) { while ((error = walk->get_next(&next, walk)) == 0) - if (commit_list_insert(next, &walk->iterator_reverse) == NULL) + if (git_commit_list_insert(next, &walk->iterator_reverse) == NULL) return -1; - if (error != GIT_REVWALKOVER) + if (error != GIT_ITEROVER) return error; walk->get_next = &revwalk_next_reverse; @@ -721,9 +412,6 @@ static int prepare_walk(git_revwalk *walk) } - - - int git_revwalk_new(git_revwalk **revwalk_out, git_repository *repo) { git_revwalk *walk; @@ -736,7 +424,7 @@ int git_revwalk_new(git_revwalk **revwalk_out, git_repository *repo) walk->commits = git_oidmap_alloc(); GITERR_CHECK_ALLOC(walk->commits); - if (git_pqueue_init(&walk->iterator_time, 8, commit_time_cmp) < 0 || + if (git_pqueue_init(&walk->iterator_time, 8, git_commit_list_time_cmp) < 0 || git_vector_init(&walk->twos, 4, NULL) < 0 || git_pool_init(&walk->commit_pool, 1, git_pool__suggest_items_per_page(COMMIT_ALLOC) * COMMIT_ALLOC) < 0) @@ -798,7 +486,7 @@ void git_revwalk_sorting(git_revwalk *walk, unsigned int sort_mode) int git_revwalk_next(git_oid *oid, git_revwalk *walk) { int error; - commit_object *next; + git_commit_list_node *next; assert(walk && oid); @@ -809,9 +497,10 @@ int git_revwalk_next(git_oid *oid, git_revwalk *walk) error = walk->get_next(&next, walk); - if (error == GIT_REVWALKOVER) { + if (error == GIT_ITEROVER) { git_revwalk_reset(walk); - return GIT_REVWALKOVER; + giterr_clear(); + return GIT_ITEROVER; } if (!error) @@ -822,7 +511,7 @@ int git_revwalk_next(git_oid *oid, git_revwalk *walk) void git_revwalk_reset(git_revwalk *walk) { - commit_object *commit; + git_commit_list_node *commit; assert(walk); @@ -834,9 +523,9 @@ void git_revwalk_reset(git_revwalk *walk) }); git_pqueue_clear(&walk->iterator_time); - commit_list_free(&walk->iterator_topo); - commit_list_free(&walk->iterator_rand); - commit_list_free(&walk->iterator_reverse); + git_commit_list_free(&walk->iterator_topo); + git_commit_list_free(&walk->iterator_rand); + git_commit_list_free(&walk->iterator_reverse); walk->walking = 0; walk->one = NULL; diff --git a/src/revwalk.h b/src/revwalk.h new file mode 100644 index 000000000..22696dfcd --- /dev/null +++ b/src/revwalk.h @@ -0,0 +1,44 @@ +/* + * 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_revwalk_h__ +#define INCLUDE_revwalk_h__ + +#include "git2/revwalk.h" +#include "oidmap.h" +#include "commit_list.h" +#include "pqueue.h" +#include "pool.h" +#include "vector.h" + +GIT__USE_OIDMAP; + +struct git_revwalk { + git_repository *repo; + git_odb *odb; + + git_oidmap *commits; + git_pool commit_pool; + + git_commit_list *iterator_topo; + git_commit_list *iterator_rand; + git_commit_list *iterator_reverse; + git_pqueue iterator_time; + + int (*get_next)(git_commit_list_node **, git_revwalk *); + int (*enqueue)(git_revwalk *, git_commit_list_node *); + + unsigned walking:1; + unsigned int sorting; + + /* merge base calculation */ + git_commit_list_node *one; + git_vector twos; +}; + +git_commit_list_node *git_revwalk__commit_lookup(git_revwalk *walk, const git_oid *oid); + +#endif diff --git a/src/sha1.h b/src/sha1.h deleted file mode 100644 index 93a244d76..000000000 --- a/src/sha1.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2009-2012 the libgit2 contributors - * - * This file is part of libgit2, distributed under the GNU GPL v2 with - * a Linking Exception. For full terms see the included COPYING file. - */ - -typedef struct { - unsigned long long size; - unsigned int H[5]; - unsigned int W[16]; -} blk_SHA_CTX; - -void git__blk_SHA1_Init(blk_SHA_CTX *ctx); -void git__blk_SHA1_Update(blk_SHA_CTX *ctx, const void *dataIn, size_t len); -void git__blk_SHA1_Final(unsigned char hashout[20], blk_SHA_CTX *ctx); - -#define SHA_CTX blk_SHA_CTX -#define SHA1_Init git__blk_SHA1_Init -#define SHA1_Update git__blk_SHA1_Update -#define SHA1_Final git__blk_SHA1_Final diff --git a/src/sha1_lookup.c b/src/sha1_lookup.c index 096da1739..b7e66cc69 100644 --- a/src/sha1_lookup.c +++ b/src/sha1_lookup.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. diff --git a/src/sha1_lookup.h b/src/sha1_lookup.h index cd40a9d57..9a3537273 100644 --- a/src/sha1_lookup.h +++ b/src/sha1_lookup.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. diff --git a/src/signature.c b/src/signature.c index 7d329c4c9..164e8eb67 100644 --- a/src/signature.c +++ b/src/signature.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -22,80 +22,60 @@ void git_signature_free(git_signature *sig) git__free(sig); } -static const char *skip_leading_spaces(const char *buffer, const char *buffer_end) -{ - while (*buffer == ' ' && buffer < buffer_end) - buffer++; - - return buffer; -} - -static const char *skip_trailing_spaces(const char *buffer_start, const char *buffer_end) -{ - while (*buffer_end == ' ' && buffer_end > buffer_start) - buffer_end--; - - return buffer_end; -} - static int signature_error(const char *msg) { giterr_set(GITERR_INVALID, "Failed to parse signature - %s", msg); return -1; } -static int process_trimming(const char *input, char **storage, const char *input_end, int fail_when_empty) +static bool contains_angle_brackets(const char *input) { - const char *left, *right; - size_t trimmed_input_length; - - assert(storage); - - left = skip_leading_spaces(input, input_end); - right = skip_trailing_spaces(input, input_end - 1); - - if (right < left) { - if (fail_when_empty) - return signature_error("input is either empty of contains only spaces"); + return strchr(input, '<') != NULL || strchr(input, '>') != NULL; +} - right = left - 1; +static char *extract_trimmed(const char *ptr, size_t len) +{ + while (len && ptr[0] == ' ') { + ptr++; len--; } - trimmed_input_length = right - left + 1; - - *storage = git__malloc(trimmed_input_length + 1); - GITERR_CHECK_ALLOC(*storage); - - memcpy(*storage, left, trimmed_input_length); - (*storage)[trimmed_input_length] = 0; + while (len && ptr[len - 1] == ' ') { + len--; + } - return 0; + return git__substrdup(ptr, len); } int git_signature_new(git_signature **sig_out, const char *name, const char *email, git_time_t time, int offset) { - int error; git_signature *p = NULL; assert(name && email); *sig_out = NULL; + if (contains_angle_brackets(name) || + contains_angle_brackets(email)) { + return signature_error( + "Neither `name` nor `email` should contain angle brackets chars."); + } + p = git__calloc(1, sizeof(git_signature)); GITERR_CHECK_ALLOC(p); - if ((error = process_trimming(name, &p->name, name + strlen(name), 1)) < 0 || - (error = process_trimming(email, &p->email, email + strlen(email), 1)) < 0) - { + 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') { git_signature_free(p); - return error; + return -1; } - + p->when.time = time; p->when.offset = offset; *sig_out = p; - return 0; } @@ -111,36 +91,26 @@ int git_signature_now(git_signature **sig_out, const char *name, const char *ema { time_t now; time_t offset; - struct tm *utc_tm, *local_tm; + struct tm *utc_tm; git_signature *sig; - -#ifndef GIT_WIN32 - struct tm _utc, _local; -#endif + struct tm _utc; *sig_out = NULL; - time(&now); - - /** - * On Win32, `gmtime_r` doesn't exist but - * `gmtime` is threadsafe, so we can use that + /* + * Get the current time as seconds since the epoch and + * transform that into a tm struct containing the time at + * UTC. Give that to mktime which considers it a local time + * (tm_isdst = -1 asks it to take DST into account) and gives + * us that time as seconds since the epoch. The difference + * between its return value and 'now' is our offset to UTC. */ -#ifdef GIT_WIN32 - utc_tm = gmtime(&now); - local_tm = localtime(&now); -#else - utc_tm = gmtime_r(&now, &_utc); - local_tm = localtime_r(&now, &_local); -#endif - - offset = mktime(local_tm) - mktime(utc_tm); + time(&now); + utc_tm = p_gmtime_r(&now, &_utc); + utc_tm->tm_isdst = -1; + offset = (time_t)difftime(now, mktime(utc_tm)); offset /= 60; - /* mktime takes care of setting tm_isdst correctly */ - if (local_tm->tm_isdst) - offset += 60; - if (git_signature_new(&sig, name, email, now, (int)offset) < 0) return -1; @@ -149,169 +119,71 @@ int git_signature_now(git_signature **sig_out, const char *name, const char *ema return 0; } -static int timezone_error(const char *msg) -{ - giterr_set(GITERR_INVALID, "Failed to parse TZ offset - %s", msg); - return -1; -} - -static int parse_timezone_offset(const char *buffer, int *offset_out) -{ - int dec_offset; - int mins, hours, offset; - - const char *offset_start; - const char *offset_end; - - offset_start = buffer; - - if (*offset_start == '\n') { - *offset_out = 0; - return 0; - } - - if (offset_start[0] != '-' && offset_start[0] != '+') - return timezone_error("does not start with '+' or '-'"); - - if (offset_start[1] < '0' || offset_start[1] > '9') - return timezone_error("expected initial digit"); - - if (git__strtol32(&dec_offset, offset_start + 1, &offset_end, 10) < 0) - return timezone_error("not a valid number"); - - if (offset_end - offset_start != 5) - return timezone_error("invalid length"); - - if (dec_offset > 1400) - return timezone_error("value too large"); - - hours = dec_offset / 100; - mins = dec_offset % 100; - - if (hours > 14) // see http://www.worldtimezone.com/faq.html - return timezone_error("hour value too large"); - - if (mins > 59) - return timezone_error("minutes value too large"); - - offset = (hours * 60) + mins; - - if (offset_start[0] == '-') - offset *= -1; - - *offset_out = offset; - - return 0; -} - -static int process_next_token(const char **buffer_out, char **storage, - const char *token_end, const char *right_boundary) -{ - int error = process_trimming(*buffer_out, storage, token_end, 0); - if (error < 0) - return error; - - *buffer_out = token_end + 1; - - if (*buffer_out > right_boundary) - return signature_error("signature is too short"); - - return 0; -} - -static const char *scan_for_previous_token(const char *buffer, const char *left_boundary) -{ - const char *start; - - if (buffer <= left_boundary) - return NULL; - - start = skip_trailing_spaces(left_boundary, buffer); - - /* Search for previous occurence of space */ - while (start[-1] != ' ' && start > left_boundary) - start--; - - return start; -} - -static int parse_time(git_time_t *time_out, const char *buffer) -{ - int time; - int error; - - if (*buffer == '+' || *buffer == '-') { - giterr_set(GITERR_INVALID, "Failed while parsing time. '%s' actually looks like a timezone offset.", buffer); - return -1; - } - - error = git__strtol32(&time, buffer, &buffer, 10); - - if (!error) - *time_out = (git_time_t)time; - - return error; -} - int git_signature__parse(git_signature *sig, const char **buffer_out, const char *buffer_end, const char *header, char ender) { const char *buffer = *buffer_out; - const char *line_end, *name_end, *email_end, *tz_start, *time_start; - int error = 0; + const char *email_start, *email_end; - memset(sig, 0x0, sizeof(git_signature)); + memset(sig, 0, sizeof(git_signature)); - if ((line_end = memchr(buffer, ender, buffer_end - buffer)) == NULL) + if ((buffer_end = memchr(buffer, ender, buffer_end - buffer)) == NULL) return signature_error("no newline given"); if (header) { const size_t header_len = strlen(header); - if (memcmp(buffer, header, header_len) != 0) + if (buffer + header_len >= buffer_end || memcmp(buffer, header, header_len) != 0) return signature_error("expected prefix doesn't match actual"); buffer += header_len; } - if (buffer > line_end) - return signature_error("signature too short"); - - if ((name_end = strchr(buffer, '<')) == NULL) - return signature_error("character '<' not allowed in signature"); + email_start = git__memrchr(buffer, '<', buffer_end - buffer); + email_end = git__memrchr(buffer, '>', buffer_end - buffer); - if ((email_end = strchr(name_end, '>')) == NULL) - return signature_error("character '>' not allowed in signature"); - - if (email_end < name_end) + if (!email_start || !email_end || email_end <= email_start) return signature_error("malformed e-mail"); - error = process_next_token(&buffer, &sig->name, name_end, line_end); - if (error < 0) - return error; - - error = process_next_token(&buffer, &sig->email, email_end, line_end); - if (error < 0) - return error; - - tz_start = scan_for_previous_token(line_end - 1, buffer); - - if (tz_start == NULL) - goto clean_exit; /* No timezone nor date */ - - time_start = scan_for_previous_token(tz_start - 1, buffer); - if (time_start == NULL || parse_time(&sig->when.time, time_start) < 0) { - /* The tz_start might point at the time */ - parse_time(&sig->when.time, tz_start); - goto clean_exit; - } - - if (parse_timezone_offset(tz_start, &sig->when.offset) < 0) { - sig->when.time = 0; /* Bogus timezone, we reset the time */ + email_start += 1; + sig->name = extract_trimmed(buffer, email_start - buffer - 1); + sig->email = extract_trimmed(email_start, email_end - email_start); + + /* Do we even have a time at the end of the signature? */ + if (email_end + 2 < buffer_end) { + const char *time_start = email_end + 2; + const char *time_end; + + if (git__strtol64(&sig->when.time, time_start, &time_end, 10) < 0) + return signature_error("invalid Unix timestamp"); + + /* do we have a timezone? */ + if (time_end + 1 < buffer_end) { + int offset, hours, mins; + const char *tz_start, *tz_end; + + 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"); + + hours = offset / 100; + mins = offset % 100; + + /* + * only store timezone if it's not overflowing; + * see http://www.worldtimezone.com/faq.html + */ + if (hours < 14 && mins < 59) { + sig->when.offset = (hours * 60) + mins; + if (tz_start[0] == '-') + sig->when.offset = -sig->when.offset; + } + } } -clean_exit: - *buffer_out = line_end + 1; + *buffer_out = buffer_end + 1; return 0; } diff --git a/src/signature.h b/src/signature.h index 97b3a055e..24655cbf5 100644 --- a/src/signature.h +++ b/src/signature.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. diff --git a/src/stash.c b/src/stash.c new file mode 100644 index 000000000..355c5dc9c --- /dev/null +++ b/src/stash.c @@ -0,0 +1,663 @@ +/* + * 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 "commit.h" +#include "tree.h" +#include "reflog.h" +#include "git2/diff.h" +#include "git2/stash.h" +#include "git2/status.h" +#include "git2/checkout.h" +#include "signature.h" + +static int create_error(int error, const char *msg) +{ + giterr_set(GITERR_STASH, "Cannot stash changes - %s", msg); + return error; +} + +static int retrieve_head(git_reference **out, git_repository *repo) +{ + int error = git_repository_head(out, repo); + + if (error == GIT_EORPHANEDHEAD) + return create_error(error, "You do not have the initial commit yet."); + + return error; +} + +static int append_abbreviated_oid(git_buf *out, const git_oid *b_commit) +{ + char *formatted_oid; + + formatted_oid = git_oid_allocfmt(b_commit); + GITERR_CHECK_ALLOC(formatted_oid); + + git_buf_put(out, formatted_oid, 7); + git__free(formatted_oid); + + return git_buf_oom(out) ? -1 : 0; +} + +static int append_commit_description(git_buf *out, git_commit* commit) +{ + const char *message; + size_t pos = 0, len; + + if (append_abbreviated_oid(out, git_commit_id(commit)) < 0) + return -1; + + message = git_commit_message(commit); + len = strlen(message); + + /* TODO: Replace with proper commit short message + * when git_commit_message_short() is implemented. + */ + while (pos < len && message[pos] != '\n') + pos++; + + git_buf_putc(out, ' '); + git_buf_put(out, message, pos); + git_buf_putc(out, '\n'); + + return git_buf_oom(out) ? -1 : 0; +} + +static int retrieve_base_commit_and_message( + git_commit **b_commit, + git_buf *stash_message, + git_repository *repo) +{ + git_reference *head = NULL; + int error; + + if ((error = retrieve_head(&head, repo)) < 0) + return error; + + if (strcmp("HEAD", git_reference_name(head)) == 0) + error = git_buf_puts(stash_message, "(no branch): "); + else + error = git_buf_printf( + stash_message, + "%s: ", + git_reference_name(head) + strlen(GIT_REFS_HEADS_DIR)); + if (error < 0) + goto cleanup; + + if ((error = git_commit_lookup( + b_commit, repo, git_reference_target(head))) < 0) + goto cleanup; + + if ((error = append_commit_description(stash_message, *b_commit)) < 0) + goto cleanup; + +cleanup: + git_reference_free(head); + return error; +} + +static int build_tree_from_index(git_tree **out, git_index *index) +{ + int error; + git_oid i_tree_oid; + + if ((error = git_index_write_tree(&i_tree_oid, index)) < 0) + return -1; + + return git_tree_lookup(out, git_index_owner(index), &i_tree_oid); +} + +static int commit_index( + git_commit **i_commit, + git_index *index, + git_signature *stasher, + const char *message, + const git_commit *parent) +{ + git_tree *i_tree = NULL; + git_oid i_commit_oid; + git_buf msg = GIT_BUF_INIT; + int error; + + if ((error = build_tree_from_index(&i_tree, index)) < 0) + goto cleanup; + + if ((error = git_buf_printf(&msg, "index on %s\n", message)) < 0) + goto cleanup; + + if ((error = git_commit_create( + &i_commit_oid, + git_index_owner(index), + NULL, + stasher, + stasher, + NULL, + git_buf_cstr(&msg), + i_tree, + 1, + &parent)) < 0) + goto cleanup; + + error = git_commit_lookup(i_commit, git_index_owner(index), &i_commit_oid); + +cleanup: + git_tree_free(i_tree); + git_buf_free(&msg); + return error; +} + +struct cb_data { + git_index *index; + + int error; + + bool include_changed; + bool include_untracked; + bool include_ignored; +}; + +static int update_index_cb( + const git_diff_delta *delta, + float progress, + void *payload) +{ + struct cb_data *data = (struct cb_data *)payload; + const char *add_path = NULL; + + GIT_UNUSED(progress); + + switch (delta->status) { + case GIT_DELTA_IGNORED: + if (data->include_ignored) + add_path = delta->new_file.path; + break; + + case GIT_DELTA_UNTRACKED: + if (data->include_untracked) + add_path = delta->new_file.path; + break; + + case GIT_DELTA_ADDED: + case GIT_DELTA_MODIFIED: + if (data->include_changed) + add_path = delta->new_file.path; + break; + + case GIT_DELTA_DELETED: + if (!data->include_changed) + break; + if (git_index_find(NULL, data->index, delta->old_file.path) == 0) + data->error = git_index_remove( + data->index, delta->old_file.path, 0); + break; + + default: + /* Unimplemented */ + giterr_set( + GITERR_INVALID, + "Cannot update index. Unimplemented status (%d)", + delta->status); + data->error = -1; + break; + } + + if (add_path != NULL) + data->error = git_index_add_bypath(data->index, add_path); + + return data->error; +} + +static int build_untracked_tree( + git_tree **tree_out, + git_index *index, + git_commit *i_commit, + uint32_t flags) +{ + git_tree *i_tree = NULL; + git_diff_list *diff = NULL; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + struct cb_data data = {0}; + int error; + + git_index_clear(index); + + data.index = index; + + if (flags & GIT_STASH_INCLUDE_UNTRACKED) { + opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED | + GIT_DIFF_RECURSE_UNTRACKED_DIRS; + data.include_untracked = true; + } + + if (flags & GIT_STASH_INCLUDE_IGNORED) { + opts.flags |= GIT_DIFF_INCLUDE_IGNORED; + data.include_ignored = true; + } + + if ((error = git_commit_tree(&i_tree, i_commit)) < 0) + goto cleanup; + + if ((error = git_diff_tree_to_workdir( + &diff, git_index_owner(index), i_tree, &opts)) < 0) + goto cleanup; + + if ((error = git_diff_foreach( + diff, update_index_cb, NULL, NULL, &data)) < 0) + { + if (error == GIT_EUSER) + error = data.error; + goto cleanup; + } + + error = build_tree_from_index(tree_out, index); + +cleanup: + git_diff_list_free(diff); + git_tree_free(i_tree); + return error; +} + +static int commit_untracked( + git_commit **u_commit, + git_index *index, + git_signature *stasher, + const char *message, + git_commit *i_commit, + uint32_t flags) +{ + git_tree *u_tree = NULL; + git_oid u_commit_oid; + git_buf msg = GIT_BUF_INIT; + int error; + + if ((error = build_untracked_tree(&u_tree, index, i_commit, flags)) < 0) + goto cleanup; + + if ((error = git_buf_printf(&msg, "untracked files on %s\n", message)) < 0) + goto cleanup; + + if ((error = git_commit_create( + &u_commit_oid, + git_index_owner(index), + NULL, + stasher, + stasher, + NULL, + git_buf_cstr(&msg), + u_tree, + 0, + NULL)) < 0) + goto cleanup; + + error = git_commit_lookup(u_commit, git_index_owner(index), &u_commit_oid); + +cleanup: + git_tree_free(u_tree); + git_buf_free(&msg); + return error; +} + +static int build_workdir_tree( + git_tree **tree_out, + git_index *index, + git_commit *b_commit) +{ + git_repository *repo = git_index_owner(index); + git_tree *b_tree = NULL; + git_diff_list *diff = NULL, *diff2 = NULL; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + struct cb_data data = {0}; + int error; + + if ((error = git_commit_tree(&b_tree, b_commit)) < 0) + goto cleanup; + + if ((error = git_diff_tree_to_index(&diff, repo, b_tree, NULL, &opts)) < 0) + goto cleanup; + + if ((error = git_diff_index_to_workdir(&diff2, repo, NULL, &opts)) < 0) + goto cleanup; + + if ((error = git_diff_merge(diff, diff2)) < 0) + goto cleanup; + + data.index = index; + data.include_changed = true; + + if ((error = git_diff_foreach( + diff, update_index_cb, NULL, NULL, &data)) < 0) + { + if (error == GIT_EUSER) + error = data.error; + goto cleanup; + } + + + if ((error = build_tree_from_index(tree_out, index)) < 0) + goto cleanup; + +cleanup: + git_diff_list_free(diff); + git_diff_list_free(diff2); + git_tree_free(b_tree); + + return error; +} + +static int commit_worktree( + git_oid *w_commit_oid, + git_index *index, + git_signature *stasher, + const char *message, + git_commit *i_commit, + git_commit *b_commit, + git_commit *u_commit) +{ + int error = 0; + git_tree *w_tree = NULL, *i_tree = NULL; + const git_commit *parents[] = { NULL, NULL, NULL }; + + parents[0] = b_commit; + parents[1] = i_commit; + parents[2] = u_commit; + + if ((error = git_commit_tree(&i_tree, i_commit)) < 0) + goto cleanup; + + if ((error = git_index_read_tree(index, i_tree)) < 0) + goto cleanup; + + if ((error = build_workdir_tree(&w_tree, index, b_commit)) < 0) + goto cleanup; + + error = git_commit_create( + w_commit_oid, + git_index_owner(index), + NULL, + stasher, + stasher, + NULL, + message, + w_tree, + u_commit ? 3 : 2, + parents); + +cleanup: + git_tree_free(i_tree); + git_tree_free(w_tree); + return error; +} + +static int prepare_worktree_commit_message( + git_buf* msg, + const char *user_message) +{ + git_buf buf = GIT_BUF_INIT; + int error; + + if ((error = git_buf_set(&buf, git_buf_cstr(msg), git_buf_len(msg))) < 0) + return error; + + git_buf_clear(msg); + + if (!user_message) + git_buf_printf(msg, "WIP on %s", git_buf_cstr(&buf)); + else { + const char *colon; + + if ((colon = strchr(git_buf_cstr(&buf), ':')) == NULL) + goto cleanup; + + git_buf_puts(msg, "On "); + git_buf_put(msg, git_buf_cstr(&buf), colon - buf.ptr); + git_buf_printf(msg, ": %s\n", user_message); + } + + error = (git_buf_oom(msg) || git_buf_oom(&buf)) ? -1 : 0; + +cleanup: + git_buf_free(&buf); + + return error; +} + +static int update_reflog( + git_oid *w_commit_oid, + git_repository *repo, + git_signature *stasher, + const char *message) +{ + git_reference *stash = NULL; + git_reflog *reflog = NULL; + int error; + + if ((error = git_reference_create(&stash, repo, GIT_REFS_STASH_FILE, w_commit_oid, 1)) < 0) + goto cleanup; + + if ((error = git_reflog_read(&reflog, stash)) < 0) + goto cleanup; + + if ((error = git_reflog_append(reflog, w_commit_oid, stasher, message)) < 0) + goto cleanup; + + if ((error = git_reflog_write(reflog)) < 0) + goto cleanup; + +cleanup: + git_reference_free(stash); + git_reflog_free(reflog); + return error; +} + +static int is_dirty_cb(const char *path, unsigned int status, void *payload) +{ + GIT_UNUSED(path); + GIT_UNUSED(status); + GIT_UNUSED(payload); + + return 1; +} + +static int ensure_there_are_changes_to_stash( + git_repository *repo, + bool include_untracked_files, + bool include_ignored_files) +{ + int error; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + + opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + if (include_untracked_files) + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + + if (include_ignored_files) + opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED; + + error = git_status_foreach_ext(repo, &opts, is_dirty_cb, NULL); + + if (error == GIT_EUSER) + return 0; + + if (!error) + return create_error(GIT_ENOTFOUND, "There is nothing to stash."); + + return error; +} + +static int reset_index_and_workdir( + git_repository *repo, + git_commit *commit, + bool remove_untracked) +{ + git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT; + + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + + if (remove_untracked) + opts.checkout_strategy |= GIT_CHECKOUT_REMOVE_UNTRACKED; + + return git_checkout_tree(repo, (git_object *)commit, &opts); +} + +int git_stash_save( + git_oid *out, + git_repository *repo, + git_signature *stasher, + const char *message, + uint32_t flags) +{ + git_index *index = NULL; + git_commit *b_commit = NULL, *i_commit = NULL, *u_commit = NULL; + git_buf msg = GIT_BUF_INIT; + int error; + + assert(out && repo && stasher); + + if ((error = git_repository__ensure_not_bare(repo, "stash save")) < 0) + return error; + + if ((error = retrieve_base_commit_and_message(&b_commit, &msg, repo)) < 0) + goto cleanup; + + if ((error = ensure_there_are_changes_to_stash( + repo, + (flags & GIT_STASH_INCLUDE_UNTRACKED) != 0, + (flags & GIT_STASH_INCLUDE_IGNORED) != 0)) < 0) + goto cleanup; + + if ((error = git_repository_index(&index, repo)) < 0) + goto cleanup; + + if ((error = commit_index( + &i_commit, index, stasher, git_buf_cstr(&msg), b_commit)) < 0) + goto cleanup; + + if ((flags & (GIT_STASH_INCLUDE_UNTRACKED | GIT_STASH_INCLUDE_IGNORED)) && + (error = commit_untracked( + &u_commit, index, stasher, git_buf_cstr(&msg), + i_commit, flags)) < 0) + goto cleanup; + + if ((error = prepare_worktree_commit_message(&msg, message)) < 0) + goto cleanup; + + if ((error = commit_worktree( + out, index, stasher, git_buf_cstr(&msg), + i_commit, b_commit, u_commit)) < 0) + goto cleanup; + + git_buf_rtrim(&msg); + + if ((error = update_reflog(out, repo, stasher, git_buf_cstr(&msg))) < 0) + goto cleanup; + + if ((error = reset_index_and_workdir( + repo, + ((flags & GIT_STASH_KEEP_INDEX) != 0) ? i_commit : b_commit, + (flags & GIT_STASH_INCLUDE_UNTRACKED) != 0)) < 0) + goto cleanup; + +cleanup: + + git_buf_free(&msg); + git_commit_free(i_commit); + git_commit_free(b_commit); + git_commit_free(u_commit); + git_index_free(index); + + return error; +} + +int git_stash_foreach( + git_repository *repo, + git_stash_cb callback, + void *payload) +{ + git_reference *stash; + git_reflog *reflog = NULL; + int error; + size_t i, max; + const git_reflog_entry *entry; + + error = git_reference_lookup(&stash, repo, GIT_REFS_STASH_FILE); + if (error == GIT_ENOTFOUND) + return 0; + if (error < 0) + goto cleanup; + + if ((error = git_reflog_read(&reflog, stash)) < 0) + goto cleanup; + + max = git_reflog_entrycount(reflog); + for (i = 0; i < max; i++) { + entry = git_reflog_entry_byindex(reflog, i); + + if (callback(i, + git_reflog_entry_message(entry), + git_reflog_entry_id_new(entry), + payload)) { + error = GIT_EUSER; + break; + } + } + +cleanup: + git_reference_free(stash); + git_reflog_free(reflog); + return error; +} + +int git_stash_drop( + git_repository *repo, + size_t index) +{ + git_reference *stash; + git_reflog *reflog = NULL; + size_t max; + int error; + + if ((error = git_reference_lookup(&stash, repo, GIT_REFS_STASH_FILE)) < 0) + return error; + + if ((error = git_reflog_read(&reflog, stash)) < 0) + goto cleanup; + + max = git_reflog_entrycount(reflog); + + if (index > max - 1) { + error = GIT_ENOTFOUND; + giterr_set(GITERR_STASH, "No stashed state at position %" PRIuZ, index); + goto cleanup; + } + + if ((error = git_reflog_drop(reflog, index, true)) < 0) + goto cleanup; + + if ((error = git_reflog_write(reflog)) < 0) + goto cleanup; + + if (max == 1) { + error = git_reference_delete(stash); + git_reference_free(stash); + stash = NULL; + } else if (index == 0) { + 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); + } + +cleanup: + git_reference_free(stash); + git_reflog_free(reflog); + return error; +} diff --git a/src/status.c b/src/status.c index e9ad3cfe4..ac6b4379b 100644 --- a/src/status.c +++ b/src/status.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -17,6 +17,7 @@ #include "git2/diff.h" #include "diff.h" +#include "diff_output.h" static unsigned int index_delta2status(git_delta_t index_status) { @@ -25,7 +26,6 @@ static unsigned int index_delta2status(git_delta_t index_status) switch (index_status) { case GIT_DELTA_ADDED: case GIT_DELTA_COPIED: - case GIT_DELTA_RENAMED: st = GIT_STATUS_INDEX_NEW; break; case GIT_DELTA_DELETED: @@ -34,6 +34,12 @@ static unsigned int index_delta2status(git_delta_t index_status) case GIT_DELTA_MODIFIED: st = GIT_STATUS_INDEX_MODIFIED; break; + case GIT_DELTA_RENAMED: + st = GIT_STATUS_INDEX_RENAMED; + break; + case GIT_DELTA_TYPECHANGE: + st = GIT_STATUS_INDEX_TYPECHANGE; + break; default: break; } @@ -47,8 +53,8 @@ static unsigned int workdir_delta2status(git_delta_t workdir_status) switch (workdir_status) { case GIT_DELTA_ADDED: - case GIT_DELTA_COPIED: case GIT_DELTA_RENAMED: + case GIT_DELTA_COPIED: case GIT_DELTA_UNTRACKED: st = GIT_STATUS_WT_NEW; break; @@ -61,6 +67,9 @@ static unsigned int workdir_delta2status(git_delta_t workdir_status) case GIT_DELTA_IGNORED: st = GIT_STATUS_IGNORED; break; + case GIT_DELTA_TYPECHANGE: + st = GIT_STATUS_WT_TYPECHANGE; + break; default: break; } @@ -68,29 +77,76 @@ 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) +{ + status_user_callback *usercb = payload; + const char *path = NULL; + unsigned int status = 0; + + if (i2w) { + path = i2w->old_file.path; + status |= workdir_delta2status(i2w->status); + } + if (h2i) { + path = h2i->old_file.path; + status |= index_delta2status(h2i->status); + } + + /* 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; + } + + return usercb->cb(path, status, usercb->payload); +} + int git_status_foreach_ext( git_repository *repo, const git_status_options *opts, - int (*cb)(const char *, unsigned int, void *), - void *cbdata) + git_status_cb cb, + void *payload) { - int err = 0, cmp; - git_diff_options diffopt; - git_diff_list *idx2head = NULL, *wd2idx = NULL; + int err = 0; + git_diff_options diffopt = GIT_DIFF_OPTIONS_INIT; + git_diff_list *head2idx = NULL, *idx2wd = NULL; git_tree *head = NULL; git_status_show_t show = opts ? opts->show : GIT_STATUS_SHOW_INDEX_AND_WORKDIR; - git_diff_delta *i2h, *w2i; - unsigned int i, j, i_max, j_max; + status_user_callback usercb; assert(show <= GIT_STATUS_SHOW_INDEX_THEN_WORKDIR); - if ((err = git_repository_head_tree(&head, repo)) < 0) + 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; - memset(&diffopt, 0, sizeof(diffopt)); + /* 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; + memcpy(&diffopt.pathspec, &opts->pathspec, sizeof(diffopt.pathspec)); + diffopt.flags = GIT_DIFF_INCLUDE_TYPECHANGE; + if ((opts->flags & GIT_STATUS_OPT_INCLUDE_UNTRACKED) != 0) diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNTRACKED; if ((opts->flags & GIT_STATUS_OPT_INCLUDE_IGNORED) != 0) @@ -99,62 +155,58 @@ int git_status_foreach_ext( diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNMODIFIED; if ((opts->flags & GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS) != 0) diffopt.flags = diffopt.flags | GIT_DIFF_RECURSE_UNTRACKED_DIRS; - /* TODO: support EXCLUDE_SUBMODULES flag */ + if ((opts->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) + diffopt.flags = diffopt.flags | GIT_DIFF_RECURSE_IGNORED_DIRS; + if ((opts->flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES) != 0) + diffopt.flags = diffopt.flags | GIT_DIFF_IGNORE_SUBMODULES; + + if (show != GIT_STATUS_SHOW_WORKDIR_ONLY) { + err = git_diff_tree_to_index(&head2idx, repo, head, NULL, &diffopt); + if (err < 0) + goto cleanup; + } - if (show != GIT_STATUS_SHOW_WORKDIR_ONLY && - (err = git_diff_index_to_tree(repo, &diffopt, head, &idx2head)) < 0) - goto cleanup; + if (show != GIT_STATUS_SHOW_INDEX_ONLY) { + err = git_diff_index_to_workdir(&idx2wd, repo, NULL, &diffopt); + if (err < 0) + goto cleanup; + } - if (show != GIT_STATUS_SHOW_INDEX_ONLY && - (err = git_diff_workdir_to_index(repo, &diffopt, &wd2idx)) < 0) - goto cleanup; + usercb.cb = cb; + usercb.payload = payload; + usercb.opts = opts; if (show == GIT_STATUS_SHOW_INDEX_THEN_WORKDIR) { - for (i = 0; !err && i < idx2head->deltas.length; i++) { - i2h = GIT_VECTOR_GET(&idx2head->deltas, i); - err = cb(i2h->old_file.path, index_delta2status(i2h->status), cbdata); - } - git_diff_list_free(idx2head); - idx2head = NULL; - } + if ((err = git_diff__paired_foreach( + head2idx, NULL, status_invoke_cb, &usercb)) < 0) + goto cleanup; - i_max = idx2head ? idx2head->deltas.length : 0; - j_max = wd2idx ? wd2idx->deltas.length : 0; - - for (i = 0, j = 0; !err && (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 : strcmp(i2h->old_file.path, w2i->old_file.path); - - if (cmp < 0) { - err = cb(i2h->old_file.path, index_delta2status(i2h->status), cbdata); - i++; - } else if (cmp > 0) { - err = cb(w2i->old_file.path, workdir_delta2status(w2i->status), cbdata); - j++; - } else { - err = cb(i2h->old_file.path, index_delta2status(i2h->status) | - workdir_delta2status(w2i->status), cbdata); - i++; j++; - } + git_diff_list_free(head2idx); + head2idx = NULL; } + err = git_diff__paired_foreach(head2idx, idx2wd, status_invoke_cb, &usercb); + cleanup: git_tree_free(head); - git_diff_list_free(idx2head); - git_diff_list_free(wd2idx); + git_diff_list_free(head2idx); + git_diff_list_free(idx2wd); + + if (err == GIT_EUSER) + giterr_clear(); + return err; } int git_status_foreach( git_repository *repo, - int (*callback)(const char *, unsigned int, void *), + git_status_cb callback, void *payload) { - git_status_options opts; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; - memset(&opts, 0, sizeof(opts)); opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED | GIT_STATUS_OPT_INCLUDE_UNTRACKED | @@ -164,22 +216,29 @@ int git_status_foreach( } struct status_file_info { + char *expected; unsigned int count; unsigned int status; - char *expected; + int fnm_flags; + int ambiguous; }; static int get_one_status(const char *path, unsigned int status, void *data) { struct status_file_info *sfi = data; + int (*strcomp)(const char *a, const char *b); sfi->count++; sfi->status = status; - if (sfi->count > 1 || strcmp(sfi->expected, path) != 0) { - giterr_set(GITERR_INVALID, - "Ambiguous path '%s' given to git_status_file", sfi->expected); - return -1; + strcomp = (sfi->fnm_flags & FNM_CASEFOLD) ? git__strcasecmp : git__strcmp; + + if (sfi->count > 1 || + (strcomp(sfi->expected, path) != 0 && + p_fnmatch(sfi->expected, path, sfi->fnm_flags) != 0)) + { + sfi->ambiguous = true; + return GIT_EAMBIGUOUS; } return 0; @@ -191,16 +250,20 @@ int git_status_file( const char *path) { int error; - git_status_options opts; - struct status_file_info sfi; + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + struct status_file_info sfi = {0}; + git_index *index; assert(status_flags && repo && path); - memset(&sfi, 0, sizeof(sfi)); + if ((error = git_repository_index__weakptr(&index, repo)) < 0) + return error; + if ((sfi.expected = git__strdup(path)) == NULL) return -1; + if (index->ignore_case) + sfi.fnm_flags = FNM_CASEFOLD; - memset(&opts, 0, sizeof(opts)); opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED | GIT_STATUS_OPT_INCLUDE_UNTRACKED | @@ -211,10 +274,29 @@ int git_status_file( error = git_status_foreach_ext(repo, &opts, get_one_status, &sfi); - if (!error && !sfi.count) { + if (error < 0 && sfi.ambiguous) { giterr_set(GITERR_INVALID, - "Attempt to get status of nonexistent file '%s'", path); - error = GIT_ENOTFOUND; + "Ambiguous path '%s' given to git_status_file", sfi.expected); + error = GIT_EAMBIGUOUS; + } + + 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); } *status_flags = sfi.status; @@ -229,14 +311,6 @@ int git_status_should_ignore( git_repository *repo, const char *path) { - int error; - git_ignores ignores; - - if (git_ignore__for_path(repo, path, &ignores) < 0) - return -1; - - error = git_ignore__lookup(&ignores, path, ignored); - git_ignore__free(&ignores); - return error; + return git_ignore_path_is_ignored(ignored, repo, path); } diff --git a/src/strmap.h b/src/strmap.h index da5ca0dba..44176a0fc 100644 --- a/src/strmap.h +++ b/src/strmap.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -19,7 +19,7 @@ __KHASH_TYPE(str, const char *, void *); typedef khash_t(str) git_strmap; #define GIT__USE_STRMAP \ - __KHASH_IMPL(str, static inline, const char *, void *, 1, kh_str_hash_func, kh_str_hash_equal) + __KHASH_IMPL(str, static kh_inline, const char *, void *, 1, kh_str_hash_func, kh_str_hash_equal) #define git_strmap_alloc() kh_init(str) #define git_strmap_free(h) kh_destroy(str, h), h = NULL diff --git a/src/submodule.c b/src/submodule.c index 3c07e657d..2fdaf2f77 100644 --- a/src/submodule.c +++ b/src/submodule.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -12,64 +12,867 @@ #include "git2/index.h" #include "git2/submodule.h" #include "buffer.h" +#include "buf_text.h" #include "vector.h" #include "posix.h" #include "config_file.h" #include "config.h" #include "repository.h" +#include "submodule.h" +#include "tree.h" +#include "iterator.h" + +#define GIT_MODULES_FILE ".gitmodules" static git_cvar_map _sm_update_map[] = { {GIT_CVAR_STRING, "checkout", GIT_SUBMODULE_UPDATE_CHECKOUT}, {GIT_CVAR_STRING, "rebase", GIT_SUBMODULE_UPDATE_REBASE}, - {GIT_CVAR_STRING, "merge", GIT_SUBMODULE_UPDATE_MERGE} + {GIT_CVAR_STRING, "merge", GIT_SUBMODULE_UPDATE_MERGE}, + {GIT_CVAR_STRING, "none", GIT_SUBMODULE_UPDATE_NONE}, }; static git_cvar_map _sm_ignore_map[] = { - {GIT_CVAR_STRING, "all", GIT_SUBMODULE_IGNORE_ALL}, - {GIT_CVAR_STRING, "dirty", GIT_SUBMODULE_IGNORE_DIRTY}, + {GIT_CVAR_STRING, "none", GIT_SUBMODULE_IGNORE_NONE}, {GIT_CVAR_STRING, "untracked", GIT_SUBMODULE_IGNORE_UNTRACKED}, - {GIT_CVAR_STRING, "none", GIT_SUBMODULE_IGNORE_NONE} + {GIT_CVAR_STRING, "dirty", GIT_SUBMODULE_IGNORE_DIRTY}, + {GIT_CVAR_STRING, "all", GIT_SUBMODULE_IGNORE_ALL}, }; -static inline khint_t str_hash_no_trailing_slash(const char *s) +static kh_inline khint_t str_hash_no_trailing_slash(const char *s) { khint_t h; for (h = 0; *s; ++s) - if (s[1] || *s != '/') + if (s[1] != '\0' || *s != '/') h = (h << 5) - h + *s; return h; } -static inline int str_equal_no_trailing_slash(const char *a, const char *b) +static kh_inline int str_equal_no_trailing_slash(const char *a, const char *b) { size_t alen = a ? strlen(a) : 0; size_t blen = b ? strlen(b) : 0; - if (alen && a[alen] == '/') + if (alen > 0 && a[alen - 1] == '/') alen--; - if (blen && b[blen] == '/') + if (blen > 0 && b[blen - 1] == '/') blen--; return (alen == blen && strncmp(a, b, alen) == 0); } -__KHASH_IMPL(str, static inline, const char *, void *, 1, str_hash_no_trailing_slash, str_equal_no_trailing_slash); +__KHASH_IMPL( + str, static kh_inline, const char *, void *, 1, + str_hash_no_trailing_slash, str_equal_no_trailing_slash); + +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 git_submodule *submodule_alloc(const char *name) +static int submodule_cmp(const void *a, const void *b) { - git_submodule *sm = git__calloc(1, sizeof(git_submodule)); - if (sm == NULL) - return sm; + return strcmp(((git_submodule *)a)->name, ((git_submodule *)b)->name); +} - sm->path = sm->name = git__strdup(name); - if (!sm->name) { - git__free(sm); +static int submodule_config_key_trunc_puts(git_buf *key, const char *suffix) +{ + ssize_t idx = git_buf_rfind(key, '.'); + git_buf_truncate(key, (size_t)(idx + 1)); + return git_buf_puts(key, suffix); +} + +/* + * PUBLIC APIS + */ + +int git_submodule_lookup( + git_submodule **sm_ptr, /* NULL if user only wants to test existence */ + git_repository *repo, + const char *name) /* trailing slash is allowed */ +{ + int error; + khiter_t pos; + + assert(repo && name); + + if ((error = load_submodule_config(repo)) < 0) + return error; + + pos = git_strmap_lookup_index(repo->submodules, name); + + if (!git_strmap_valid_index(repo->submodules, pos)) { + error = GIT_ENOTFOUND; + + /* check if a plausible submodule exists at path */ + if (git_repository_workdir(repo)) { + git_buf path = GIT_BUF_INIT; + + if (git_buf_joinpath(&path, git_repository_workdir(repo), name) < 0) + return -1; + + if (git_path_contains_dir(&path, DOT_GIT)) + error = GIT_EEXISTS; + + git_buf_free(&path); + } + + return error; + } + + if (sm_ptr) + *sm_ptr = git_strmap_value_at(repo->submodules, pos); + + return 0; +} + +int git_submodule_foreach( + git_repository *repo, + int (*callback)(git_submodule *sm, const char *name, void *payload), + void *payload) +{ + int error; + git_submodule *sm; + git_vector seen = GIT_VECTOR_INIT; + seen._cmp = submodule_cmp; + + assert(repo && callback); + + if ((error = load_submodule_config(repo)) < 0) + return error; + + git_strmap_foreach_value(repo->submodules, sm, { + /* Usually the following will not come into play - it just prevents + * us from issuing a callback twice for a submodule where the name + * and path are not the same. + */ + if (sm->refcount > 1) { + if (git_vector_bsearch(NULL, &seen, sm) != GIT_ENOTFOUND) + continue; + if ((error = git_vector_insert(&seen, sm)) < 0) + break; + } + + if (callback(sm, sm->name, payload)) { + giterr_clear(); + error = GIT_EUSER; + break; + } + }); + + git_vector_free(&seen); + + return error; +} + +void git_submodule_config_free(git_repository *repo) +{ + git_strmap *smcfg; + git_submodule *sm; + + assert(repo); + + smcfg = repo->submodules; + repo->submodules = NULL; + + if (smcfg == NULL) + return; + + git_strmap_foreach_value(smcfg, sm, { + submodule_release(sm,1); + }); + git_strmap_free(smcfg); +} + +int git_submodule_add_setup( + git_submodule **submodule, + git_repository *repo, + const char *url, + const char *path, + int use_gitlink) +{ + int error = 0; + git_config_backend *mods = NULL; + git_submodule *sm; + git_buf name = GIT_BUF_INIT, real_url = GIT_BUF_INIT; + git_repository_init_options initopt = GIT_REPOSITORY_INIT_OPTIONS_INIT; + git_repository *subrepo = NULL; + + assert(repo && url && path); + + /* see if there is already an entry for this submodule */ + + if (git_submodule_lookup(&sm, repo, path) < 0) + giterr_clear(); + else { + giterr_set(GITERR_SUBMODULE, + "Attempt to add a submodule that already exists"); + return GIT_EEXISTS; + } + + /* resolve parameters */ + + if (url[0] == '.' && (url[1] == '/' || (url[1] == '.' && url[2] == '/'))) { + if (!(error = lookup_head_remote(&real_url, repo))) + error = git_path_apply_relative(&real_url, url); + } else if (strchr(url, ':') != NULL || url[0] == '/') { + error = git_buf_sets(&real_url, url); + } else { + giterr_set(GITERR_SUBMODULE, "Invalid format for submodule URL"); + error = -1; + } + if (error) + goto cleanup; + + /* validate and normalize path */ + + if (git__prefixcmp(path, git_repository_workdir(repo)) == 0) + path += strlen(git_repository_workdir(repo)); + + if (git_path_root(path) >= 0) { + giterr_set(GITERR_SUBMODULE, "Submodule path must be a relative path"); + error = -1; + goto cleanup; + } + + /* update .gitmodules */ + + if ((mods = open_gitmodules(repo, true, NULL)) == NULL) { + giterr_set(GITERR_SUBMODULE, + "Adding submodules to a bare repository is not supported (for now)"); + return -1; + } + + if ((error = git_buf_printf(&name, "submodule.%s.path", path)) < 0 || + (error = git_config_file_set_string(mods, name.ptr, path)) < 0) + goto cleanup; + + if ((error = submodule_config_key_trunc_puts(&name, "url")) < 0 || + (error = git_config_file_set_string(mods, name.ptr, real_url.ptr)) < 0) + goto cleanup; + + git_buf_clear(&name); + + /* init submodule repository and add origin remote as needed */ + + error = git_buf_joinpath(&name, git_repository_workdir(repo), path); + if (error < 0) + goto cleanup; + + /* New style: sub-repo goes in <repo-dir>/modules/<name>/ with a + * gitlink in the sub-repo workdir directory to that repository + * + * Old style: sub-repo goes directly into repo/<name>/.git/ + */ + + initopt.flags = GIT_REPOSITORY_INIT_MKPATH | + GIT_REPOSITORY_INIT_NO_REINIT; + initopt.origin_url = real_url.ptr; + + if (git_path_exists(name.ptr) && + git_path_contains(&name, DOT_GIT)) + { + /* repo appears to already exist - reinit? */ + } + else if (use_gitlink) { + git_buf repodir = GIT_BUF_INIT; + + error = git_buf_join_n( + &repodir, '/', 3, git_repository_path(repo), "modules", path); + if (error < 0) + goto cleanup; + + initopt.workdir_path = name.ptr; + initopt.flags |= GIT_REPOSITORY_INIT_NO_DOTGIT_DIR; + + error = git_repository_init_ext(&subrepo, repodir.ptr, &initopt); + + git_buf_free(&repodir); + } + else { + error = git_repository_init_ext(&subrepo, name.ptr, &initopt); + } + if (error < 0) + goto cleanup; + + /* add submodule to hash and "reload" it */ + + if (!(error = submodule_get(&sm, repo, path, NULL)) && + !(error = git_submodule_reload(sm))) + error = git_submodule_init(sm, false); + +cleanup: + if (submodule != NULL) + *submodule = !error ? sm : NULL; + + if (mods != NULL) + git_config_file_free(mods); + git_repository_free(subrepo); + git_buf_free(&real_url); + git_buf_free(&name); + + return error; +} + +int git_submodule_add_finalize(git_submodule *sm) +{ + int error; + git_index *index; + + assert(sm); + + if ((error = git_repository_index__weakptr(&index, sm->owner)) < 0 || + (error = git_index_add_bypath(index, GIT_MODULES_FILE)) < 0) + return error; + + return git_submodule_add_to_index(sm, true); +} + +int git_submodule_add_to_index(git_submodule *sm, int write_index) +{ + int error; + git_repository *repo, *sm_repo = NULL; + git_index *index; + git_buf path = GIT_BUF_INIT; + git_commit *head; + git_index_entry entry; + struct stat st; + + 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 || + (error = git_buf_joinpath( + &path, git_repository_workdir(repo), sm->path)) < 0 || + (error = git_submodule_open(&sm_repo, sm)) < 0) + goto cleanup; + + /* read stat information for submodule working directory */ + if (p_stat(path.ptr, &st) < 0) { + giterr_set(GITERR_SUBMODULE, + "Cannot add submodule without working directory"); + error = -1; + goto cleanup; + } + + memset(&entry, 0, sizeof(entry)); + entry.path = sm->path; + git_index_entry__init_from_stat(&entry, &st); + + /* calling git_submodule_open will have set sm->wd_oid if possible */ + if ((sm->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) == 0) { + giterr_set(GITERR_SUBMODULE, + "Cannot add submodule without HEAD to index"); + error = -1; + goto cleanup; + } + git_oid_cpy(&entry.oid, &sm->wd_oid); + + if ((error = git_commit_lookup(&head, sm_repo, &sm->wd_oid)) < 0) + goto cleanup; + + entry.ctime.seconds = git_commit_time(head); + entry.ctime.nanoseconds = 0; + entry.mtime.seconds = git_commit_time(head); + entry.mtime.nanoseconds = 0; + + git_commit_free(head); + + /* add it */ + error = git_index_add(index, &entry); + + /* write it, if requested */ + if (!error && write_index) { + error = git_index_write(index); + + if (!error) + git_oid_cpy(&sm->index_oid, &sm->wd_oid); + } + +cleanup: + git_repository_free(sm_repo); + git_buf_free(&path); + return error; +} + +int git_submodule_save(git_submodule *submodule) +{ + int error = 0; + git_config_backend *mods; + git_buf key = GIT_BUF_INIT; + + assert(submodule); + + mods = open_gitmodules(submodule->owner, true, NULL); + if (!mods) { + giterr_set(GITERR_SUBMODULE, + "Adding submodules to a bare repository is not supported (for now)"); + return -1; + } + + if ((error = git_buf_printf(&key, "submodule.%s.", submodule->name)) < 0) + goto cleanup; + + /* save values for path, url, update, ignore, fetchRecurseSubmodules */ + + if ((error = submodule_config_key_trunc_puts(&key, "path")) < 0 || + (error = git_config_file_set_string(mods, key.ptr, submodule->path)) < 0) + goto cleanup; + + if ((error = submodule_config_key_trunc_puts(&key, "url")) < 0 || + (error = git_config_file_set_string(mods, key.ptr, submodule->url)) < 0) + 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; + 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; + error = git_config_file_set_string(mods, key.ptr, val); + } + if (error < 0) + goto cleanup; + + if ((error = submodule_config_key_trunc_puts( + &key, "fetchRecurseSubmodules")) < 0 || + (error = git_config_file_set_string( + mods, key.ptr, submodule->fetch_recurse ? "true" : "false")) < 0) + goto cleanup; + + /* update internal defaults */ + + submodule->ignore_default = submodule->ignore; + submodule->update_default = submodule->update; + submodule->flags |= GIT_SUBMODULE_STATUS_IN_CONFIG; + +cleanup: + if (mods != NULL) + git_config_file_free(mods); + git_buf_free(&key); + + return error; +} + +git_repository *git_submodule_owner(git_submodule *submodule) +{ + assert(submodule); + return submodule->owner; +} + +const char *git_submodule_name(git_submodule *submodule) +{ + assert(submodule); + return submodule->name; +} + +const char *git_submodule_path(git_submodule *submodule) +{ + assert(submodule); + return submodule->path; +} + +const char *git_submodule_url(git_submodule *submodule) +{ + assert(submodule); + return submodule->url; +} + +int git_submodule_set_url(git_submodule *submodule, const char *url) +{ + assert(submodule && url); + + git__free(submodule->url); + + submodule->url = git__strdup(url); + GITERR_CHECK_ALLOC(submodule->url); + + return 0; +} + +const git_oid *git_submodule_index_id(git_submodule *submodule) +{ + assert(submodule); + + if (submodule->flags & GIT_SUBMODULE_STATUS__INDEX_OID_VALID) + return &submodule->index_oid; + else + return NULL; +} + +const git_oid *git_submodule_head_id(git_submodule *submodule) +{ + assert(submodule); + + if (submodule->flags & GIT_SUBMODULE_STATUS__HEAD_OID_VALID) + return &submodule->head_oid; + else + return NULL; +} + +const git_oid *git_submodule_wd_id(git_submodule *submodule) +{ + assert(submodule); + + 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)) + git_repository_free(subrepo); + else + giterr_clear(); + } + + if (submodule->flags & GIT_SUBMODULE_STATUS__WD_OID_VALID) + return &submodule->wd_oid; + else return NULL; +} + +git_submodule_ignore_t git_submodule_ignore(git_submodule *submodule) +{ + assert(submodule); + return submodule->ignore; +} + +git_submodule_ignore_t git_submodule_set_ignore( + git_submodule *submodule, git_submodule_ignore_t ignore) +{ + git_submodule_ignore_t old; + + assert(submodule); + + if (ignore == GIT_SUBMODULE_IGNORE_DEFAULT) + ignore = submodule->ignore_default; + + old = submodule->ignore; + submodule->ignore = ignore; + return old; +} + +git_submodule_update_t git_submodule_update(git_submodule *submodule) +{ + assert(submodule); + return submodule->update; +} + +git_submodule_update_t git_submodule_set_update( + git_submodule *submodule, git_submodule_update_t update) +{ + git_submodule_update_t old; + + assert(submodule); + + if (update == GIT_SUBMODULE_UPDATE_DEFAULT) + update = submodule->update_default; + + old = submodule->update; + submodule->update = update; + return old; +} + +int git_submodule_fetch_recurse_submodules( + git_submodule *submodule) +{ + assert(submodule); + return submodule->fetch_recurse; +} + +int git_submodule_set_fetch_recurse_submodules( + git_submodule *submodule, + int fetch_recurse_submodules) +{ + int old; + + assert(submodule); + + old = submodule->fetch_recurse; + submodule->fetch_recurse = (fetch_recurse_submodules != 0); + return old; +} + +int git_submodule_init(git_submodule *submodule, int overwrite) +{ + int error; + + /* write "submodule.NAME.url" */ + + if (!submodule->url) { + giterr_set(GITERR_SUBMODULE, + "No URL configured for submodule '%s'", submodule->name); + return -1; + } + + error = submodule_update_config( + submodule, "url", submodule->url, overwrite != 0, false); + if (error < 0) + return error; + + /* 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); + + return error; +} + +int git_submodule_sync(git_submodule *submodule) +{ + if (!submodule->url) { + giterr_set(GITERR_SUBMODULE, + "No URL configured for submodule '%s'", submodule->name); + return -1; + } + + /* copy URL over to config only if it already exists */ + + return submodule_update_config( + submodule, "url", submodule->url, true, true); +} + +int git_submodule_open( + git_repository **subrepo, + git_submodule *submodule) +{ + int error; + git_buf path = GIT_BUF_INIT; + git_repository *repo; + const char *workdir; + + assert(submodule && subrepo); + + repo = submodule->owner; + workdir = git_repository_workdir(repo); + + if (!workdir) { + giterr_set(GITERR_REPOSITORY, + "Cannot open submodule repository in a bare repo"); + return GIT_ENOTFOUND; + } + + 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; + } + + if (git_buf_joinpath(&path, workdir, submodule->path) < 0) + return -1; + + error = git_repository_open(subrepo, path.ptr); + + git_buf_free(&path); + + /* if we have opened the submodule successfully, let's grab the HEAD OID */ + if (!error) { + if (!git_reference_name_to_id( + &submodule->wd_oid, *subrepo, GIT_HEAD_FILE)) + submodule->flags |= GIT_SUBMODULE_STATUS__WD_OID_VALID; + else + giterr_clear(); } + return error; +} + +int git_submodule_reload_all(git_repository *repo) +{ + assert(repo); + git_submodule_config_free(repo); + return load_submodule_config(repo); +} + +int git_submodule_reload(git_submodule *submodule) +{ + git_repository *repo; + git_index *index; + int error; + size_t pos; + git_tree *head; + git_config_backend *mods; + + assert(submodule); + + /* refresh index data */ + + repo = submodule->owner; + if (git_repository_index__weakptr(&index, repo) < 0) + return -1; + + submodule->flags = submodule->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 (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); + } + } + + /* refresh HEAD tree data */ + + 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); + + if (!(error = git_tree_entry_bypath(&te, head, submodule->path))) { + + 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); + } + + git_tree_entry_free(te); + } + else if (error == GIT_ENOTFOUND) { + giterr_clear(); + error = 0; + } + + git_tree_free(head); + } + + if (error < 0) + return error; + + /* refresh config data */ + + if ((mods = open_gitmodules(repo, false, NULL)) != NULL) { + git_buf path = GIT_BUF_INIT; + + git_buf_sets(&path, "submodule\\."); + git_buf_text_puts_escape_regex(&path, submodule->name); + git_buf_puts(&path, ".*"); + + if (git_buf_oom(&path)) + error = -1; + else + error = git_config_file_foreach_match( + mods, path.ptr, submodule_load_from_config, repo); + + git_buf_free(&path); + git_config_file_free(mods); + } + + if (error < 0) + return error; + + /* refresh wd data */ + + submodule->flags = submodule->flags & + ~(GIT_SUBMODULE_STATUS_IN_WD | GIT_SUBMODULE_STATUS__WD_OID_VALID); + + error = submodule_load_from_wd_lite(submodule, submodule->path, NULL); + + return error; +} + +int git_submodule_status( + unsigned int *status, + git_submodule *submodule) +{ + int error = 0; + unsigned int status_val; + + assert(status && submodule); + + status_val = GIT_SUBMODULE_STATUS__CLEAR_INTERNAL(submodule->flags); + + if (submodule->ignore != GIT_SUBMODULE_IGNORE_ALL) { + if (!(error = submodule_index_status(&status_val, submodule))) + error = submodule_wd_status(&status_val, submodule); + } + + *status = status_val; + + return error; +} + +int git_submodule_location( + unsigned int *location_status, + git_submodule *submodule) +{ + assert(location_status && submodule); + + *location_status = submodule->flags & + (GIT_SUBMODULE_STATUS_IN_HEAD | GIT_SUBMODULE_STATUS_IN_INDEX | + GIT_SUBMODULE_STATUS_IN_CONFIG | GIT_SUBMODULE_STATUS_IN_WD); + + return 0; +} + + +/* + * INTERNAL FUNCTIONS + */ + +static git_submodule *submodule_alloc(git_repository *repo, const char *name) +{ + git_submodule *sm; + + if (!name || !strlen(name)) { + giterr_set(GITERR_SUBMODULE, "Invalid submodule name"); + return NULL; + } + + sm = git__calloc(1, sizeof(git_submodule)); + if (sm == NULL) + goto fail; + + sm->path = sm->name = git__strdup(name); + if (!sm->name) + goto fail; + + sm->owner = repo; + sm->refcount = 1; + return sm; + +fail: + submodule_release(sm, 0); + return NULL; } static void submodule_release(git_submodule *sm, int decr) @@ -80,70 +883,122 @@ static void submodule_release(git_submodule *sm, int decr) sm->refcount -= decr; if (sm->refcount == 0) { - if (sm->name != sm->path) + 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; + git__free(sm); } } -static int submodule_from_entry( - git_strmap *smcfg, git_index_entry *entry) +static int submodule_get( + git_submodule **sm_ptr, + git_repository *repo, + const char *name, + const char *alternate) { - git_submodule *sm; - void *old_sm; + git_strmap *smcfg = repo->submodules; khiter_t pos; + git_submodule *sm; int error; - pos = git_strmap_lookup_index(smcfg, entry->path); + assert(repo && name); - if (git_strmap_valid_index(smcfg, pos)) - sm = git_strmap_value_at(smcfg, pos); - else - sm = submodule_alloc(entry->path); + pos = git_strmap_lookup_index(smcfg, name); - git_oid_cpy(&sm->oid, &entry->oid); + if (!git_strmap_valid_index(smcfg, pos) && alternate) + pos = git_strmap_lookup_index(smcfg, alternate); - if (strcmp(sm->path, entry->path) != 0) { - if (sm->path != sm->name) { - git__free(sm->path); - sm->path = sm->name; + if (!git_strmap_valid_index(smcfg, pos)) { + sm = submodule_alloc(repo, name); + + /* insert value at name - if another thread beats us to it, then use + * their record and release our own. + */ + pos = kh_put(str, smcfg, sm->name, &error); + + if (error < 0) { + submodule_release(sm, 1); + sm = NULL; + } else if (error == 0) { + submodule_release(sm, 1); + sm = git_strmap_value_at(smcfg, pos); + } else { + git_strmap_set_value_at(smcfg, pos, sm); } - sm->path = git__strdup(entry->path); - if (!sm->path) - goto fail; + } else { + sm = git_strmap_value_at(smcfg, pos); } - git_strmap_insert2(smcfg, sm->path, sm, old_sm, error); - if (error < 0) - goto fail; - sm->refcount++; + *sm_ptr = sm; + + return (sm != NULL) ? 0 : -1; +} - if (old_sm && ((git_submodule *)old_sm) != sm) { - /* TODO: log warning about multiple entrys for same submodule path */ - submodule_release(old_sm, 1); +static int submodule_load_from_index( + git_repository *repo, const git_index_entry *entry) +{ + git_submodule *sm; + + if (submodule_get(&sm, repo, entry->path, NULL) < 0) + return -1; + + if (sm->flags & GIT_SUBMODULE_STATUS_IN_INDEX) { + sm->flags |= GIT_SUBMODULE_STATUS__INDEX_MULTIPLE_ENTRIES; + return 0; } + sm->flags |= GIT_SUBMODULE_STATUS_IN_INDEX; + + git_oid_cpy(&sm->index_oid, &entry->oid); + sm->flags |= GIT_SUBMODULE_STATUS__INDEX_OID_VALID; + return 0; +} -fail: - submodule_release(sm, 0); +static int submodule_load_from_head( + git_repository *repo, const char *path, const git_oid *oid) +{ + git_submodule *sm; + + if (submodule_get(&sm, repo, path, NULL) < 0) + return -1; + + sm->flags |= GIT_SUBMODULE_STATUS_IN_HEAD; + + git_oid_cpy(&sm->head_oid, oid); + sm->flags |= GIT_SUBMODULE_STATUS__HEAD_OID_VALID; + + 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_from_config( - const char *key, const char *value, void *data) +static int submodule_load_from_config( + const git_config_entry *entry, void *data) { - git_strmap *smcfg = data; - const char *namestart; - const char *property; + git_repository *repo = data; + git_strmap *smcfg = repo->submodules; + const char *namestart, *property, *alternate = NULL; + const char *key = entry->name, *value = entry->value; git_buf name = GIT_BUF_INIT; git_submodule *sm; - void *old_sm = NULL; bool is_path; - khiter_t pos; - int error; + int error = 0; if (git__prefixcmp(key, "submodule.") != 0) return 0; @@ -153,235 +1008,504 @@ static int submodule_from_config( if (property == NULL) return 0; property++; - is_path = (strcmp(property, "path") == 0); + is_path = (strcasecmp(property, "path") == 0); if (git_buf_set(&name, namestart, property - namestart - 1) < 0) return -1; - pos = git_strmap_lookup_index(smcfg, name.ptr); - if (!git_strmap_valid_index(smcfg, pos) && is_path) - pos = git_strmap_lookup_index(smcfg, value); - if (!git_strmap_valid_index(smcfg, pos)) - sm = submodule_alloc(name.ptr); - else - sm = git_strmap_value_at(smcfg, pos); - if (!sm) - goto fail; + if (submodule_get(&sm, repo, name.ptr, is_path ? value : NULL) < 0) { + git_buf_free(&name); + return -1; + } - if (strcmp(sm->name, name.ptr) != 0) { - assert(sm->path == sm->name); - sm->name = git_buf_detach(&name); + sm->flags |= GIT_SUBMODULE_STATUS_IN_CONFIG; - git_strmap_insert2(smcfg, sm->name, sm, old_sm, error); - if (error < 0) - goto fail; - sm->refcount++; + /* Only from config might we get differing names & paths. If so, then + * update the submodule and insert under the alternative key. + */ + + /* TODO: if case insensitive filesystem, then the following strcmps + * should be strcasecmp + */ + + if (strcmp(sm->name, name.ptr) != 0) { + alternate = sm->name = git_buf_detach(&name); + } else if (is_path && value && strcmp(sm->path, value) != 0) { + alternate = sm->path = git__strdup(value); + if (!sm->path) + error = -1; } - else if (is_path && strcmp(sm->path, value) != 0) { - assert(sm->path == sm->name); - sm->path = git__strdup(value); - if (sm->path == NULL) - goto fail; + if (alternate) { + void *old_sm = NULL; + git_strmap_insert2(smcfg, alternate, sm, old_sm, error); - git_strmap_insert2(smcfg, sm->path, sm, old_sm, error); - if (error < 0) - goto fail; - sm->refcount++; + if (error >= 0) + sm->refcount++; /* 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); + /* TODO: log warning about multiple submodules with same path */ + } } + git_buf_free(&name); + if (error < 0) + return error; - if (old_sm && ((git_submodule *)old_sm) != sm) { - /* TODO: log warning about multiple submodules with same path */ - submodule_release(old_sm, 1); - } + /* TODO: Look up path in index and if it is present but not a GITLINK + * then this should be deleted (at least to match git's behavior) + */ if (is_path) return 0; /* copy other properties into submodule entry */ - if (strcmp(property, "url") == 0) { - if (sm->url) { - git__free(sm->url); - sm->url = NULL; - } - if ((sm->url = git__strdup(value)) == NULL) - goto fail; + if (strcasecmp(property, "url") == 0) { + git__free(sm->url); + sm->url = NULL; + + if (value != NULL && (sm->url = git__strdup(value)) == NULL) + return -1; } - else if (strcmp(property, "update") == 0) { + else if (strcasecmp(property, "update") == 0) { int val; if (git_config_lookup_map_value( - _sm_update_map, ARRAY_SIZE(_sm_update_map), value, &val) < 0) { - giterr_set(GITERR_INVALID, - "Invalid value for submodule update property: '%s'", value); - goto fail; - } - sm->update = (git_submodule_update_t)val; + &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; } - else if (strcmp(property, "fetchRecurseSubmodules") == 0) { - if (git__parse_bool(&sm->fetch_recurse, value) < 0) { - giterr_set(GITERR_INVALID, - "Invalid value for submodule 'fetchRecurseSubmodules' property: '%s'", value); - goto fail; - } + else if (strcasecmp(property, "fetchRecurseSubmodules") == 0) { + if (git__parse_bool(&sm->fetch_recurse, value) < 0) + return submodule_config_error("fetchRecurseSubmodules", value); } - else if (strcmp(property, "ignore") == 0) { + else if (strcasecmp(property, "ignore") == 0) { int val; if (git_config_lookup_map_value( - _sm_ignore_map, ARRAY_SIZE(_sm_ignore_map), value, &val) < 0) { - giterr_set(GITERR_INVALID, - "Invalid value for submodule ignore property: '%s'", value); - goto fail; - } - sm->ignore = (git_submodule_ignore_t)val; + &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; } /* ignore other unknown submodule properties */ return 0; +} -fail: - submodule_release(sm, 0); - git_buf_free(&name); - return -1; +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) + return -1; + + if (git_path_isdir(path.ptr)) + sm->flags |= GIT_SUBMODULE_STATUS__WD_SCANNED; + + if (git_path_contains(&path, DOT_GIT)) + sm->flags |= GIT_SUBMODULE_STATUS_IN_WD; + + git_buf_free(&path); + + return 0; } -static int load_submodule_config(git_repository *repo) +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) { int error; git_index *index; - unsigned int i, max_i; - git_oid gitmodules_oid; - git_strmap *smcfg; - struct git_config_file *mods = NULL; + git_iterator *i; + const git_index_entry *entry; - if (repo->submodules) - return 0; + if ((error = git_repository_index__weakptr(&index, repo)) < 0 || + (error = git_iterator_for_index(&i, index, 0, NULL, NULL)) < 0) + return error; - /* submodule data is kept in a hashtable with each submodule stored - * under both its name and its path. These are usually the same, but - * that is not guaranteed. - */ - smcfg = git_strmap_alloc(); - GITERR_CHECK_ALLOC(smcfg); + error = git_iterator_current(&entry, i); - /* scan index for gitmodules (and .gitmodules entry) */ - if ((error = git_repository_index__weakptr(&index, repo)) < 0) - goto cleanup; - memset(&gitmodules_oid, 0, sizeof(gitmodules_oid)); - max_i = git_index_entrycount(index); + while (!error && entry != NULL) { - for (i = 0; i < max_i; i++) { - git_index_entry *entry = git_index_get(index, i); if (S_ISGITLINK(entry->mode)) { - if ((error = submodule_from_entry(smcfg, entry)) < 0) - goto cleanup; + 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); } - else if (strcmp(entry->path, ".gitmodules") == 0) - git_oid_cpy(&gitmodules_oid, &entry->oid); + + error = git_iterator_advance(&entry, i); } - /* load .gitmodules from workdir if it exists */ - if (git_repository_workdir(repo) != NULL) { - /* look in workdir for .gitmodules */ - git_buf path = GIT_BUF_INIT; - if (!git_buf_joinpath( - &path, git_repository_workdir(repo), ".gitmodules") && - git_path_isfile(path.ptr)) - { - if (!(error = git_config_file__ondisk(&mods, path.ptr))) - error = git_config_file_open(mods); + git_iterator_free(i); + + return error; +} + +static int load_submodule_config_from_head( + git_repository *repo, git_oid *gitmodules_oid) +{ + int error; + git_tree *head; + git_iterator *i; + const git_index_entry *entry; + + if ((error = git_repository_head_tree(&head, repo)) < 0) + return error; + + 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); } - git_buf_free(&path); + + error = git_iterator_advance(&entry, i); } - /* load .gitmodules from object cache if not in workdir */ - if (!error && mods == NULL && !git_oid_iszero(&gitmodules_oid)) { - /* TODO: is it worth loading gitmodules from object cache? */ + git_iterator_free(i); + git_tree_free(head); + + return error; +} + +static git_config_backend *open_gitmodules( + git_repository *repo, + bool okay_to_create, + const git_oid *gitmodules_oid) +{ + const char *workdir = git_repository_workdir(repo); + git_buf path = GIT_BUF_INIT; + git_config_backend *mods = NULL; + + if (workdir != NULL) { + if (git_buf_joinpath(&path, workdir, GIT_MODULES_FILE) != 0) + return NULL; + + if (okay_to_create || git_path_isfile(path.ptr)) { + /* git_config_file__ondisk should only fail if OOM */ + if (git_config_file__ondisk(&mods, path.ptr) < 0) + mods = NULL; + /* open should only fail here if the file is malformed */ + else if (git_config_file_open(mods, GIT_CONFIG_LEVEL_LOCAL) < 0) { + git_config_file_free(mods); + mods = NULL; + } + } + } + + if (!mods && gitmodules_oid && !git_oid_iszero(gitmodules_oid)) { + /* TODO: Retrieve .gitmodules content from ODB */ + + /* Should we actually do this? Core git does not, but it means you + * can't really get much information about submodules on bare repos. + */ + } + + git_buf_free(&path); + + return mods; +} + +static int load_submodule_config(git_repository *repo) +{ + int error; + git_oid gitmodules_oid; + git_buf path = GIT_BUF_INIT; + git_config_backend *mods = NULL; + + if (repo->submodules) + return 0; + + memset(&gitmodules_oid, 0, sizeof(gitmodules_oid)); + + /* Submodule data is kept in a hashtable keyed by both name and path. + * These are usually the same, but that is not guaranteed. + */ + if (!repo->submodules) { + repo->submodules = git_strmap_alloc(); + GITERR_CHECK_ALLOC(repo->submodules); } - /* process .gitmodules info */ - if (!error && mods != NULL) - error = git_config_file_foreach(mods, submodule_from_config, smcfg); + /* add submodule information from index */ + + if ((error = load_submodule_config_from_index(repo, &gitmodules_oid)) < 0) + goto cleanup; - /* store submodule config in repo */ - if (!error) - repo->submodules = smcfg; + /* add submodule information from HEAD */ + + if ((error = load_submodule_config_from_head(repo, &gitmodules_oid)) < 0) + goto cleanup; + + /* add submodule information from .gitmodules */ + + if ((mods = open_gitmodules(repo, false, &gitmodules_oid)) != NULL) + error = git_config_file_foreach(mods, submodule_load_from_config, repo); + + if (error != 0) + goto cleanup; + + /* shallow scan submodules in work tree */ + + if (!git_repository_is_bare(repo)) + error = git_submodule_foreach(repo, submodule_load_from_wd_lite, NULL); cleanup: + git_buf_free(&path); + if (mods != NULL) git_config_file_free(mods); + if (error) - git_strmap_free(smcfg); + git_submodule_config_free(repo); + return error; } -void git_submodule_config_free(git_repository *repo) +static int lookup_head_remote(git_buf *url, git_repository *repo) { - git_strmap *smcfg = repo->submodules; - git_submodule *sm; + int error; + git_config *cfg; + git_reference *head = NULL, *remote = NULL; + const char *tgt, *scan; + git_buf key = GIT_BUF_INIT; + + /* 1. resolve HEAD -> refs/heads/BRANCH + * 2. lookup config branch.BRANCH.remote -> ORIGIN + * 3. lookup remote.ORIGIN.url + */ - repo->submodules = NULL; + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + return error; - if (smcfg == NULL) - return; + if (git_reference_lookup(&head, repo, GIT_HEAD_FILE) < 0) { + giterr_set(GITERR_SUBMODULE, + "Cannot resolve relative URL when HEAD cannot be resolved"); + error = GIT_ENOTFOUND; + goto cleanup; + } - git_strmap_foreach_value(smcfg, sm, { - submodule_release(sm,1); - }); - git_strmap_free(smcfg); -} + if (git_reference_type(head) != GIT_REF_SYMBOLIC) { + giterr_set(GITERR_SUBMODULE, + "Cannot resolve relative URL when HEAD is not symbolic"); + error = GIT_ENOTFOUND; + goto cleanup; + } -static int submodule_cmp(const void *a, const void *b) -{ - return strcmp(((git_submodule *)a)->name, ((git_submodule *)b)->name); + if ((error = git_branch_upstream(&remote, head)) < 0) + goto cleanup; + + /* remote should refer to something like refs/remotes/ORIGIN/BRANCH */ + + if (git_reference_type(remote) != GIT_REF_SYMBOLIC || + git__prefixcmp(git_reference_symbolic_target(remote), GIT_REFS_REMOTES_DIR) != 0) + { + giterr_set(GITERR_SUBMODULE, + "Cannot resolve relative URL when HEAD is not symbolic"); + error = GIT_ENOTFOUND; + goto cleanup; + } + + scan = tgt = git_reference_symbolic_target(remote) + strlen(GIT_REFS_REMOTES_DIR); + while (*scan && (*scan != '/' || (scan > tgt && scan[-1] != '\\'))) + scan++; /* find non-escaped slash to end ORIGIN name */ + + error = git_buf_printf(&key, "remote.%.*s.url", (int)(scan - tgt), tgt); + if (error < 0) + goto cleanup; + + if ((error = git_config_get_string(&tgt, cfg, key.ptr)) < 0) + goto cleanup; + + error = git_buf_sets(url, tgt); + +cleanup: + git_buf_free(&key); + git_reference_free(head); + git_reference_free(remote); + + return error; } -int git_submodule_foreach( - git_repository *repo, - int (*callback)(const char *name, void *payload), - void *payload) +static int submodule_update_config( + git_submodule *submodule, + const char *attr, + const char *value, + bool overwrite, + bool only_existing) { int error; - git_submodule *sm; - git_vector seen = GIT_VECTOR_INIT; - seen._cmp = submodule_cmp; + git_config *config; + git_buf key = GIT_BUF_INIT; + const char *old = NULL; - if ((error = load_submodule_config(repo)) < 0) + assert(submodule); + + error = git_repository_config__weakptr(&config, submodule->owner); + if (error < 0) return error; - git_strmap_foreach_value(repo->submodules, sm, { - /* usually the following will not come into play */ - if (sm->refcount > 1) { - if (git_vector_bsearch(&seen, sm) != GIT_ENOTFOUND) - continue; - if ((error = git_vector_insert(&seen, sm)) < 0) - break; - } + error = git_buf_printf(&key, "submodule.%s.%s", submodule->name, attr); + if (error < 0) + goto cleanup; - if ((error = callback(sm->name, payload)) < 0) - break; - }); + if (git_config_get_string(&old, config, key.ptr) < 0) + giterr_clear(); - git_vector_free(&seen); + if (!old && only_existing) + goto cleanup; + if (old && !overwrite) + goto cleanup; + if ((!old && !value) || (old && value && strcmp(old, value) == 0)) + goto cleanup; + + if (!value) + error = git_config_delete_entry(config, key.ptr); + else + error = git_config_set_string(config, key.ptr, value); +cleanup: + git_buf_free(&key); return error; } -int git_submodule_lookup( - git_submodule **sm_ptr, /* NULL allowed if user only wants to test */ - git_repository *repo, - const char *name) /* trailing slash is allowed */ +static int submodule_index_status(unsigned int *status, git_submodule *sm) { - khiter_t pos; + const git_oid *head_oid = git_submodule_head_id(sm); + const git_oid *index_oid = git_submodule_index_id(sm); - if (load_submodule_config(repo) < 0) - return -1; + if (!head_oid) { + if (index_oid) + *status |= GIT_SUBMODULE_STATUS_INDEX_ADDED; + } + else if (!index_oid) + *status |= GIT_SUBMODULE_STATUS_INDEX_DELETED; + else if (!git_oid_equal(head_oid, index_oid)) + *status |= GIT_SUBMODULE_STATUS_INDEX_MODIFIED; - pos = git_strmap_lookup_index(repo->submodules, name); - if (!git_strmap_valid_index(repo->submodules, pos)) - return GIT_ENOTFOUND; + return 0; +} - if (sm_ptr) - *sm_ptr = git_strmap_value_at(repo->submodules, pos); +static int submodule_wd_status(unsigned int *status, git_submodule *sm) +{ + 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; + } - return 0; + index_oid = git_submodule_index_id(sm); + wd_oid = git_submodule_wd_id(sm); + + if (!index_oid) { + if (wd_oid) + *status |= GIT_SUBMODULE_STATUS_WD_ADDED; + } + else if (!wd_oid) { + if ((sm->flags & GIT_SUBMODULE_STATUS__WD_SCANNED) != 0 && + (sm->flags & GIT_SUBMODULE_STATUS_IN_WD) == 0) + *status |= GIT_SUBMODULE_STATUS_WD_UNINITIALIZED; + else + *status |= GIT_SUBMODULE_STATUS_WD_DELETED; + } + 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 (sm->ignore == GIT_SUBMODULE_IGNORE_NONE) + opt.flags |= GIT_DIFF_INCLUDE_UNTRACKED; + + error = git_diff_tree_to_index(&diff, sm_repo, sm_head, NULL, &opt); + + if (!error) { + 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; + + if (git_diff_num_deltas(diff) != untracked) + *status |= GIT_SUBMODULE_STATUS_WD_WD_MODIFIED; + + git_diff_list_free(diff); + diff = NULL; + } + + git_repository_free(sm_repo); + } + + return error; } diff --git a/src/submodule.h b/src/submodule.h new file mode 100644 index 000000000..ba8e2518e --- /dev/null +++ b/src/submodule.h @@ -0,0 +1,102 @@ +/* + * 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_submodule_h__ +#define INCLUDE_submodule_h__ + +/* Notes: + * + * Submodule information can be in four places: the index, the config files + * (both .git/config and .gitmodules), the HEAD tree, and the working + * directory. + * + * In the index: + * - submodule is found by path + * - may be missing, present, or of the wrong type + * - will have an oid if present + * + * In the HEAD tree: + * - submodule is found by path + * - may be missing, present, or of the wrong type + * - will have an oid if present + * + * In the config files: + * - submodule is found by submodule "name" which is usually the path + * - may be missing or present + * - will have a name, path, url, and other properties + * + * In the working directory: + * - submodule is found by path + * - may be missing, an empty directory, a checked out directory, + * or of the wrong type + * - if checked out, will have a HEAD oid + * - if checked out, will have git history that can be used to compare oids + * - if checked out, may have modified files and/or untracked files + */ + +/** + * Description of submodule + * + * This record describes a submodule found in a repository. There should be + * 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 + * - `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. + * - `ignore` is a git_submodule_ignore_t value - see gitmodules(5) ignore. + * - `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. + * + * 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. + */ +struct git_submodule { + git_repository *owner; + char *name; + char *path; /* important: may point to same string data as "name" */ + 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; +}; + +/* Additional flags on top of public GIT_SUBMODULE_STATUS values */ +enum { + GIT_SUBMODULE_STATUS__WD_SCANNED = (1u << 20), + GIT_SUBMODULE_STATUS__HEAD_OID_VALID = (1u << 21), + GIT_SUBMODULE_STATUS__INDEX_OID_VALID = (1u << 22), + GIT_SUBMODULE_STATUS__WD_OID_VALID = (1u << 23), + GIT_SUBMODULE_STATUS__HEAD_NOT_SUBMODULE = (1u << 24), + GIT_SUBMODULE_STATUS__INDEX_NOT_SUBMODULE = (1u << 25), + GIT_SUBMODULE_STATUS__WD_NOT_SUBMODULE = (1u << 26), + GIT_SUBMODULE_STATUS__INDEX_MULTIPLE_ENTRIES = (1u << 27), +}; + +#define GIT_SUBMODULE_STATUS__CLEAR_INTERNAL(S) \ + ((S) & ~(0xFFFFFFFFu << 20)) + +#endif @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -22,41 +22,41 @@ void git_tag__free(git_tag *tag) git__free(tag); } -const git_oid *git_tag_id(git_tag *c) +const git_oid *git_tag_id(const git_tag *c) { - return git_object_id((git_object *)c); + return git_object_id((const git_object *)c); } -int git_tag_target(git_object **target, git_tag *t) +int git_tag_target(git_object **target, const git_tag *t) { assert(t); return git_object_lookup(target, t->object.repo, &t->target, t->type); } -const git_oid *git_tag_target_oid(git_tag *t) +const git_oid *git_tag_target_id(const git_tag *t) { assert(t); return &t->target; } -git_otype git_tag_type(git_tag *t) +git_otype git_tag_target_type(const git_tag *t) { assert(t); return t->type; } -const char *git_tag_name(git_tag *t) +const char *git_tag_name(const git_tag *t) { assert(t); return t->tag_name; } -const git_signature *git_tag_tagger(git_tag *t) +const git_signature *git_tag_tagger(const git_tag *t) { return t->tagger; } -const char *git_tag_message(git_tag *t) +const char *git_tag_message(const git_tag *t) { assert(t); return t->message; @@ -131,7 +131,7 @@ int git_tag__parse_buffer(git_tag *tag, const char *buffer, size_t length) buffer = search + 1; tag->tagger = NULL; - if (*buffer != '\n') { + if (buffer < buffer_end && *buffer != '\n') { tag->tagger = git__malloc(sizeof(git_signature)); GITERR_CHECK_ALLOC(tag->tagger); @@ -139,16 +139,19 @@ int git_tag__parse_buffer(git_tag *tag, const char *buffer, size_t length) return -1; } - if( *buffer != '\n' ) - return tag_error("No new line before message"); + tag->message = NULL; + if (buffer < buffer_end) { + if( *buffer != '\n' ) + return tag_error("No new line before message"); - text_len = buffer_end - ++buffer; + text_len = buffer_end - ++buffer; - tag->message = git__malloc(text_len + 1); - GITERR_CHECK_ALLOC(tag->message); + tag->message = git__malloc(text_len + 1); + GITERR_CHECK_ALLOC(tag->message); - memcpy(tag->message, buffer, text_len); - tag->message[text_len] = '\0'; + memcpy(tag->message, buffer, text_len); + tag->message[text_len] = '\0'; + } return 0; } @@ -185,7 +188,7 @@ static int retrieve_tag_reference_oid( if (git_buf_joinpath(ref_name_out, GIT_REFS_TAGS_DIR, tag_name) < 0) return -1; - return git_reference_name_to_oid(oid, repo, ref_name_out->ptr); + return git_reference_name_to_id(oid, repo, ref_name_out->ptr); } static int write_tag_annotation( @@ -196,7 +199,7 @@ static int write_tag_annotation( const git_signature *tagger, const char *message) { - git_buf tag = GIT_BUF_INIT, cleaned_message = GIT_BUF_INIT; + git_buf tag = GIT_BUF_INIT; git_odb *odb; git_oid__writebuf(&tag, "object ", git_object_id(target)); @@ -205,15 +208,9 @@ static int write_tag_annotation( git_signature__writebuf(&tag, "tagger ", tagger); git_buf_putc(&tag, '\n'); - /* Remove comments by default */ - if (git_message_prettify(&cleaned_message, message, 1) < 0) - goto on_error; - - if (git_buf_puts(&tag, git_buf_cstr(&cleaned_message)) < 0) + if (git_buf_puts(&tag, message) < 0) goto on_error; - git_buf_free(&cleaned_message); - if (git_repository_odb__weakptr(&odb, repo) < 0) goto on_error; @@ -225,7 +222,6 @@ static int write_tag_annotation( on_error: git_buf_free(&tag); - git_buf_free(&cleaned_message); giterr_set(GITERR_OBJECT, "Failed to create tag annotation."); return -1; } @@ -255,7 +251,7 @@ static int git_tag_create__internal( error = retrieve_tag_reference_oid(oid, &ref_name, repo, tag_name); if (error < 0 && error != GIT_ENOTFOUND) - return -1; + goto cleanup; /** Ensure the tag name doesn't conflict with an already existing * reference unless overwriting has explictly been requested **/ @@ -271,8 +267,9 @@ static int git_tag_create__internal( } else git_oid_cpy(oid, git_object_id(target)); - error = git_reference_create_oid(&new_ref, repo, ref_name.ptr, oid, allow_ref_overwrite); + error = git_reference_create(&new_ref, repo, ref_name.ptr, oid, allow_ref_overwrite); +cleanup: git_reference_free(new_ref); git_buf_free(&ref_name); return error; @@ -362,7 +359,7 @@ int git_tag_create_frombuffer(git_oid *oid, git_repository *repo, const char *bu return -1; } - error = git_reference_create_oid(&new_ref, repo, ref_name.ptr, oid, allow_ref_overwrite); + error = git_reference_create(&new_ref, repo, ref_name.ptr, oid, allow_ref_overwrite); git_reference_free(new_ref); git_buf_free(&ref_name); @@ -379,18 +376,21 @@ on_error: int git_tag_delete(git_repository *repo, const char *tag_name) { - int error; git_reference *tag_ref; git_buf ref_name = GIT_BUF_INIT; + int error; error = retrieve_tag_reference(&tag_ref, &ref_name, repo, tag_name); git_buf_free(&ref_name); if (error < 0) - return -1; + return error; - return git_reference_delete(tag_ref); + 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) @@ -400,22 +400,52 @@ int git_tag__parse(git_tag *tag, git_odb_object *obj) } typedef struct { - git_vector *taglist; - const char *pattern; + git_repository *repo; + git_tag_foreach_cb cb; + void *cb_data; +} tag_cb_data; + +static int tags_cb(const char *ref, void *data) +{ + git_oid oid; + tag_cb_data *d = (tag_cb_data *)data; + + if (git__prefixcmp(ref, GIT_REFS_TAGS_DIR) != 0) + return 0; /* no tag */ + + if (git_reference_name_to_id(&oid, d->repo, ref) < 0) + return -1; + + return d->cb(ref, &oid, d->cb_data); +} + +int git_tag_foreach(git_repository *repo, git_tag_foreach_cb cb, void *cb_data) +{ + tag_cb_data data; + + assert(repo && cb); + + data.cb = cb; + data.cb_data = cb_data; + data.repo = repo; + + return git_reference_foreach(repo, GIT_REF_OID, &tags_cb, &data); +} + +typedef struct { + git_vector *taglist; + const char *pattern; } tag_filter_data; #define GIT_REFS_TAGS_DIR_LEN strlen(GIT_REFS_TAGS_DIR) -static int tag_list_cb(const char *tag_name, void *payload) +static int tag_list_cb(const char *tag_name, git_oid *oid, void *data) { - tag_filter_data *filter; + tag_filter_data *filter = (tag_filter_data *)data; + GIT_UNUSED(oid); - if (git__prefixcmp(tag_name, GIT_REFS_TAGS_DIR) != 0) - return 0; - - filter = (tag_filter_data *)payload; if (!*filter->pattern || p_fnmatch(filter->pattern, tag_name + GIT_REFS_TAGS_DIR_LEN, 0) == 0) - return git_vector_insert(filter->taglist, git__strdup(tag_name)); + return git_vector_insert(filter->taglist, git__strdup(tag_name + GIT_REFS_TAGS_DIR_LEN)); return 0; } @@ -434,7 +464,7 @@ int git_tag_list_match(git_strarray *tag_names, const char *pattern, git_reposit filter.taglist = &taglist; filter.pattern = pattern; - error = git_reference_foreach(repo, GIT_REF_OID|GIT_REF_PACKED, &tag_list_cb, (void *)&filter); + error = git_tag_foreach(repo, &tag_list_cb, (void *)&filter); if (error < 0) { git_vector_free(&taglist); return -1; @@ -450,22 +480,7 @@ int git_tag_list(git_strarray *tag_names, git_repository *repo) return git_tag_list_match(tag_names, "", repo); } -int git_tag_peel(git_object **tag_target, git_tag *tag) +int git_tag_peel(git_object **tag_target, const git_tag *tag) { - int error; - git_object *target; - - assert(tag_target && tag); - - if (git_tag_target(&target, tag) < 0) - return -1; - - if (git_object_type(target) == GIT_OBJ_TAG) { - error = git_tag_peel(tag_target, (git_tag *)target); - git_object_free(target); - return error; - } - - *tag_target = target; - return 0; + return git_object_peel(tag_target, (const git_object *)tag, GIT_OBJ_ANY); } @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. diff --git a/src/thread-utils.c b/src/thread-utils.c index 0ca01ef82..c3baf411a 100644 --- a/src/thread-utils.c +++ b/src/thread-utils.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. diff --git a/src/thread-utils.h b/src/thread-utils.h index a309e93d1..2ca290adf 100644 --- a/src/thread-utils.h +++ b/src/thread-utils.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -38,13 +38,13 @@ GIT_INLINE(void) git_atomic_set(git_atomic *a, int val) #define git_mutex_unlock(a) pthread_mutex_unlock(a) #define git_mutex_free(a) pthread_mutex_destroy(a) -/* Pthreads condition vars -- disabled by now */ -#define git_cond unsigned int //pthread_cond_t -#define git_cond_init(c, a) (void)0 //pthread_cond_init(c, a) -#define git_cond_free(c) (void)0 //pthread_cond_destroy(c) -#define git_cond_wait(c, l) (void)0 //pthread_cond_wait(c, l) -#define git_cond_signal(c) (void)0 //pthread_cond_signal(c) -#define git_cond_broadcast(c) (void)0 //pthread_cond_broadcast(c) +/* Pthreads condition vars */ +#define git_cond pthread_cond_t +#define git_cond_init(c) pthread_cond_init(c, NULL) +#define git_cond_free(c) pthread_cond_destroy(c) +#define git_cond_wait(c, l) pthread_cond_wait(c, l) +#define git_cond_signal(c) pthread_cond_signal(c) +#define git_cond_broadcast(c) pthread_cond_broadcast(c) GIT_INLINE(int) git_atomic_inc(git_atomic *a) { @@ -79,7 +79,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_lock(a) (void)0 +#define git_mutex_lock(a) 0 #define git_mutex_unlock(a) (void)0 #define git_mutex_free(a) (void)0 diff --git a/src/trace.c b/src/trace.c new file mode 100644 index 000000000..159ac91cc --- /dev/null +++ b/src/trace.c @@ -0,0 +1,39 @@ +/* + * 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 "buffer.h" +#include "common.h" +#include "global.h" +#include "trace.h" +#include "git2/trace.h" + +#ifdef GIT_TRACE + +struct git_trace_data git_trace__data = {0}; + +#endif + +int git_trace_set(git_trace_level_t level, git_trace_callback callback) +{ +#ifdef GIT_TRACE + assert(level == 0 || callback != NULL); + + git_trace__data.level = level; + git_trace__data.callback = callback; + GIT_MEMORY_BARRIER; + + return 0; +#else + GIT_UNUSED(level); + GIT_UNUSED(callback); + + giterr_set(GITERR_INVALID, + "This version of libgit2 was not built with tracing."); + return -1; +#endif +} + diff --git a/src/trace.h b/src/trace.h new file mode 100644 index 000000000..f4bdff88a --- /dev/null +++ b/src/trace.h @@ -0,0 +1,56 @@ +/* + * 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_trace_h__ +#define INCLUDE_trace_h__ + +#include <stdarg.h> + +#include <git2/trace.h> +#include "buffer.h" + +#ifdef GIT_TRACE + +struct git_trace_data { + git_trace_level_t level; + git_trace_callback callback; +}; + +extern struct git_trace_data git_trace__data; + +GIT_INLINE(void) git_trace__write_fmt( + git_trace_level_t level, + const char *fmt, ...) +{ + 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); +} + +#define git_trace_level() (git_trace__data.level) +#define git_trace(l, ...) { \ + if (git_trace__data.level >= l && \ + git_trace__data.callback != NULL) { \ + git_trace__write_fmt(l, __VA_ARGS__); \ + } \ + } + +#else + +#define git_trace_level() ((void)0) +#define git_trace(lvl, ...) ((void)0) + +#endif + +#endif diff --git a/src/transport.c b/src/transport.c index 5b2cd7ea4..adb6d5355 100644 --- a/src/transport.c +++ b/src/transport.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -8,76 +8,116 @@ #include "git2/types.h" #include "git2/remote.h" #include "git2/net.h" -#include "transport.h" +#include "git2/transport.h" #include "path.h" -static struct { +typedef struct transport_definition { char *prefix; + unsigned priority; git_transport_cb fn; -} transports[] = { - {"git://", git_transport_git}, - {"http://", git_transport_http}, - {"https://", git_transport_dummy}, - {"file://", git_transport_local}, - {"git+ssh://", git_transport_dummy}, - {"ssh+git://", git_transport_dummy}, - {NULL, 0} + 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 }; + +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}, + {NULL, 0, 0} }; #define GIT_TRANSPORT_COUNT (sizeof(transports)/sizeof(transports[0])) - 1 -static git_transport_cb transport_find_fn(const char *url) +static int transport_find_fn(const char *url, git_transport_cb *callback, void **param) { size_t i = 0; + unsigned priority = 0; + transport_definition *definition = NULL, *definition_iter; // First, check to see if it's an obvious URL, which a URL scheme for (i = 0; i < GIT_TRANSPORT_COUNT; ++i) { - if (!strncasecmp(url, transports[i].prefix, strlen(transports[i].prefix))) - return transports[i].fn; - } + definition_iter = &transports[i]; + + if (strncasecmp(url, definition_iter->prefix, strlen(definition_iter->prefix))) + continue; - /* still here? Check to see if the path points to a file on the local file system */ - if ((git_path_exists(url) == 0) && git_path_isdir(url)) - return &git_transport_local; + if (definition_iter->priority > priority) + definition = definition_iter; + } - /* It could be a SSH remote path. Check to see if there's a : */ - if (strrchr(url, ':')) - return &git_transport_dummy; /* SSH is an unsupported transport mechanism in this version of libgit2 */ +#ifdef GIT_WIN32 + /* On Windows, it might not be possible to discern between absolute local + * and ssh paths - first check if this is a valid local path that points + * to a directory and if so assume local path, else assume SSH */ + + /* 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)) + definition = &local_transport_definition; + + /* 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; +#else + /* For other systems, perform the SSH check first, to avoid going to the + * filesystem if it is not necessary */ + + /* 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; + + /* 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)) + definition = &local_transport_definition; +#endif + + if (!definition) + return -1; - return NULL; + *callback = definition->fn; + *param = definition->param; + + return 0; } /************** * Public API * **************/ -int git_transport_dummy(git_transport **transport) +int git_transport_dummy(git_transport **transport, git_remote *owner, void *param) { GIT_UNUSED(transport); + GIT_UNUSED(owner); + GIT_UNUSED(param); giterr_set(GITERR_NET, "This transport isn't implemented. Sorry"); return -1; } -int git_transport_new(git_transport **out, const char *url) +int git_transport_new(git_transport **out, git_remote *owner, const char *url) { git_transport_cb fn; git_transport *transport; + void *param; int error; - fn = transport_find_fn(url); - - if (fn == NULL) { + if (transport_find_fn(url, &fn, ¶m) < 0) { giterr_set(GITERR_NET, "Unsupported URL protocol"); return -1; } - error = fn(&transport); + error = fn(&transport, owner, param); if (error < 0) return error; - transport->url = git__strdup(url); - GITERR_CHECK_ALLOC(transport->url); - *out = transport; return 0; @@ -86,12 +126,19 @@ int git_transport_new(git_transport **out, const char *url) /* from remote.h */ int git_remote_valid_url(const char *url) { - return transport_find_fn(url) != NULL; + git_transport_cb fn; + void *param; + + return !transport_find_fn(url, &fn, ¶m); } int git_remote_supported_url(const char* url) { - git_transport_cb transport_fn = transport_find_fn(url); + git_transport_cb fn; + void *param; + + if (transport_find_fn(url, &fn, ¶m) < 0) + return 0; - return ((transport_fn != NULL) && (transport_fn != &git_transport_dummy)); + return fn != &git_transport_dummy; } diff --git a/src/transport.h b/src/transport.h deleted file mode 100644 index 125df2745..000000000 --- a/src/transport.h +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2009-2012 the libgit2 contributors - * - * This file is part of libgit2, distributed under the GNU GPL v2 with - * a Linking Exception. For full terms see the included COPYING file. - */ -#ifndef INCLUDE_transport_h__ -#define INCLUDE_transport_h__ - -#include "git2/net.h" -#include "git2/indexer.h" -#include "vector.h" - -#define GIT_CAP_OFS_DELTA "ofs-delta" - -typedef struct git_transport_caps { - int common:1, - ofs_delta:1; -} git_transport_caps; - -/* - * A day in the life of a network operation - * ======================================== - * - * The library gets told to ls-remote/push/fetch on/to/from some - * remote. We look at the URL of the remote and fill the function - * table with whatever is appropriate (the remote may be git over git, - * ssh or http(s). It may even be an hg or svn repository, the library - * at this level doesn't care, it just calls the helpers. - * - * The first call is to ->connect() which connects to the remote, - * making use of the direction if necessary. This function must also - * store the remote heads and any other information it needs. - * - * The next useful step is to call ->ls() to get the list of - * references available to the remote. These references may have been - * collected on connect, or we may build them now. For ls-remote, - * nothing else is needed other than closing the connection. - * Otherwise, the higher leves decide which objects we want to - * have. ->send_have() is used to tell the other end what we have. If - * we do need to download a pack, ->download_pack() is called. - * - * When we're done, we call ->close() to close the - * connection. ->free() takes care of freeing all the resources. - */ - -struct git_transport { - /** - * Where the repo lives - */ - char *url; - /** - * Whether we want to push or fetch - */ - int direction : 1, /* 0 fetch, 1 push */ - connected : 1; - /** - * Connect and store the remote heads - */ - int (*connect)(struct git_transport *transport, int dir); - /** - * Give a list of references, useful for ls-remote - */ - int (*ls)(struct git_transport *transport, git_headlist_cb list_cb, void *opaque); - /** - * Push the changes over - */ - int (*push)(struct git_transport *transport); - /** - * Negotiate the minimal amount of objects that need to be - * retrieved - */ - int (*negotiate_fetch)(struct git_transport *transport, git_repository *repo, const git_vector *wants); - /** - * Download the packfile - */ - int (*download_pack)(struct git_transport *transport, git_repository *repo, git_off_t *bytes, git_indexer_stats *stats); - /** - * Fetch the changes - */ - int (*fetch)(struct git_transport *transport); - /** - * Close the connection - */ - int (*close)(struct git_transport *transport); - /** - * Free the associated resources - */ - void (*free)(struct git_transport *transport); -}; - - -int git_transport_new(struct git_transport **transport, const char *url); -int git_transport_local(struct git_transport **transport); -int git_transport_git(struct git_transport **transport); -int git_transport_http(struct git_transport **transport); -int git_transport_dummy(struct git_transport **transport); - -/** - Returns true if the passed URL is valid (a URL with a Git supported scheme, - or pointing to an existing path) -*/ -int git_transport_valid_url(const char *url); - -typedef struct git_transport git_transport; -typedef int (*git_transport_cb)(git_transport **transport); - -#endif diff --git a/src/transports/cred.c b/src/transports/cred.c new file mode 100644 index 000000000..ecb026062 --- /dev/null +++ b/src/transports/cred.c @@ -0,0 +1,60 @@ +/* + * 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 "smart.h" +#include "git2/cred_helpers.h" + +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)); + + git__free(c); +} + +int git_cred_userpass_plaintext_new( + git_cred **cred, + const char *username, + const char *password) +{ + git_cred_userpass_plaintext *c; + + if (!cred) + return -1; + + c = git__malloc(sizeof(git_cred_userpass_plaintext)); + GITERR_CHECK_ALLOC(c); + + c->parent.credtype = GIT_CREDTYPE_USERPASS_PLAINTEXT; + c->parent.free = plaintext_free; + c->username = git__strdup(username); + + if (!c->username) { + git__free(c); + return -1; + } + + c->password = git__strdup(password); + + if (!c->password) { + git__free(c->username); + git__free(c); + return -1; + } + + *cred = &c->parent; + return 0; +} diff --git a/src/transports/cred_helpers.c b/src/transports/cred_helpers.c new file mode 100644 index 000000000..d420e3e3c --- /dev/null +++ b/src/transports/cred_helpers.c @@ -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. + */ + +#include "common.h" +#include "git2/cred_helpers.h" + +int git_cred_userpass( + git_cred **cred, + const char *url, + const char *user_from_url, + unsigned int allowed_types, + void *payload) +{ + git_cred_userpass_payload *userpass = (git_cred_userpass_payload*)payload; + const char *effective_username = NULL; + + GIT_UNUSED(url); + + if (!userpass || !userpass->password) return -1; + + /* Username resolution: a username can be passed with the URL, the + * credentials payload, or both. Here's what we do. Note that if we get + * this far, we know that any password the url may contain has already + * failed at least once, so we ignore it. + * + * | Payload | URL | Used | + * +-------------+----------+-----------+ + * | yes | no | payload | + * | yes | yes | payload | + * | no | yes | url | + * | no | no | FAIL | + */ + if (userpass->username) + effective_username = userpass->username; + else if (user_from_url) + effective_username = user_from_url; + else + return -1; + + if ((GIT_CREDTYPE_USERPASS_PLAINTEXT & allowed_types) == 0 || + git_cred_userpass_plaintext_new(cred, effective_username, userpass->password) < 0) + return -1; + + return 0; +} diff --git a/src/transports/git.c b/src/transports/git.c index 5baa810f0..3a0b86345 100644 --- a/src/transports/git.c +++ b/src/transports/git.c @@ -1,50 +1,42 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ -#include "git2/net.h" -#include "git2/common.h" -#include "git2/types.h" -#include "git2/errors.h" -#include "git2/net.h" -#include "git2/revwalk.h" - -#include "vector.h" -#include "transport.h" -#include "pkt.h" -#include "common.h" +#include "git2.h" +#include "buffer.h" #include "netops.h" -#include "filebuf.h" -#include "repository.h" -#include "fetch.h" -#include "protocol.h" + +#define OWNING_SUBTRANSPORT(s) ((git_subtransport *)(s)->parent.subtransport) + +static const char prefix_git[] = "git://"; +static const char cmd_uploadpack[] = "git-upload-pack"; +static const char cmd_receivepack[] = "git-receive-pack"; typedef struct { - git_transport parent; - git_protocol proto; - GIT_SOCKET socket; - git_vector refs; - git_remote_head **heads; - git_transport_caps caps; - char buff[1024]; - gitno_buffer buf; -#ifdef GIT_WIN32 - WSADATA wsd; -#endif -} transport_git; + git_smart_subtransport_stream parent; + gitno_socket socket; + const char *cmd; + char *url; + unsigned sent_command : 1; +} git_stream; + +typedef struct { + git_smart_subtransport parent; + git_transport *owner; + git_stream *current_stream; +} git_subtransport; /* - * Create a git procol request. + * Create a git protocol request. * * For example: 0035git-upload-pack /libgit2/libgit2\0host=github.com\0 */ static int gen_proto(git_buf *request, const char *cmd, const char *url) { char *delim, *repo; - char default_command[] = "git-upload-pack"; char host[] = "host="; size_t len; @@ -60,9 +52,6 @@ static int gen_proto(git_buf *request, const char *cmd, const char *url) if (delim == NULL) delim = strchr(url, '/'); - if (cmd == NULL) - cmd = default_command; - len = 4 + strlen(cmd) + 1 + strlen(repo) + 1 + strlen(host) + (delim - url) + 1; git_buf_grow(request, len); @@ -77,402 +66,287 @@ static int gen_proto(git_buf *request, const char *cmd, const char *url) return 0; } -static int send_request(GIT_SOCKET s, const char *cmd, const char *url) +static int send_command(git_stream *s) { int error; git_buf request = GIT_BUF_INIT; - error = gen_proto(&request, cmd, url); + error = gen_proto(&request, s->cmd, s->url); if (error < 0) goto cleanup; - error = gitno_send(s, request.ptr, request.size, 0); + /* It looks like negative values are errors here, and positive values + * are the number of bytes sent. */ + error = gitno_send(&s->socket, request.ptr, request.size, 0); + + if (error >= 0) + s->sent_command = 1; cleanup: git_buf_free(&request); return error; } -/* - * Parse the URL and connect to a server, storing the socket in - * out. For convenience this also takes care of asking for the remote - * refs - */ -static int do_connect(transport_git *t, const char *url) +static int git_stream_read( + git_smart_subtransport_stream *stream, + char *buffer, + size_t buf_size, + size_t *bytes_read) { - char *host, *port; - const char prefix[] = "git://"; - int error; - - t->socket = INVALID_SOCKET; + git_stream *s = (git_stream *)stream; + gitno_buffer buf; - if (!git__prefixcmp(url, prefix)) - url += strlen(prefix); + *bytes_read = 0; - if (gitno_extract_host_and_port(&host, &port, url, GIT_DEFAULT_PORT) < 0) + if (!s->sent_command && send_command(s) < 0) return -1; - if ((error = gitno_connect(&t->socket, host, port)) == 0) { - error = send_request(t->socket, NULL, url); - } - - git__free(host); - git__free(port); - - if (error < 0 && t->socket != INVALID_SOCKET) { - gitno_close(t->socket); - t->socket = INVALID_SOCKET; - } + gitno_buffer_setup(&s->socket, &buf, buffer, buf_size); - if (t->socket == INVALID_SOCKET) { - giterr_set(GITERR_NET, "Failed to connect to the host"); + if (gitno_recv(&buf) < 0) return -1; - } + + *bytes_read = buf.offset; return 0; } -/* - * Read from the socket and store the references in the vector - */ -static int store_refs(transport_git *t) +static int git_stream_write( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) { - gitno_buffer *buf = &t->buf; - int ret = 0; - - while (1) { - if ((ret = gitno_recv(buf)) < 0) - return -1; - if (ret == 0) /* Orderly shutdown, so exit */ - return 0; - - ret = git_protocol_store_refs(&t->proto, buf->data, buf->offset); - if (ret == GIT_EBUFS) { - gitno_consume_n(buf, buf->len); - continue; - } - - if (ret < 0) - return ret; - - gitno_consume_n(buf, buf->offset); - - if (t->proto.flush) { /* No more refs */ - t->proto.flush = 0; - return 0; - } - } + git_stream *s = (git_stream *)stream; + + if (!s->sent_command && send_command(s) < 0) + return -1; + + return gitno_send(&s->socket, buffer, len, 0); } -static int detect_caps(transport_git *t) +static void git_stream_free(git_smart_subtransport_stream *stream) { - git_vector *refs = &t->refs; - git_pkt_ref *pkt; - git_transport_caps *caps = &t->caps; - const char *ptr; - - pkt = git_vector_get(refs, 0); - /* No refs or capabilites, odd but not a problem */ - if (pkt == NULL || pkt->capabilities == NULL) - return 0; + git_stream *s = (git_stream *)stream; + git_subtransport *t = OWNING_SUBTRANSPORT(s); + int ret; - ptr = pkt->capabilities; - while (ptr != NULL && *ptr != '\0') { - if (*ptr == ' ') - ptr++; + GIT_UNUSED(ret); - if(!git__prefixcmp(ptr, GIT_CAP_OFS_DELTA)) { - caps->common = caps->ofs_delta = 1; - ptr += strlen(GIT_CAP_OFS_DELTA); - continue; - } + t->current_stream = NULL; - /* We don't know this capability, so skip it */ - ptr = strchr(ptr, ' '); + if (s->socket.socket) { + ret = gitno_close(&s->socket); + assert(!ret); } - return 0; + git__free(s->url); + git__free(s); } -/* - * Since this is a network connection, we need to parse and store the - * pkt-lines at this stage and keep them there. - */ -static int git_connect(git_transport *transport, int direction) +static int git_stream_alloc( + git_subtransport *t, + const char *url, + const char *cmd, + git_smart_subtransport_stream **stream) { - transport_git *t = (transport_git *) transport; - - if (direction == GIT_DIR_PUSH) { - giterr_set(GITERR_NET, "Pushing over git:// is not supported"); - return -1; - } + git_stream *s; - t->parent.direction = direction; - if (git_vector_init(&t->refs, 16, NULL) < 0) + if (!stream) return -1; - /* Connect and ask for the refs */ - if (do_connect(t, transport->url) < 0) - goto cleanup; + s = git__calloc(sizeof(git_stream), 1); + GITERR_CHECK_ALLOC(s); - gitno_buffer_setup(&t->buf, t->buff, sizeof(t->buff), t->socket); + s->parent.subtransport = &t->parent; + s->parent.read = git_stream_read; + s->parent.write = git_stream_write; + s->parent.free = git_stream_free; - t->parent.connected = 1; - if (store_refs(t) < 0) - goto cleanup; + s->cmd = cmd; + s->url = git__strdup(url); - if (detect_caps(t) < 0) - goto cleanup; + if (!s->url) { + git__free(s); + return -1; + } + *stream = &s->parent; return 0; -cleanup: - git_vector_free(&t->refs); - return -1; } -static int git_ls(git_transport *transport, git_headlist_cb list_cb, void *opaque) +static int _git_uploadpack_ls( + git_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) { - transport_git *t = (transport_git *) transport; - git_vector *refs = &t->refs; - unsigned int i; - git_pkt *p = NULL; + char *host, *port, *user=NULL, *pass=NULL; + git_stream *s; + + *stream = NULL; - git_vector_foreach(refs, i, p) { - git_pkt_ref *pkt = NULL; + if (!git__prefixcmp(url, prefix_git)) + url += strlen(prefix_git); - if (p->type != GIT_PKT_REF) - continue; + if (git_stream_alloc(t, url, cmd_uploadpack, stream) < 0) + return -1; - pkt = (git_pkt_ref *)p; + s = (git_stream *)*stream; - if (list_cb(&pkt->head, opaque) < 0) { - giterr_set(GITERR_NET, "User callback returned error"); - return -1; - } - } + if (gitno_extract_url_parts(&host, &port, &user, &pass, url, GIT_DEFAULT_PORT) < 0) + goto on_error; + if (gitno_connect(&s->socket, host, port, 0) < 0) + goto on_error; + + t->current_stream = s; + git__free(host); + git__free(port); + git__free(user); + git__free(pass); return 0; + +on_error: + if (*stream) + git_stream_free(*stream); + + git__free(host); + git__free(port); + return -1; } -/* Wait until we get an ack from the */ -static int recv_pkt(gitno_buffer *buf) +static int _git_uploadpack( + git_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) { - const char *ptr = buf->data, *line_end; - git_pkt *pkt; - int pkt_type, error; - - do { - /* Wait for max. 1 second */ - if ((error = gitno_select_in(buf, 1, 0)) < 0) { - return -1; - } else if (error == 0) { - /* - * Some servers don't respond immediately, so if this - * happens, we keep sending information until it - * answers. Pretend we received a NAK to convince higher - * layers to do so. - */ - return GIT_PKT_NAK; - } - - if ((error = gitno_recv(buf)) < 0) - return -1; - - error = git_pkt_parse_line(&pkt, ptr, &line_end, buf->offset); - if (error == GIT_EBUFS) - continue; - if (error < 0) - return -1; - } while (error); - - gitno_consume(buf, line_end); - pkt_type = pkt->type; - git__free(pkt); - - return pkt_type; + 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 git_negotiate_fetch(git_transport *transport, git_repository *repo, const git_vector *wants) +static int _git_receivepack_ls( + git_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) { - transport_git *t = (transport_git *) transport; - git_revwalk *walk; - git_oid oid; - int error; - unsigned int i; - git_buf data = GIT_BUF_INIT; - gitno_buffer *buf = &t->buf; + char *host, *port, *user=NULL, *pass=NULL; + git_stream *s; - if (git_pkt_buffer_wants(wants, &t->caps, &data) < 0) - return -1; + *stream = NULL; - if (git_fetch_setup_walk(&walk, repo) < 0) - goto on_error; + if (!git__prefixcmp(url, prefix_git)) + url += strlen(prefix_git); - if (gitno_send(t->socket, data.ptr, data.size, 0) < 0) - goto on_error; + if (git_stream_alloc(t, url, cmd_receivepack, stream) < 0) + return -1; - git_buf_clear(&data); - /* - * We don't support any kind of ACK extensions, so the negotiation - * boils down to sending what we have and listening for an ACK - * every once in a while. - */ - i = 0; - while ((error = git_revwalk_next(&oid, walk)) == 0) { - git_pkt_buffer_have(&oid, &data); - i++; - if (i % 20 == 0) { - int pkt_type; - - git_pkt_buffer_flush(&data); - if (git_buf_oom(&data)) - goto on_error; - - if (gitno_send(t->socket, data.ptr, data.size, 0) < 0) - goto on_error; - - pkt_type = recv_pkt(buf); - - if (pkt_type == GIT_PKT_ACK) { - break; - } else if (pkt_type == GIT_PKT_NAK) { - continue; - } else { - giterr_set(GITERR_NET, "Unexpected pkt type"); - goto on_error; - } - - } - } - if (error < 0 && error != GIT_REVWALKOVER) + s = (git_stream *)*stream; + + if (gitno_extract_url_parts(&host, &port, &user, &pass, url, GIT_DEFAULT_PORT) < 0) goto on_error; - /* Tell the other end that we're done negotiating */ - git_buf_clear(&data); - git_pkt_buffer_flush(&data); - git_pkt_buffer_done(&data); - if (gitno_send(t->socket, data.ptr, data.size, 0) < 0) + if (gitno_connect(&s->socket, host, port, 0) < 0) goto on_error; - git_buf_free(&data); - git_revwalk_free(walk); + t->current_stream = s; + git__free(host); + git__free(port); + git__free(user); + git__free(pass); return 0; on_error: - git_buf_free(&data); - git_revwalk_free(walk); + if (*stream) + git_stream_free(*stream); + + git__free(host); + git__free(port); return -1; } -static int git_download_pack(git_transport *transport, git_repository *repo, git_off_t *bytes, git_indexer_stats *stats) +static int _git_receivepack( + git_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) { - transport_git *t = (transport_git *) transport; - int error = 0, read_bytes; - gitno_buffer *buf = &t->buf; - git_pkt *pkt; - const char *line_end, *ptr; - - /* - * For now, we ignore everything and wait for the pack - */ - do { - ptr = buf->data; - /* Whilst we're searching for the pack */ - while (1) { - if (buf->offset == 0) { - break; - } - - error = git_pkt_parse_line(&pkt, ptr, &line_end, buf->offset); - if (error == GIT_EBUFS) - break; - - if (error < 0) - return error; - - if (pkt->type == GIT_PKT_PACK) { - git__free(pkt); - return git_fetch__download_pack(buf->data, buf->offset, t->socket, repo, bytes, stats); - } - - /* For now we don't care about anything */ - git__free(pkt); - gitno_consume(buf, line_end); - } - - read_bytes = gitno_recv(buf); - } while (read_bytes); - - return read_bytes; + 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 git_close(git_transport *transport) +static int _git_action( + git_smart_subtransport_stream **stream, + git_smart_subtransport *subtransport, + const char *url, + git_smart_service_t action) { - transport_git *t = (transport_git*) transport; + git_subtransport *t = (git_subtransport *) subtransport; - /* Can't do anything if there's an error, so don't bother checking */ - git_pkt_send_flush(t->socket); - if (gitno_close(t->socket) < 0) { - giterr_set(GITERR_NET, "Failed to close socket"); - return -1; + switch (action) { + case GIT_SERVICE_UPLOADPACK_LS: + return _git_uploadpack_ls(t, url, stream); + + case GIT_SERVICE_UPLOADPACK: + return _git_uploadpack(t, url, stream); + + case GIT_SERVICE_RECEIVEPACK_LS: + return _git_receivepack_ls(t, url, stream); + + case GIT_SERVICE_RECEIVEPACK: + return _git_receivepack(t, url, stream); } -#ifdef GIT_WIN32 - WSACleanup(); -#endif + *stream = NULL; + return -1; +} + +static int _git_close(git_smart_subtransport *subtransport) +{ + git_subtransport *t = (git_subtransport *) subtransport; + + assert(!t->current_stream); + + GIT_UNUSED(t); return 0; } -static void git_free(git_transport *transport) +static void _git_free(git_smart_subtransport *subtransport) { - transport_git *t = (transport_git *) transport; - git_vector *refs = &t->refs; - unsigned int i; + git_subtransport *t = (git_subtransport *) subtransport; - for (i = 0; i < refs->length; ++i) { - git_pkt *p = git_vector_get(refs, i); - git_pkt_free(p); - } + assert(!t->current_stream); - git_vector_free(refs); - git__free(t->heads); - git_buf_free(&t->proto.buf); - git__free(t->parent.url); git__free(t); } -int git_transport_git(git_transport **out) +int git_smart_subtransport_git(git_smart_subtransport **out, git_transport *owner) { - transport_git *t; -#ifdef GIT_WIN32 - int ret; -#endif - - t = git__malloc(sizeof(transport_git)); - GITERR_CHECK_ALLOC(t); - - memset(t, 0x0, sizeof(transport_git)); + git_subtransport *t; - t->parent.connect = git_connect; - t->parent.ls = git_ls; - t->parent.negotiate_fetch = git_negotiate_fetch; - t->parent.download_pack = git_download_pack; - t->parent.close = git_close; - t->parent.free = git_free; - t->proto.refs = &t->refs; - t->proto.transport = (git_transport *) t; + if (!out) + return -1; - *out = (git_transport *) t; + t = git__calloc(sizeof(git_subtransport), 1); + GITERR_CHECK_ALLOC(t); -#ifdef GIT_WIN32 - ret = WSAStartup(MAKEWORD(2,2), &t->wsd); - if (ret != 0) { - git_free(*out); - giterr_set(GITERR_NET, "Winsock init failed"); - return -1; - } -#endif + t->owner = owner; + t->parent.action = _git_action; + t->parent.close = _git_close; + t->parent.free = _git_free; + *out = (git_smart_subtransport *) t; return 0; } diff --git a/src/transports/http.c b/src/transports/http.c index 2a8ebbb09..eca06ead2 100644 --- a/src/transports/http.c +++ b/src/transports/http.c @@ -1,25 +1,35 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ +#ifndef GIT_WINHTTP -#include <stdlib.h> #include "git2.h" #include "http_parser.h" - -#include "transport.h" -#include "common.h" -#include "netops.h" #include "buffer.h" -#include "pkt.h" -#include "refs.h" -#include "pack.h" -#include "fetch.h" -#include "filebuf.h" -#include "repository.h" -#include "protocol.h" +#include "netops.h" +#include "smart.h" + +static const char *prefix_http = "http://"; +static const char *prefix_https = "https://"; +static const char *upload_pack_service = "upload-pack"; +static const char *upload_pack_ls_service_url = "/info/refs?service=git-upload-pack"; +static const char *upload_pack_service_url = "/git-upload-pack"; +static const char *receive_pack_service = "receive-pack"; +static const char *receive_pack_ls_service_url = "/info/refs?service=git-receive-pack"; +static const char *receive_pack_service_url = "/git-receive-pack"; +static const char *get_verb = "GET"; +static const char *post_verb = "POST"; +static const char *basic_authtype = "Basic"; + +#define OWNING_SUBTRANSPORT(s) ((http_subtransport *)(s)->parent.subtransport) + +#define PARSE_ERROR_GENERIC -1 +#define PARSE_ERROR_REPLAY -2 + +#define CHUNK_SIZE 4096 enum last_cb { NONE, @@ -27,54 +37,132 @@ enum last_cb { VALUE }; +typedef enum { + GIT_HTTP_AUTH_BASIC = 1, +} http_authmechanism_t; + typedef struct { - git_transport parent; - git_protocol proto; - git_vector refs; - git_vector common; - GIT_SOCKET socket; - git_buf buf; - git_remote_head **heads; - int error; - int transfer_finished :1, - ct_found :1, - ct_finished :1, - pack_ready :1; - enum last_cb last_cb; - http_parser parser; - char *content_type; + git_smart_subtransport_stream parent; + const char *service; + const char *service_url; + char *redirect_url; + const char *verb; + char *chunk_buffer; + unsigned chunk_buffer_len; + unsigned sent_request : 1, + received_response : 1, + chunked : 1, + redirect_count : 3; +} http_stream; + +typedef struct { + git_smart_subtransport parent; + transport_smart *owner; + gitno_socket socket; + const char *path; char *host; char *port; - char *service; - git_transport_caps caps; -#ifdef GIT_WIN32 - WSADATA wsd; -#endif -} transport_http; - -static int gen_request(git_buf *buf, const char *url, const char *host, const char *op, - const char *service, ssize_t content_length, int ls) + char *user_from_url; + char *pass_from_url; + git_cred *cred; + git_cred *url_cred; + http_authmechanism_t auth_mechanism; + unsigned connected : 1, + use_ssl : 1; + + /* Parser structures */ + http_parser parser; + http_parser_settings settings; + gitno_buffer parse_buffer; + git_buf parse_header_name; + git_buf parse_header_value; + char parse_buffer_data[2048]; + char *content_type; + char *location; + git_vector www_authenticate; + enum last_cb last_cb; + int parse_error; + unsigned parse_finished : 1; +} http_subtransport; + +typedef struct { + http_stream *s; + http_subtransport *t; + + /* Target buffer details from read() */ + char *buffer; + size_t buf_size; + size_t *bytes_read; +} parser_context; + +static int apply_basic_credential(git_buf *buf, git_cred *cred) { - const char *path = url; + git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred; + git_buf raw = GIT_BUF_INIT; + int error = -1; - path = strchr(path, '/'); - if (path == NULL) /* Is 'git fetch http://host.com/' valid? */ - path = "/"; + git_buf_printf(&raw, "%s:%s", c->username, c->password); + + if (git_buf_oom(&raw) || + git_buf_puts(buf, "Authorization: Basic ") < 0 || + git_buf_put_base64(buf, git_buf_cstr(&raw), raw.size) < 0 || + git_buf_puts(buf, "\r\n") < 0) + goto on_error; + + error = 0; + +on_error: + if (raw.size) + memset(raw.ptr, 0x0, raw.size); + + git_buf_free(&raw); + return error; +} + +static int gen_request( + git_buf *buf, + http_stream *s, + size_t content_length) +{ + http_subtransport *t = OWNING_SUBTRANSPORT(s); + + if (!t->path) + t->path = "/"; + + /* If we were redirected, make sure to respect that here */ + if (s->redirect_url) + git_buf_printf(buf, "%s %s HTTP/1.1\r\n", s->verb, s->redirect_url); + else + git_buf_printf(buf, "%s %s%s HTTP/1.1\r\n", s->verb, t->path, s->service_url); - if (ls) { - git_buf_printf(buf, "%s %s/info/refs?service=git-%s HTTP/1.1\r\n", op, path, service); - } else { - git_buf_printf(buf, "%s %s/git-%s HTTP/1.1\r\n", op, path, service); - } git_buf_puts(buf, "User-Agent: git/1.0 (libgit2 " LIBGIT2_VERSION ")\r\n"); - git_buf_printf(buf, "Host: %s\r\n", host); - if (content_length > 0) { - git_buf_printf(buf, "Accept: application/x-git-%s-result\r\n", service); - git_buf_printf(buf, "Content-Type: application/x-git-%s-request\r\n", service); - git_buf_printf(buf, "Content-Length: %"PRIuZ "\r\n", content_length); - } else { + git_buf_printf(buf, "Host: %s\r\n", t->host); + + if (s->chunked || content_length > 0) { + git_buf_printf(buf, "Accept: application/x-git-%s-result\r\n", s->service); + git_buf_printf(buf, "Content-Type: application/x-git-%s-request\r\n", s->service); + + if (s->chunked) + git_buf_puts(buf, "Transfer-Encoding: chunked\r\n"); + else + git_buf_printf(buf, "Content-Length: %"PRIuZ "\r\n", content_length); + } else git_buf_puts(buf, "Accept: */*\r\n"); + + /* Apply credentials to the request */ + if (t->cred && t->cred->credtype == GIT_CREDTYPE_USERPASS_PLAINTEXT && + t->auth_mechanism == GIT_HTTP_AUTH_BASIC && + apply_basic_credential(buf, t->cred) < 0) + return -1; + + /* Use url-parsed basic auth if username and password are both provided */ + if (!t->cred && t->user_from_url && t->pass_from_url) { + if (!t->url_cred && + git_cred_userpass_plaintext_new(&t->url_cred, t->user_from_url, t->pass_from_url) < 0) + return -1; + if (apply_basic_credential(buf, t->url_cred) < 0) return -1; } + git_buf_puts(buf, "\r\n"); if (git_buf_oom(buf)) @@ -83,600 +171,783 @@ static int gen_request(git_buf *buf, const char *url, const char *host, const ch return 0; } -static int do_connect(transport_http *t, const char *host, const char *port) +static int parse_unauthorized_response( + git_vector *www_authenticate, + int *allowed_types, + http_authmechanism_t *auth_mechanism) { - GIT_SOCKET s; + unsigned i; + char *entry; + + git_vector_foreach(www_authenticate, i, entry) { + if (!strncmp(entry, basic_authtype, 5) && + (entry[5] == '\0' || entry[5] == ' ')) { + *allowed_types |= GIT_CREDTYPE_USERPASS_PLAINTEXT; + *auth_mechanism = GIT_HTTP_AUTH_BASIC; + } + } - if (t->parent.connected && http_should_keep_alive(&t->parser)) - return 0; + return 0; +} - if (gitno_connect(&s, host, port) < 0) - return -1; +static int on_header_ready(http_subtransport *t) +{ + git_buf *name = &t->parse_header_name; + git_buf *value = &t->parse_header_value; + + if (!strcasecmp("Content-Type", git_buf_cstr(name))) { + if (!t->content_type) { + t->content_type = git__strdup(git_buf_cstr(value)); + GITERR_CHECK_ALLOC(t->content_type); + } + } + else if (!strcmp("WWW-Authenticate", git_buf_cstr(name))) { + char *dup = git__strdup(git_buf_cstr(value)); + GITERR_CHECK_ALLOC(dup); - t->socket = s; - t->parent.connected = 1; + git_vector_insert(&t->www_authenticate, dup); + } + else if (!strcasecmp("Location", git_buf_cstr(name))) { + if (!t->location) { + t->location= git__strdup(git_buf_cstr(value)); + GITERR_CHECK_ALLOC(t->location); + } + } return 0; } -/* - * The HTTP parser is streaming, so we need to wait until we're in the - * field handler before we can be sure that we can store the previous - * value. Right now, we only care about the - * Content-Type. on_header_{field,value} should be kept generic enough - * to work for any request. - */ - -static const char *typestr = "Content-Type"; - static int on_header_field(http_parser *parser, const char *str, size_t len) { - transport_http *t = (transport_http *) parser->data; - git_buf *buf = &t->buf; - - if (t->last_cb == VALUE && t->ct_found) { - t->ct_finished = 1; - t->ct_found = 0; - t->content_type = git__strdup(git_buf_cstr(buf)); - GITERR_CHECK_ALLOC(t->content_type); - git_buf_clear(buf); - } + parser_context *ctx = (parser_context *) parser->data; + http_subtransport *t = ctx->t; - if (t->ct_found) { - t->last_cb = FIELD; - return 0; - } + /* Both parse_header_name and parse_header_value are populated + * and ready for consumption */ + if (VALUE == t->last_cb) + if (on_header_ready(t) < 0) + return t->parse_error = PARSE_ERROR_GENERIC; - if (t->last_cb != FIELD) - git_buf_clear(buf); + if (NONE == t->last_cb || VALUE == t->last_cb) + git_buf_clear(&t->parse_header_name); - git_buf_put(buf, str, len); - t->last_cb = FIELD; + if (git_buf_put(&t->parse_header_name, str, len) < 0) + return t->parse_error = PARSE_ERROR_GENERIC; - return git_buf_oom(buf); + t->last_cb = FIELD; + return 0; } static int on_header_value(http_parser *parser, const char *str, size_t len) { - transport_http *t = (transport_http *) parser->data; - git_buf *buf = &t->buf; + parser_context *ctx = (parser_context *) parser->data; + http_subtransport *t = ctx->t; - if (t->ct_finished) { - t->last_cb = VALUE; - return 0; - } + assert(NONE != t->last_cb); - if (t->last_cb == VALUE) - git_buf_put(buf, str, len); + if (FIELD == t->last_cb) + git_buf_clear(&t->parse_header_value); - if (t->last_cb == FIELD && !strcmp(git_buf_cstr(buf), typestr)) { - t->ct_found = 1; - git_buf_clear(buf); - git_buf_put(buf, str, len); - } + if (git_buf_put(&t->parse_header_value, str, len) < 0) + return t->parse_error = PARSE_ERROR_GENERIC; t->last_cb = VALUE; - - return git_buf_oom(buf); + return 0; } static int on_headers_complete(http_parser *parser) { - transport_http *t = (transport_http *) parser->data; - git_buf *buf = &t->buf; + parser_context *ctx = (parser_context *) parser->data; + http_subtransport *t = ctx->t; + http_stream *s = ctx->s; + git_buf buf = GIT_BUF_INIT; + + /* Both parse_header_name and parse_header_value are populated + * and ready for consumption. */ + if (VALUE == t->last_cb) + if (on_header_ready(t) < 0) + return t->parse_error = PARSE_ERROR_GENERIC; + + /* Check for an authentication failure. */ + if (parser->status_code == 401 && + get_verb == s->verb && + t->owner->cred_acquire_cb) { + int allowed_types = 0; + + if (parse_unauthorized_response(&t->www_authenticate, + &allowed_types, &t->auth_mechanism) < 0) + return t->parse_error = PARSE_ERROR_GENERIC; + + if (allowed_types && + (!t->cred || 0 == (t->cred->credtype & allowed_types))) { + + if (t->owner->cred_acquire_cb(&t->cred, + t->owner->url, + t->user_from_url, + allowed_types, + t->owner->cred_acquire_payload) < 0) + return PARSE_ERROR_GENERIC; + + assert(t->cred); + + /* Successfully acquired a credential. */ + return t->parse_error = PARSE_ERROR_REPLAY; + } + } - /* The content-type is text/plain for 404, so don't validate */ - if (parser->status_code == 404) { - git_buf_clear(buf); - return 0; + /* Check for a redirect. + * Right now we only permit a redirect to the same hostname. */ + if ((parser->status_code == 301 || + parser->status_code == 302 || + (parser->status_code == 303 && get_verb == s->verb) || + parser->status_code == 307) && + t->location) { + + if (s->redirect_count >= 7) { + giterr_set(GITERR_NET, "Too many redirects"); + return t->parse_error = PARSE_ERROR_GENERIC; + } + + if (t->location[0] != '/') { + giterr_set(GITERR_NET, "Only relative redirects are supported"); + return t->parse_error = PARSE_ERROR_GENERIC; + } + + /* Set the redirect URL on the stream. This is a transfer of + * ownership of the memory. */ + if (s->redirect_url) + git__free(s->redirect_url); + + s->redirect_url = t->location; + t->location = NULL; + + t->connected = 0; + s->redirect_count++; + + return t->parse_error = PARSE_ERROR_REPLAY; } - if (t->content_type == NULL) { - t->content_type = git__strdup(git_buf_cstr(buf)); - if (t->content_type == NULL) - return t->error = -1; + /* Check for a 200 HTTP status code. */ + if (parser->status_code != 200) { + giterr_set(GITERR_NET, + "Unexpected HTTP status code: %d", + parser->status_code); + return t->parse_error = PARSE_ERROR_GENERIC; } - git_buf_clear(buf); - git_buf_printf(buf, "application/x-git-%s-advertisement", t->service); - if (git_buf_oom(buf)) - return t->error = -1; + /* The response must contain a Content-Type header. */ + if (!t->content_type) { + giterr_set(GITERR_NET, "No Content-Type header in response"); + return t->parse_error = PARSE_ERROR_GENERIC; + } - if (strcmp(t->content_type, git_buf_cstr(buf))) - return t->error = -1; + /* The Content-Type header must match our expectation. */ + if (get_verb == s->verb) + git_buf_printf(&buf, + "application/x-git-%s-advertisement", + ctx->s->service); + else + git_buf_printf(&buf, + "application/x-git-%s-result", + ctx->s->service); + + if (git_buf_oom(&buf)) + return t->parse_error = PARSE_ERROR_GENERIC; + + if (strcmp(t->content_type, git_buf_cstr(&buf))) { + git_buf_free(&buf); + giterr_set(GITERR_NET, + "Invalid Content-Type: %s", + t->content_type); + return t->parse_error = PARSE_ERROR_GENERIC; + } + + git_buf_free(&buf); - git_buf_clear(buf); return 0; } -static int on_body_store_refs(http_parser *parser, const char *str, size_t len) +static int on_message_complete(http_parser *parser) { - transport_http *t = (transport_http *) parser->data; + parser_context *ctx = (parser_context *) parser->data; + http_subtransport *t = ctx->t; - if (parser->status_code == 404) { - return git_buf_put(&t->buf, str, len); - } + t->parse_finished = 1; - return git_protocol_store_refs(&t->proto, str, len); + return 0; } -static int on_message_complete(http_parser *parser) +static int on_body_fill_buffer(http_parser *parser, const char *str, size_t len) { - transport_http *t = (transport_http *) parser->data; + parser_context *ctx = (parser_context *) parser->data; + http_subtransport *t = ctx->t; - t->transfer_finished = 1; - - if (parser->status_code == 404) { - giterr_set(GITERR_NET, "Remote error: %s", git_buf_cstr(&t->buf)); - t->error = -1; + if (ctx->buf_size < len) { + giterr_set(GITERR_NET, "Can't fit data in the buffer"); + return t->parse_error = PARSE_ERROR_GENERIC; } + memcpy(ctx->buffer, str, len); + *(ctx->bytes_read) += len; + ctx->buffer += len; + ctx->buf_size -= len; + return 0; } -static int store_refs(transport_http *t) +static void clear_parser_state(http_subtransport *t) { - http_parser_settings settings; - char buffer[1024]; - gitno_buffer buf; - git_pkt *pkt; - int ret; + unsigned i; + char *entry; http_parser_init(&t->parser, HTTP_RESPONSE); - t->parser.data = t; - memset(&settings, 0x0, sizeof(http_parser_settings)); - settings.on_header_field = on_header_field; - settings.on_header_value = on_header_value; - settings.on_headers_complete = on_headers_complete; - settings.on_body = on_body_store_refs; - settings.on_message_complete = on_message_complete; + gitno_buffer_setup(&t->socket, + &t->parse_buffer, + t->parse_buffer_data, + sizeof(t->parse_buffer_data)); - gitno_buffer_setup(&buf, buffer, sizeof(buffer), t->socket); + t->last_cb = NONE; + t->parse_error = 0; + t->parse_finished = 0; - while(1) { - size_t parsed; - - if ((ret = gitno_recv(&buf)) < 0) - return -1; + git_buf_free(&t->parse_header_name); + git_buf_init(&t->parse_header_name, 0); - parsed = http_parser_execute(&t->parser, &settings, buf.data, buf.offset); - /* Both should happen at the same time */ - if (parsed != buf.offset || t->error < 0) - return t->error; + git_buf_free(&t->parse_header_value); + git_buf_init(&t->parse_header_value, 0); - gitno_consume_n(&buf, parsed); + git__free(t->content_type); + t->content_type = NULL; - if (ret == 0 || t->transfer_finished) - return 0; - } + git__free(t->location); + t->location = NULL; - pkt = git_vector_get(&t->refs, 0); - if (pkt == NULL || pkt->type != GIT_PKT_COMMENT) { - giterr_set(GITERR_NET, "Invalid HTTP response"); - return t->error = -1; - } else { - git_vector_remove(&t->refs, 0); - } + git_vector_foreach(&t->www_authenticate, i, entry) + git__free(entry); - return 0; + git_vector_free(&t->www_authenticate); } -static int http_connect(git_transport *transport, int direction) +static int write_chunk(gitno_socket *socket, const char *buffer, size_t len) { - transport_http *t = (transport_http *) transport; - int ret; - git_buf request = GIT_BUF_INIT; - const char *service = "upload-pack"; - const char *url = t->parent.url, *prefix = "http://"; + git_buf buf = GIT_BUF_INIT; + + /* Chunk header */ + git_buf_printf(&buf, "%X\r\n", (unsigned)len); + + if (git_buf_oom(&buf)) + return -1; - if (direction == GIT_DIR_PUSH) { - giterr_set(GITERR_NET, "Pushing over HTTP is not implemented"); + if (gitno_send(socket, buf.ptr, buf.size, 0) < 0) { + git_buf_free(&buf); return -1; } - t->parent.direction = direction; - if (git_vector_init(&t->refs, 16, NULL) < 0) + git_buf_free(&buf); + + /* Chunk body */ + if (len > 0 && gitno_send(socket, buffer, len, 0) < 0) + return -1; + + /* Chunk footer */ + if (gitno_send(socket, "\r\n", 2, 0) < 0) return -1; - if (!git__prefixcmp(url, prefix)) - url += strlen(prefix); + return 0; +} - if ((ret = gitno_extract_host_and_port(&t->host, &t->port, url, "80")) < 0) - goto cleanup; +static int http_connect(http_subtransport *t) +{ + int flags = 0; - t->service = git__strdup(service); - GITERR_CHECK_ALLOC(t->service); + if (t->connected && + http_should_keep_alive(&t->parser) && + http_body_is_final(&t->parser)) + return 0; - if ((ret = do_connect(t, t->host, t->port)) < 0) - goto cleanup; + if (t->socket.socket) + gitno_close(&t->socket); - /* Generate and send the HTTP request */ - if ((ret = gen_request(&request, url, t->host, "GET", service, 0, 1)) < 0) { - giterr_set(GITERR_NET, "Failed to generate request"); - goto cleanup; - } + if (t->use_ssl) { + int tflags; - if ((ret = gitno_send(t->socket, request.ptr, request.size, 0)) < 0) - goto cleanup; + if (t->owner->parent.read_flags(&t->owner->parent, &tflags) < 0) + return -1; - ret = store_refs(t); + flags |= GITNO_CONNECT_SSL; -cleanup: - git_buf_free(&request); - git_buf_clear(&t->buf); + if (GIT_TRANSPORTFLAGS_NO_CHECK_CERT & tflags) + flags |= GITNO_CONNECT_SSL_NO_CHECK_CERT; + } + + if (gitno_connect(&t->socket, t->host, t->port, flags) < 0) + return -1; - return ret; + t->connected = 1; + return 0; } -static int http_ls(git_transport *transport, git_headlist_cb list_cb, void *opaque) +static int http_stream_read( + git_smart_subtransport_stream *stream, + char *buffer, + size_t buf_size, + size_t *bytes_read) { - transport_http *t = (transport_http *) transport; - git_vector *refs = &t->refs; - unsigned int i; - git_pkt_ref *p; + http_stream *s = (http_stream *)stream; + http_subtransport *t = OWNING_SUBTRANSPORT(s); + parser_context ctx; + size_t bytes_parsed; + +replay: + *bytes_read = 0; + + assert(t->connected); + + if (!s->sent_request) { + git_buf request = GIT_BUF_INIT; + + clear_parser_state(t); - git_vector_foreach(refs, i, p) { - if (p->type != GIT_PKT_REF) - continue; + if (gen_request(&request, s, 0) < 0) { + giterr_set(GITERR_NET, "Failed to generate request"); + return -1; + } - if (list_cb(&p->head, opaque) < 0) { - giterr_set(GITERR_NET, "The user callback returned error"); + if (gitno_send(&t->socket, request.ptr, request.size, 0) < 0) { + git_buf_free(&request); return -1; } + + git_buf_free(&request); + + s->sent_request = 1; } - return 0; -} + if (!s->received_response) { + if (s->chunked) { + assert(s->verb == post_verb); -static int on_body_parse_response(http_parser *parser, const char *str, size_t len) -{ - transport_http *t = (transport_http *) parser->data; - git_buf *buf = &t->buf; - git_vector *common = &t->common; - int error; - const char *line_end, *ptr; - - if (len == 0) { /* EOF */ - if (git_buf_len(buf) != 0) { - giterr_set(GITERR_NET, "Unexpected EOF"); - return t->error = -1; - } else { - return 0; + /* Flush, if necessary */ + if (s->chunk_buffer_len > 0 && + write_chunk(&t->socket, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + + /* Write the final chunk. */ + if (gitno_send(&t->socket, "0\r\n\r\n", 5, 0) < 0) + return -1; } + + s->received_response = 1; } - git_buf_put(buf, str, len); - ptr = buf->ptr; - while (1) { - git_pkt *pkt; + while (!*bytes_read && !t->parse_finished) { + t->parse_buffer.offset = 0; - if (git_buf_len(buf) == 0) - return 0; + if (gitno_recv(&t->parse_buffer) < 0) + return -1; - error = git_pkt_parse_line(&pkt, ptr, &line_end, git_buf_len(buf)); - if (error == GIT_EBUFS) { - return 0; /* Ask for more */ - } - if (error < 0) - return t->error = -1; + /* This call to http_parser_execute will result in invocations of the + * on_* family of callbacks. The most interesting of these is + * on_body_fill_buffer, which is called when data is ready to be copied + * into the target buffer. We need to marshal the buffer, buf_size, and + * bytes_read parameters to this callback. */ + ctx.t = t; + ctx.s = s; + ctx.buffer = buffer; + ctx.buf_size = buf_size; + ctx.bytes_read = bytes_read; - git_buf_consume(buf, line_end); + /* Set the context, call the parser, then unset the context. */ + t->parser.data = &ctx; - if (pkt->type == GIT_PKT_PACK) { - git__free(pkt); - t->pack_ready = 1; - return 0; - } + bytes_parsed = http_parser_execute(&t->parser, + &t->settings, + t->parse_buffer.data, + t->parse_buffer.offset); - if (pkt->type == GIT_PKT_NAK) { - git__free(pkt); - return 0; - } + t->parser.data = NULL; + + /* If there was a handled authentication failure, then parse_error + * will have signaled us that we should replay the request. */ + if (PARSE_ERROR_REPLAY == t->parse_error) { + s->sent_request = 0; - if (pkt->type != GIT_PKT_ACK) { - git__free(pkt); - continue; + if (http_connect(t) < 0) + return -1; + + goto replay; } - if (git_vector_insert(common, pkt) < 0) + if (t->parse_error < 0) return -1; - } - return error; + if (bytes_parsed != t->parse_buffer.offset) { + giterr_set(GITERR_NET, + "HTTP parser error: %s", + http_errno_description((enum http_errno)t->parser.http_errno)); + return -1; + } + } + return 0; } -static int parse_response(transport_http *t) +static int http_stream_write_chunked( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) { - int ret = 0; - http_parser_settings settings; - char buffer[1024]; - gitno_buffer buf; + http_stream *s = (http_stream *)stream; + http_subtransport *t = OWNING_SUBTRANSPORT(s); - http_parser_init(&t->parser, HTTP_RESPONSE); - t->parser.data = t; - t->transfer_finished = 0; - memset(&settings, 0x0, sizeof(http_parser_settings)); - settings.on_header_field = on_header_field; - settings.on_header_value = on_header_value; - settings.on_headers_complete = on_headers_complete; - settings.on_body = on_body_parse_response; - settings.on_message_complete = on_message_complete; + assert(t->connected); - gitno_buffer_setup(&buf, buffer, sizeof(buffer), t->socket); + /* Send the request, if necessary */ + if (!s->sent_request) { + git_buf request = GIT_BUF_INIT; - while(1) { - size_t parsed; + clear_parser_state(t); - if ((ret = gitno_recv(&buf)) < 0) + if (gen_request(&request, s, 0) < 0) { + giterr_set(GITERR_NET, "Failed to generate request"); return -1; + } - parsed = http_parser_execute(&t->parser, &settings, buf.data, buf.offset); - /* Both should happen at the same time */ - if (parsed != buf.offset || t->error < 0) - return t->error; + if (gitno_send(&t->socket, request.ptr, request.size, 0) < 0) { + git_buf_free(&request); + return -1; + } + + git_buf_free(&request); + + s->sent_request = 1; + } + + if (len > CHUNK_SIZE) { + /* Flush, if necessary */ + if (s->chunk_buffer_len > 0) { + if (write_chunk(&t->socket, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + } - gitno_consume_n(&buf, parsed); + /* Write chunk directly */ + if (write_chunk(&t->socket, buffer, len) < 0) + return -1; + } + else { + /* Append as much to the buffer as we can */ + int count = min(CHUNK_SIZE - s->chunk_buffer_len, len); + + if (!s->chunk_buffer) + s->chunk_buffer = git__malloc(CHUNK_SIZE); + + memcpy(s->chunk_buffer + s->chunk_buffer_len, buffer, count); + s->chunk_buffer_len += count; + buffer += count; + len -= count; - if (ret == 0 || t->transfer_finished || t->pack_ready) { - return 0; + /* Is the buffer full? If so, then flush */ + if (CHUNK_SIZE == s->chunk_buffer_len) { + if (write_chunk(&t->socket, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + + if (len > 0) { + memcpy(s->chunk_buffer, buffer, len); + s->chunk_buffer_len = len; + } } } - return ret; + return 0; } -static int http_negotiate_fetch(git_transport *transport, git_repository *repo, const git_vector *wants) +static int http_stream_write_single( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) { - transport_http *t = (transport_http *) transport; - int ret; - unsigned int i; - char buff[128]; - gitno_buffer buf; - git_revwalk *walk = NULL; - git_oid oid; - git_pkt_ack *pkt; - git_vector *common = &t->common; - const char *prefix = "http://", *url = t->parent.url; - git_buf request = GIT_BUF_INIT, data = GIT_BUF_INIT; - gitno_buffer_setup(&buf, buff, sizeof(buff), t->socket); - - /* TODO: Store url in the transport */ - if (!git__prefixcmp(url, prefix)) - url += strlen(prefix); - - if (git_vector_init(common, 16, NULL) < 0) - return -1; + http_stream *s = (http_stream *)stream; + http_subtransport *t = OWNING_SUBTRANSPORT(s); + git_buf request = GIT_BUF_INIT; - if (git_fetch_setup_walk(&walk, repo) < 0) + assert(t->connected); + + if (s->sent_request) { + giterr_set(GITERR_NET, "Subtransport configured for only one write"); return -1; + } - do { - if ((ret = do_connect(t, t->host, t->port)) < 0) - goto cleanup; + clear_parser_state(t); - if ((ret = git_pkt_buffer_wants(wants, &t->caps, &data)) < 0) - goto cleanup; + if (gen_request(&request, s, len) < 0) { + giterr_set(GITERR_NET, "Failed to generate request"); + return -1; + } - /* We need to send these on each connection */ - git_vector_foreach (common, i, pkt) { - if ((ret = git_pkt_buffer_have(&pkt->oid, &data)) < 0) - goto cleanup; - } + if (gitno_send(&t->socket, request.ptr, request.size, 0) < 0) + goto on_error; - i = 0; - while ((i < 20) && ((ret = git_revwalk_next(&oid, walk)) == 0)) { - if ((ret = git_pkt_buffer_have(&oid, &data)) < 0) - goto cleanup; + if (len && gitno_send(&t->socket, buffer, len, 0) < 0) + goto on_error; - i++; - } + git_buf_free(&request); + s->sent_request = 1; - git_pkt_buffer_done(&data); + return 0; + +on_error: + git_buf_free(&request); + return -1; +} - if ((ret = gen_request(&request, url, t->host, "POST", "upload-pack", data.size, 0)) < 0) - goto cleanup; +static void http_stream_free(git_smart_subtransport_stream *stream) +{ + http_stream *s = (http_stream *)stream; - if ((ret = gitno_send(t->socket, request.ptr, request.size, 0)) < 0) - goto cleanup; + if (s->chunk_buffer) + git__free(s->chunk_buffer); - if ((ret = gitno_send(t->socket, data.ptr, data.size, 0)) < 0) - goto cleanup; + if (s->redirect_url) + git__free(s->redirect_url); - git_buf_clear(&request); - git_buf_clear(&data); + git__free(s); +} - if (ret < 0 || i >= 256) - break; +static int http_stream_alloc(http_subtransport *t, + git_smart_subtransport_stream **stream) +{ + http_stream *s; - if ((ret = parse_response(t)) < 0) - goto cleanup; + if (!stream) + return -1; - if (t->pack_ready) { - ret = 0; - goto cleanup; - } + s = git__calloc(sizeof(http_stream), 1); + GITERR_CHECK_ALLOC(s); - } while(1); + s->parent.subtransport = &t->parent; + s->parent.read = http_stream_read; + s->parent.write = http_stream_write_single; + s->parent.free = http_stream_free; -cleanup: - git_buf_free(&request); - git_buf_free(&data); - git_revwalk_free(walk); - return ret; + *stream = (git_smart_subtransport_stream *)s; + return 0; } -typedef struct { - git_indexer_stream *idx; - git_indexer_stats *stats; - transport_http *transport; -} download_pack_cbdata; - -static int on_message_complete_download_pack(http_parser *parser) +static int http_uploadpack_ls( + http_subtransport *t, + git_smart_subtransport_stream **stream) { - download_pack_cbdata *data = (download_pack_cbdata *) parser->data; + http_stream *s; + + if (http_stream_alloc(t, stream) < 0) + return -1; + + s = (http_stream *)*stream; - data->transport->transfer_finished = 1; + s->service = upload_pack_service; + s->service_url = upload_pack_ls_service_url; + s->verb = get_verb; return 0; } -static int on_body_download_pack(http_parser *parser, const char *str, size_t len) + +static int http_uploadpack( + http_subtransport *t, + git_smart_subtransport_stream **stream) { - download_pack_cbdata *data = (download_pack_cbdata *) parser->data; - transport_http *t = data->transport; - git_indexer_stream *idx = data->idx; - git_indexer_stats *stats = data->stats; + http_stream *s; - return t->error = git_indexer_stream_add(idx, str, len, stats); + if (http_stream_alloc(t, stream) < 0) + return -1; + + s = (http_stream *)*stream; + + s->service = upload_pack_service; + s->service_url = upload_pack_service_url; + s->verb = post_verb; + + return 0; } -/* - * As the server is probably using Transfer-Encoding: chunked, we have - * to use the HTTP parser to download the pack instead of giving it to - * the simple downloader. Furthermore, we're using keep-alive - * connections, so the simple downloader would just hang. - */ -static int http_download_pack(git_transport *transport, git_repository *repo, git_off_t *bytes, git_indexer_stats *stats) +static int http_receivepack_ls( + http_subtransport *t, + git_smart_subtransport_stream **stream) { - transport_http *t = (transport_http *) transport; - git_buf *oldbuf = &t->buf; - int recvd; - http_parser_settings settings; - char buffer[1024]; - gitno_buffer buf; - git_indexer_stream *idx = NULL; - download_pack_cbdata data; + http_stream *s; + + if (http_stream_alloc(t, stream) < 0) + return -1; + + s = (http_stream *)*stream; + + s->service = receive_pack_service; + s->service_url = receive_pack_ls_service_url; + s->verb = get_verb; + + return 0; +} - gitno_buffer_setup(&buf, buffer, sizeof(buffer), t->socket); +static int http_receivepack( + http_subtransport *t, + git_smart_subtransport_stream **stream) +{ + http_stream *s; - if (memcmp(oldbuf->ptr, "PACK", strlen("PACK"))) { - giterr_set(GITERR_NET, "The pack doesn't start with a pack signature"); + if (http_stream_alloc(t, stream) < 0) return -1; - } - if (git_indexer_stream_new(&idx, git_repository_path(repo)) < 0) + s = (http_stream *)*stream; + + /* Use Transfer-Encoding: chunked for this request */ + s->chunked = 1; + s->parent.write = http_stream_write_chunked; + + s->service = receive_pack_service; + s->service_url = receive_pack_service_url; + s->verb = post_verb; + + return 0; +} + +static int http_action( + git_smart_subtransport_stream **stream, + git_smart_subtransport *subtransport, + const char *url, + git_smart_service_t action) +{ + http_subtransport *t = (http_subtransport *)subtransport; + const char *default_port = NULL; + int ret; + + if (!stream) return -1; + if (!t->host || !t->port || !t->path) { + if (!git__prefixcmp(url, prefix_http)) { + url = url + strlen(prefix_http); + default_port = "80"; + } - /* - * This is part of the previous response, so we don't want to - * re-init the parser, just set these two callbacks. - */ - memset(stats, 0, sizeof(git_indexer_stats)); - data.stats = stats; - data.idx = idx; - data.transport = t; - t->parser.data = &data; - t->transfer_finished = 0; - memset(&settings, 0x0, sizeof(settings)); - settings.on_message_complete = on_message_complete_download_pack; - settings.on_body = on_body_download_pack; - *bytes = git_buf_len(oldbuf); - - if (git_indexer_stream_add(idx, git_buf_cstr(oldbuf), git_buf_len(oldbuf), stats) < 0) - goto on_error; + if (!git__prefixcmp(url, prefix_https)) { + url += strlen(prefix_https); + default_port = "443"; + t->use_ssl = 1; + } - do { - size_t parsed; + if (!default_port) + return -1; - if ((recvd = gitno_recv(&buf)) < 0) - goto on_error; + if ((ret = gitno_extract_url_parts(&t->host, &t->port, + &t->user_from_url, &t->pass_from_url, url, default_port)) < 0) + return ret; - parsed = http_parser_execute(&t->parser, &settings, buf.data, buf.offset); - if (parsed != buf.offset || t->error < 0) - goto on_error; + t->path = strchr(url, '/'); + } - *bytes += recvd; - gitno_consume_n(&buf, parsed); - } while (recvd > 0 && !t->transfer_finished); + if (http_connect(t) < 0) + return -1; - if (git_indexer_stream_finalize(idx, stats) < 0) - goto on_error; + switch (action) + { + case GIT_SERVICE_UPLOADPACK_LS: + return http_uploadpack_ls(t, stream); - git_indexer_stream_free(idx); - return 0; + case GIT_SERVICE_UPLOADPACK: + return http_uploadpack(t, stream); -on_error: - git_indexer_stream_free(idx); + case GIT_SERVICE_RECEIVEPACK_LS: + return http_receivepack_ls(t, stream); + + case GIT_SERVICE_RECEIVEPACK: + return http_receivepack(t, stream); + } + + *stream = NULL; return -1; } -static int http_close(git_transport *transport) +static int http_close(git_smart_subtransport *subtransport) { - transport_http *t = (transport_http *) transport; + http_subtransport *t = (http_subtransport *) subtransport; - if (gitno_close(t->socket) < 0) { - giterr_set(GITERR_OS, "Failed to close the socket: %s", strerror(errno)); - return -1; + clear_parser_state(t); + + if (t->socket.socket) { + gitno_close(&t->socket); + memset(&t->socket, 0x0, sizeof(gitno_socket)); } - return 0; -} + if (t->cred) { + t->cred->free(t->cred); + t->cred = NULL; + } + if (t->url_cred) { + t->url_cred->free(t->url_cred); + t->url_cred = NULL; + } -static void http_free(git_transport *transport) -{ - transport_http *t = (transport_http *) transport; - git_vector *refs = &t->refs; - git_vector *common = &t->common; - unsigned int i; - git_pkt *p; - -#ifdef GIT_WIN32 - /* cleanup the WSA context. note that this context - * can be initialized more than once with WSAStartup(), - * and needs to be cleaned one time for each init call - */ - WSACleanup(); -#endif - - git_vector_foreach(refs, i, p) { - git_pkt_free(p); + if (t->host) { + git__free(t->host); + t->host = NULL; } - git_vector_free(refs); - git_vector_foreach(common, i, p) { - git_pkt_free(p); + + if (t->port) { + git__free(t->port); + t->port = NULL; } - git_vector_free(common); - git_buf_free(&t->buf); - git_buf_free(&t->proto.buf); - git__free(t->heads); - git__free(t->content_type); - git__free(t->host); - git__free(t->port); - git__free(t->service); - git__free(t->parent.url); + + if (t->user_from_url) { + git__free(t->user_from_url); + t->user_from_url = NULL; + } + + if (t->pass_from_url) { + git__free(t->pass_from_url); + t->pass_from_url = NULL; + } + + return 0; +} + +static void http_free(git_smart_subtransport *subtransport) +{ + http_subtransport *t = (http_subtransport *) subtransport; + + http_close(subtransport); + git__free(t); } -int git_transport_http(git_transport **out) +int git_smart_subtransport_http(git_smart_subtransport **out, git_transport *owner) { - transport_http *t; + http_subtransport *t; - t = git__malloc(sizeof(transport_http)); - GITERR_CHECK_ALLOC(t); + if (!out) + return -1; - memset(t, 0x0, sizeof(transport_http)); + t = git__calloc(sizeof(http_subtransport), 1); + GITERR_CHECK_ALLOC(t); - t->parent.connect = http_connect; - t->parent.ls = http_ls; - t->parent.negotiate_fetch = http_negotiate_fetch; - t->parent.download_pack = http_download_pack; + t->owner = (transport_smart *)owner; + t->parent.action = http_action; t->parent.close = http_close; t->parent.free = http_free; - t->proto.refs = &t->refs; - t->proto.transport = (git_transport *) t; - -#ifdef GIT_WIN32 - /* on win32, the WSA context needs to be initialized - * before any socket calls can be performed */ - if (WSAStartup(MAKEWORD(2,2), &t->wsd) != 0) { - http_free((git_transport *) t); - giterr_set(GITERR_OS, "Winsock init failed"); - return -1; - } -#endif - *out = (git_transport *) t; + t->settings.on_header_field = on_header_field; + t->settings.on_header_value = on_header_value; + t->settings.on_headers_complete = on_headers_complete; + t->settings.on_body = on_body_fill_buffer; + t->settings.on_message_complete = on_message_complete; + + *out = (git_smart_subtransport *) t; return 0; } + +#endif /* !GIT_WINHTTP */ diff --git a/src/transports/local.c b/src/transports/local.c index 000993e69..8af970eac 100644 --- a/src/transports/local.c +++ b/src/transports/local.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -10,16 +10,34 @@ #include "git2/repository.h" #include "git2/object.h" #include "git2/tag.h" +#include "git2/transport.h" +#include "git2/revwalk.h" +#include "git2/odb_backend.h" +#include "git2/pack.h" +#include "git2/commit.h" +#include "git2/revparse.h" +#include "git2/push.h" +#include "pack-objects.h" #include "refs.h" -#include "transport.h" #include "posix.h" #include "path.h" #include "buffer.h" +#include "repository.h" +#include "odb.h" +#include "push.h" +#include "remote.h" typedef struct { git_transport parent; + git_remote *owner; + char *url; + int direction; + int flags; + git_atomic cancelled; git_repository *repo; git_vector refs; + unsigned connected : 1, + have_refs : 1; } transport_local; static int add_ref(transport_local *t, const char *name) @@ -28,15 +46,28 @@ static int add_ref(transport_local *t, const char *name) git_remote_head *head; git_object *obj = NULL, *target = NULL; git_buf buf = GIT_BUF_INIT; + int error; - head = git__malloc(sizeof(git_remote_head)); + head = git__calloc(1, sizeof(git_remote_head)); GITERR_CHECK_ALLOC(head); head->name = git__strdup(name); GITERR_CHECK_ALLOC(head->name); - if (git_reference_name_to_oid(&head->oid, t->repo, name) < 0 || - git_vector_insert(&t->refs, head) < 0) + error = git_reference_name_to_id(&head->oid, t->repo, name); + if (error < 0) { + git__free(head->name); + git__free(head); + if (!strcmp(name, GIT_HEAD_FILE) && error == GIT_ENOTFOUND) { + /* This is actually okay. Empty repos often have a HEAD that points to + * a nonexistent "refs/heads/master". */ + giterr_clear(); + return 0; + } + return error; + } + + if (git_vector_insert(&t->refs, head) < 0) { git__free(head->name); git__free(head); @@ -52,14 +83,16 @@ static int add_ref(transport_local *t, const char *name) head = NULL; - /* If it's not an annotated tag, just get out */ - if (git_object_type(obj) != GIT_OBJ_TAG) { + /* If it's not an annotated tag, or if we're mocking + * git-receive-pack, just get out */ + if (git_object_type(obj) != GIT_OBJ_TAG || + t->direction != GIT_DIRECTION_FETCH) { git_object_free(obj); return 0; } /* And if it's a tag, peel it, and add it to the list */ - head = git__malloc(sizeof(git_remote_head)); + head = git__calloc(1, sizeof(git_remote_head)); GITERR_CHECK_ALLOC(head); if (git_buf_join(&buf, 0, name, peeled) < 0) return -1; @@ -92,14 +125,14 @@ static int store_refs(transport_local *t) assert(t); if (git_reference_list(&ref_names, t->repo, GIT_REF_LISTALL) < 0 || - git_vector_init(&t->refs, (unsigned int)ref_names.count, NULL) < 0) + git_vector_init(&t->refs, ref_names.count, NULL) < 0) goto on_error; /* Sort the references first */ git__tsort((void **)ref_names.strings, ref_names.count, &git__strcmp_cb); - /* Add HEAD */ - if (add_ref(t, GIT_HEAD_FILE) < 0) + /* Add HEAD iff direction is fetch */ + if (t->direction == GIT_DIRECTION_FETCH && add_ref(t, GIT_HEAD_FILE) < 0) goto on_error; for (i = 0; i < ref_names.count; ++i) { @@ -107,6 +140,7 @@ static int store_refs(transport_local *t) goto on_error; } + t->have_refs = 1; git_strarray_free(&ref_names); return 0; @@ -116,28 +150,16 @@ on_error: return -1; } -static int local_ls(git_transport *transport, git_headlist_cb list_cb, void *payload) -{ - transport_local *t = (transport_local *) transport; - git_vector *refs = &t->refs; - unsigned int i; - git_remote_head *h; - - assert(transport && transport->connected); - - git_vector_foreach(refs, i, h) { - if (list_cb(h, payload) < 0) - return -1; - } - - return 0; -} - /* * Try to open the url as a git directory. The direction doesn't * matter in this case because we're calulating the heads ourselves. */ -static int local_connect(git_transport *transport, int direction) +static int local_connect( + git_transport *transport, + const char *url, + git_cred_acquire_cb cred_acquire_cb, + void *cred_acquire_payload, + int direction, int flags) { git_repository *repo; int error; @@ -145,18 +167,24 @@ static int local_connect(git_transport *transport, int direction) const char *path; git_buf buf = GIT_BUF_INIT; - GIT_UNUSED(direction); + GIT_UNUSED(cred_acquire_cb); + GIT_UNUSED(cred_acquire_payload); + + t->url = git__strdup(url); + GITERR_CHECK_ALLOC(t->url); + t->direction = direction; + t->flags = flags; /* The repo layer doesn't want the prefix */ - if (!git__prefixcmp(transport->url, "file://")) { - if (git_path_fromurl(&buf, transport->url) < 0) { + if (!git__prefixcmp(t->url, "file://")) { + if (git_path_fromurl(&buf, t->url) < 0) { git_buf_free(&buf); return -1; } path = git_buf_cstr(&buf); } else { /* We assume transport->url is already a path */ - path = transport->url; + path = t->url; } error = git_repository_open(&repo, path); @@ -171,47 +199,411 @@ static int local_connect(git_transport *transport, int direction) if (store_refs(t) < 0) return -1; - t->parent.connected = 1; + t->connected = 1; + + return 0; +} + +static int local_ls(git_transport *transport, git_headlist_cb list_cb, void *payload) +{ + transport_local *t = (transport_local *)transport; + unsigned int i; + git_remote_head *head = NULL; + + if (!t->have_refs) { + giterr_set(GITERR_NET, "The transport has not yet loaded the refs"); + return -1; + } + + git_vector_foreach(&t->refs, i, head) { + if (list_cb(head, payload)) + return GIT_EUSER; + } return 0; } -static int local_negotiate_fetch(git_transport *transport, git_repository *repo, const git_vector *wants) +static int local_negotiate_fetch( + git_transport *transport, + git_repository *repo, + const git_remote_head * const *refs, + size_t count) { - GIT_UNUSED(transport); - GIT_UNUSED(repo); - GIT_UNUSED(wants); + transport_local *t = (transport_local*)transport; + git_remote_head *rhead; + unsigned int i; - giterr_set(GITERR_NET, "Fetch via local transport isn't implemented. Sorry"); - return -1; + GIT_UNUSED(refs); + GIT_UNUSED(count); + + /* Fill in the loids */ + git_vector_foreach(&t->refs, i, rhead) { + git_object *obj; + + int error = git_revparse_single(&obj, repo, rhead->name); + if (!error) + git_oid_cpy(&rhead->loid, git_object_id(obj)); + else if (error != GIT_ENOTFOUND) + return error; + git_object_free(obj); + giterr_clear(); + } + + return 0; +} + +static int local_push_copy_object( + git_odb *local_odb, + git_odb *remote_odb, + git_pobject *obj) +{ + int error = 0; + git_odb_object *odb_obj = NULL; + git_odb_stream *odb_stream; + size_t odb_obj_size; + git_otype odb_obj_type; + git_oid remote_odb_obj_oid; + + /* Object already exists in the remote ODB; do nothing and return 0*/ + if (git_odb_exists(remote_odb, &obj->id)) + return 0; + + if ((error = git_odb_read(&odb_obj, local_odb, &obj->id)) < 0) + return error; + + odb_obj_size = git_odb_object_size(odb_obj); + odb_obj_type = git_odb_object_type(odb_obj); + + if ((error = git_odb_open_wstream(&odb_stream, remote_odb, + odb_obj_size, odb_obj_type)) < 0) + goto on_error; + + if (odb_stream->write(odb_stream, (char *)git_odb_object_data(odb_obj), + 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) { + giterr_set(GITERR_ODB, "Error when writing object to remote odb " + "during local push operation. Remote odb object oid does not " + "match local oid."); + error = -1; + } + + odb_stream->free(odb_stream); + +on_error: + git_odb_object_free(odb_obj); + return error; +} + +static int local_push_update_remote_ref( + git_repository *remote_repo, + const char *lref, + const char *rref, + git_oid *loid, + git_oid *roid) +{ + int error; + git_reference *remote_ref = NULL; + + /* rref will be NULL if it is implicit in the pushspec (e.g. 'b1:') */ + rref = rref ? rref : lref; + + if (lref) { + /* Create or update a ref */ + if ((error = git_reference_create(NULL, remote_repo, rref, loid, + !git_oid_iszero(roid))) < 0) + return error; + } else { + /* Delete a ref */ + if ((error = git_reference_lookup(&remote_ref, remote_repo, rref)) < 0) { + if (error == GIT_ENOTFOUND) + error = 0; + return error; + } + + if ((error = git_reference_delete(remote_ref)) < 0) + return error; + + git_reference_free(remote_ref); + } + + return 0; +} + +static int local_push( + git_transport *transport, + git_push *push) +{ + transport_local *t = (transport_local *)transport; + git_odb *remote_odb = NULL; + git_odb *local_odb = NULL; + git_repository *remote_repo = NULL; + push_spec *spec; + char *url = NULL; + int error; + unsigned int i; + size_t j; + + 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 + 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) { + error = -1; + goto on_error; + } + + if ((error = git_repository_odb__weakptr(&remote_odb, remote_repo)) < 0 || + (error = git_repository_odb__weakptr(&local_odb, push->repo)) < 0) + goto on_error; + + for (i = 0; i < push->pb->nr_objects; i++) { + if ((error = local_push_copy_object(local_odb, remote_odb, + &push->pb->object_list[i])) < 0) + goto on_error; + } + + push->unpack_ok = 1; + + git_vector_foreach(&push->specs, j, spec) { + push_status *status; + const git_error *last; + char *ref = spec->rref ? spec->rref : spec->lref; + + status = git__calloc(sizeof(push_status), 1); + if (!status) + goto on_error; + + status->ref = git__strdup(ref); + if (!status->ref) { + git_push_status_free(status); + goto on_error; + } + + error = local_push_update_remote_ref(remote_repo, spec->lref, spec->rref, + &spec->loid, &spec->roid); + + switch (error) { + case GIT_OK: + break; + case GIT_EINVALIDSPEC: + status->msg = git__strdup("funny refname"); + break; + case GIT_ENOTFOUND: + status->msg = git__strdup("Remote branch not found to delete"); + break; + default: + last = giterr_last(); + + if (last && last->message) + status->msg = git__strdup(last->message); + else + status->msg = git__strdup("Unspecified error encountered"); + break; + } + + /* failed to allocate memory for a status message */ + if (error < 0 && !status->msg) { + git_push_status_free(status); + goto on_error; + } + + /* failed to insert the ref update status */ + if ((error = git_vector_insert(&push->status, status)) < 0) { + git_push_status_free(status); + goto on_error; + } + } + + if (push->specs.length) { + int flags = t->flags; + url = git__strdup(t->url); + + if (!url || t->parent.close(&t->parent) < 0 || + t->parent.connect(&t->parent, url, + push->remote->cred_acquire_cb, NULL, GIT_DIRECTION_PUSH, flags)) + goto on_error; + } + + error = 0; + +on_error: + git_repository_free(remote_repo); + git__free(url); + + return error; +} + +typedef struct foreach_data { + git_transfer_progress *stats; + git_transfer_progress_callback progress_cb; + void *progress_payload; + git_odb_writepack *writepack; +} foreach_data; + +static int foreach_cb(void *buf, size_t len, void *payload) +{ + foreach_data *data = (foreach_data*)payload; + + data->stats->received_bytes += len; + return data->writepack->add(data->writepack, buf, len, data->stats); +} + +static int local_download_pack( + git_transport *transport, + git_repository *repo, + git_transfer_progress *stats, + git_transfer_progress_callback progress_cb, + void *progress_payload) +{ + transport_local *t = (transport_local*)transport; + git_revwalk *walk = NULL; + git_remote_head *rhead; + unsigned int i; + int error = -1; + git_oid oid; + git_packbuilder *pack = NULL; + git_odb_writepack *writepack = NULL; + git_odb *odb = NULL; + + if ((error = git_revwalk_new(&walk, t->repo)) < 0) + goto cleanup; + git_revwalk_sorting(walk, GIT_SORT_TIME); + + if ((error = git_packbuilder_new(&pack, t->repo)) < 0) + goto cleanup; + + stats->total_objects = 0; + stats->indexed_objects = 0; + stats->received_objects = 0; + stats->received_bytes = 0; + + git_vector_foreach(&t->refs, i, rhead) { + git_object *obj; + if ((error = git_object_lookup(&obj, t->repo, &rhead->oid, GIT_OBJ_ANY)) < 0) + goto cleanup; + + if (git_object_type(obj) == GIT_OBJ_COMMIT) { + /* Revwalker includes only wanted commits */ + error = git_revwalk_push(walk, &rhead->oid); + if (!git_oid_iszero(&rhead->loid)) + error = git_revwalk_hide(walk, &rhead->loid); + } else { + /* Tag or some other wanted object. Add it on its own */ + error = git_packbuilder_insert(pack, &rhead->oid, rhead->name); + } + git_object_free(obj); + } + + /* Walk the objects, building a packfile */ + if ((error = git_repository_odb__weakptr(&odb, repo)) < 0) + goto cleanup; + + while ((error = git_revwalk_next(&oid, walk)) == 0) { + git_commit *commit; + + /* Skip commits we already have */ + if (git_odb_exists(odb, &oid)) continue; + + if (!git_object_lookup((git_object**)&commit, t->repo, &oid, GIT_OBJ_COMMIT)) { + const git_oid *tree_oid = git_commit_tree_id(commit); + + /* Add the commit and its tree */ + if ((error = git_packbuilder_insert(pack, &oid, NULL)) < 0 || + (error = git_packbuilder_insert_tree(pack, tree_oid)) < 0) { + git_commit_free(commit); + goto cleanup; + } + + git_commit_free(commit); + } + } + + if ((error = git_odb_write_pack(&writepack, odb, progress_cb, progress_payload)) < 0) + goto cleanup; + + /* Write the data to the ODB */ + { + foreach_data data = {0}; + data.stats = stats; + data.progress_cb = progress_cb; + data.progress_payload = progress_payload; + data.writepack = writepack; + + if ((error = git_packbuilder_foreach(pack, foreach_cb, &data)) < 0) + goto cleanup; + } + error = writepack->commit(writepack, stats); + +cleanup: + if (writepack) writepack->free(writepack); + git_packbuilder_free(pack); + git_revwalk_free(walk); + return error; +} + +static int local_is_connected(git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + return t->connected; +} + +static int local_read_flags(git_transport *transport, int *flags) +{ + transport_local *t = (transport_local *)transport; + + *flags = t->flags; + + return 0; +} + +static void local_cancel(git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + git_atomic_set(&t->cancelled, 1); } static int local_close(git_transport *transport) { transport_local *t = (transport_local *)transport; - git_repository_free(t->repo); - t->repo = NULL; + t->connected = 0; + + if (t->repo) { + git_repository_free(t->repo); + t->repo = NULL; + } + + if (t->url) { + git__free(t->url); + t->url = NULL; + } return 0; } static void local_free(git_transport *transport) { - unsigned int i; - transport_local *t = (transport_local *) transport; - git_vector *vec = &t->refs; - git_remote_head *h; + transport_local *t = (transport_local *)transport; + size_t i; + git_remote_head *head; - assert(transport); + /* Close the transport, if it's still open. */ + local_close(transport); - git_vector_foreach (vec, i, h) { - git__free(h->name); - git__free(h); + git_vector_foreach(&t->refs, i, head) { + git__free(head->name); + git__free(head); } - git_vector_free(vec); - git__free(t->parent.url); + git_vector_free(&t->refs); + + /* Free the transport */ git__free(t); } @@ -219,20 +611,28 @@ static void local_free(git_transport *transport) * Public API * **************/ -int git_transport_local(git_transport **out) +int git_transport_local(git_transport **out, git_remote *owner, void *param) { transport_local *t; - t = git__malloc(sizeof(transport_local)); - GITERR_CHECK_ALLOC(t); + GIT_UNUSED(param); - memset(t, 0x0, sizeof(transport_local)); + t = git__calloc(1, sizeof(transport_local)); + GITERR_CHECK_ALLOC(t); + t->parent.version = GIT_TRANSPORT_VERSION; t->parent.connect = local_connect; - t->parent.ls = local_ls; t->parent.negotiate_fetch = local_negotiate_fetch; + t->parent.download_pack = local_download_pack; + t->parent.push = local_push; t->parent.close = local_close; t->parent.free = local_free; + t->parent.ls = local_ls; + t->parent.is_connected = local_is_connected; + t->parent.read_flags = local_read_flags; + t->parent.cancel = local_cancel; + + t->owner = owner; *out = (git_transport *) t; diff --git a/src/transports/smart.c b/src/transports/smart.c new file mode 100644 index 000000000..416eb221f --- /dev/null +++ b/src/transports/smart.c @@ -0,0 +1,345 @@ +/* + * 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 "smart.h" +#include "refs.h" + +static int git_smart__recv_cb(gitno_buffer *buf) +{ + transport_smart *t = (transport_smart *) buf->cb_data; + size_t old_len, bytes_read; + int error; + + assert(t->current_stream); + + old_len = buf->offset; + + if ((error = t->current_stream->read(t->current_stream, buf->data + buf->offset, buf->len - buf->offset, &bytes_read)) < 0) + return error; + + buf->offset += bytes_read; + + if (t->packetsize_cb) + t->packetsize_cb(bytes_read, t->packetsize_payload); + + return (int)(buf->offset - old_len); +} + +GIT_INLINE(int) git_smart__reset_stream(transport_smart *t, bool close_subtransport) +{ + if (t->current_stream) { + t->current_stream->free(t->current_stream); + t->current_stream = NULL; + } + + if (close_subtransport && + t->wrapped->close(t->wrapped) < 0) + return -1; + + return 0; +} + +static int git_smart__set_callbacks( + git_transport *transport, + git_transport_message_cb progress_cb, + git_transport_message_cb error_cb, + void *message_cb_payload) +{ + transport_smart *t = (transport_smart *)transport; + + t->progress_cb = progress_cb; + t->error_cb = error_cb; + t->message_cb_payload = message_cb_payload; + + return 0; +} + +static int git_smart__connect( + git_transport *transport, + const char *url, + git_cred_acquire_cb cred_acquire_cb, + void *cred_acquire_payload, + int direction, + int flags) +{ + transport_smart *t = (transport_smart *)transport; + git_smart_subtransport_stream *stream; + int error; + git_pkt *pkt; + git_pkt_ref *first; + git_smart_service_t service; + + if (git_smart__reset_stream(t, true) < 0) + return -1; + + t->url = git__strdup(url); + GITERR_CHECK_ALLOC(t->url); + + t->direction = direction; + t->flags = flags; + t->cred_acquire_cb = cred_acquire_cb; + t->cred_acquire_payload = cred_acquire_payload; + + if (GIT_DIRECTION_FETCH == t->direction) + service = GIT_SERVICE_UPLOADPACK_LS; + else if (GIT_DIRECTION_PUSH == t->direction) + service = GIT_SERVICE_RECEIVEPACK_LS; + else { + giterr_set(GITERR_NET, "Invalid direction"); + return -1; + } + + if ((error = t->wrapped->action(&stream, t->wrapped, t->url, service)) < 0) + return error; + + /* Save off the current stream (i.e. socket) that we are working with */ + t->current_stream = stream; + + gitno_buffer_setup_callback(NULL, &t->buffer, t->buffer_data, sizeof(t->buffer_data), git_smart__recv_cb, t); + + /* 2 flushes for RPC; 1 for stateful */ + if ((error = git_smart__store_refs(t, t->rpc ? 2 : 1)) < 0) + return error; + + /* Strip the comment packet for RPC */ + if (t->rpc) { + pkt = (git_pkt *)git_vector_get(&t->refs, 0); + + if (!pkt || GIT_PKT_COMMENT != pkt->type) { + giterr_set(GITERR_NET, "Invalid response"); + return -1; + } else { + /* Remove the comment pkt from the list */ + git_vector_remove(&t->refs, 0); + git__free(pkt); + } + } + + /* We now have loaded the refs. */ + t->have_refs = 1; + + first = (git_pkt_ref *)git_vector_get(&t->refs, 0); + + /* Detect capabilities */ + if (git_smart__detect_caps(first, &t->caps) < 0) + return -1; + + /* If the only ref in the list is capabilities^{} with OID_ZERO, remove it */ + if (1 == t->refs.length && !strcmp(first->head.name, "capabilities^{}") && + git_oid_iszero(&first->head.oid)) { + git_vector_clear(&t->refs); + git_pkt_free((git_pkt *)first); + } + + if (t->rpc && git_smart__reset_stream(t, false) < 0) + return -1; + + /* We're now logically connected. */ + t->connected = 1; + + return 0; +} + +static int git_smart__ls(git_transport *transport, git_headlist_cb list_cb, void *payload) +{ + transport_smart *t = (transport_smart *)transport; + unsigned int i; + git_pkt *p = NULL; + + if (!t->have_refs) { + giterr_set(GITERR_NET, "The transport has not yet loaded the refs"); + return -1; + } + + git_vector_foreach(&t->refs, i, p) { + git_pkt_ref *pkt = NULL; + + if (p->type != GIT_PKT_REF) + continue; + + pkt = (git_pkt_ref *)p; + + if (list_cb(&pkt->head, payload)) + return GIT_EUSER; + } + + return 0; +} + +int git_smart__negotiation_step(git_transport *transport, void *data, size_t len) +{ + transport_smart *t = (transport_smart *)transport; + git_smart_subtransport_stream *stream; + int error; + + if (t->rpc && git_smart__reset_stream(t, false) < 0) + return -1; + + if (GIT_DIRECTION_FETCH != t->direction) { + giterr_set(GITERR_NET, "This operation is only valid for fetch"); + return -1; + } + + if ((error = t->wrapped->action(&stream, t->wrapped, t->url, GIT_SERVICE_UPLOADPACK)) < 0) + return error; + + /* If this is a stateful implementation, the stream we get back should be the same */ + assert(t->rpc || t->current_stream == stream); + + /* Save off the current stream (i.e. socket) that we are working with */ + t->current_stream = stream; + + if ((error = stream->write(stream, (const char *)data, len)) < 0) + return error; + + gitno_buffer_setup_callback(NULL, &t->buffer, t->buffer_data, sizeof(t->buffer_data), git_smart__recv_cb, t); + + return 0; +} + +int git_smart__get_push_stream(transport_smart *t, git_smart_subtransport_stream **stream) +{ + int error; + + if (t->rpc && git_smart__reset_stream(t, false) < 0) + return -1; + + if (GIT_DIRECTION_PUSH != t->direction) { + giterr_set(GITERR_NET, "This operation is only valid for push"); + return -1; + } + + if ((error = t->wrapped->action(stream, t->wrapped, t->url, GIT_SERVICE_RECEIVEPACK)) < 0) + return error; + + /* If this is a stateful implementation, the stream we get back should be the same */ + assert(t->rpc || t->current_stream == *stream); + + /* Save off the current stream (i.e. socket) that we are working with */ + t->current_stream = *stream; + + gitno_buffer_setup_callback(NULL, &t->buffer, t->buffer_data, sizeof(t->buffer_data), git_smart__recv_cb, t); + + return 0; +} + +static void git_smart__cancel(git_transport *transport) +{ + transport_smart *t = (transport_smart *)transport; + + git_atomic_set(&t->cancelled, 1); +} + +static int git_smart__is_connected(git_transport *transport) +{ + transport_smart *t = (transport_smart *)transport; + + return t->connected; +} + +static int git_smart__read_flags(git_transport *transport, int *flags) +{ + transport_smart *t = (transport_smart *)transport; + + *flags = t->flags; + + return 0; +} + +static int git_smart__close(git_transport *transport) +{ + transport_smart *t = (transport_smart *)transport; + git_vector *common = &t->common; + unsigned int i; + git_pkt *p; + int ret; + + ret = git_smart__reset_stream(t, true); + + git_vector_foreach(common, i, p) + git_pkt_free(p); + + git_vector_free(common); + + if (t->url) { + git__free(t->url); + t->url = NULL; + } + + t->connected = 0; + + return ret; +} + +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); + + /* 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); +} + +static int ref_name_cmp(const void *a, const void *b) +{ + const git_pkt_ref *ref_a = a, *ref_b = b; + + return strcmp(ref_a->head.name, ref_b->head.name); +} + +int git_transport_smart(git_transport **out, git_remote *owner, void *param) +{ + transport_smart *t; + git_smart_subtransport_definition *definition = (git_smart_subtransport_definition *)param; + + if (!param) + return -1; + + t = git__calloc(sizeof(transport_smart), 1); + GITERR_CHECK_ALLOC(t); + + t->parent.version = GIT_TRANSPORT_VERSION; + t->parent.set_callbacks = git_smart__set_callbacks; + t->parent.connect = git_smart__connect; + t->parent.close = git_smart__close; + t->parent.free = git_smart__free; + t->parent.negotiate_fetch = git_smart__negotiate_fetch; + t->parent.download_pack = git_smart__download_pack; + t->parent.push = git_smart__push; + t->parent.ls = git_smart__ls; + t->parent.is_connected = git_smart__is_connected; + t->parent.read_flags = git_smart__read_flags; + t->parent.cancel = git_smart__cancel; + + t->owner = owner; + t->rpc = definition->rpc; + + if (git_vector_init(&t->refs, 16, ref_name_cmp) < 0) { + git__free(t); + return -1; + } + + if (definition->callback(&t->wrapped, &t->parent) < 0) { + git__free(t); + return -1; + } + + *out = (git_transport *) t; + return 0; +} diff --git a/src/transports/smart.h b/src/transports/smart.h new file mode 100644 index 000000000..c52401a3c --- /dev/null +++ b/src/transports/smart.h @@ -0,0 +1,179 @@ +/* + * 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 "vector.h" +#include "netops.h" +#include "buffer.h" +#include "push.h" + +#define GIT_SIDE_BAND_DATA 1 +#define GIT_SIDE_BAND_PROGRESS 2 +#define GIT_SIDE_BAND_ERROR 3 + +#define GIT_CAP_OFS_DELTA "ofs-delta" +#define GIT_CAP_MULTI_ACK "multi_ack" +#define GIT_CAP_SIDE_BAND "side-band" +#define GIT_CAP_SIDE_BAND_64K "side-band-64k" +#define GIT_CAP_INCLUDE_TAG "include-tag" +#define GIT_CAP_DELETE_REFS "delete-refs" +#define GIT_CAP_REPORT_STATUS "report-status" + +enum git_pkt_type { + GIT_PKT_CMD, + GIT_PKT_FLUSH, + GIT_PKT_REF, + GIT_PKT_HAVE, + GIT_PKT_ACK, + GIT_PKT_NAK, + GIT_PKT_PACK, + GIT_PKT_COMMENT, + GIT_PKT_ERR, + GIT_PKT_DATA, + GIT_PKT_PROGRESS, + GIT_PKT_OK, + GIT_PKT_NG, + GIT_PKT_UNPACK, +}; + +/* Used for multi-ack */ +enum git_ack_status { + GIT_ACK_NONE, + GIT_ACK_CONTINUE, + GIT_ACK_COMMON, + GIT_ACK_READY +}; + +/* This would be a flush pkt */ +typedef struct { + enum git_pkt_type type; +} git_pkt; + +struct git_pkt_cmd { + enum git_pkt_type type; + char *cmd; + char *path; + char *host; +}; + +/* This is a pkt-line with some info in it */ +typedef struct { + enum git_pkt_type type; + git_remote_head head; + char *capabilities; +} git_pkt_ref; + +/* Useful later */ +typedef struct { + enum git_pkt_type type; + git_oid oid; + enum git_ack_status status; +} git_pkt_ack; + +typedef struct { + enum git_pkt_type type; + char comment[GIT_FLEX_ARRAY]; +} git_pkt_comment; + +typedef struct { + enum git_pkt_type type; + int len; + char data[GIT_FLEX_ARRAY]; +} git_pkt_data; + +typedef git_pkt_data git_pkt_progress; + +typedef struct { + enum git_pkt_type type; + int len; + char error[GIT_FLEX_ARRAY]; +} git_pkt_err; + +typedef struct { + enum git_pkt_type type; + char *ref; +} git_pkt_ok; + +typedef struct { + enum git_pkt_type type; + char *ref; + char *msg; +} git_pkt_ng; + +typedef struct { + enum git_pkt_type type; + int unpack_ok; +} git_pkt_unpack; + +typedef struct transport_smart_caps { + int common:1, + ofs_delta:1, + multi_ack: 1, + side_band:1, + side_band_64k:1, + include_tag:1, + delete_refs:1, + report_status:1; +} transport_smart_caps; + +typedef void (*packetsize_cb)(size_t received, void *payload); + +typedef struct { + git_transport parent; + git_remote *owner; + char *url; + git_cred_acquire_cb cred_acquire_cb; + void *cred_acquire_payload; + int direction; + int flags; + git_transport_message_cb progress_cb; + git_transport_message_cb error_cb; + void *message_cb_payload; + git_smart_subtransport *wrapped; + git_smart_subtransport_stream *current_stream; + transport_smart_caps caps; + git_vector refs; + git_vector common; + git_atomic cancelled; + packetsize_cb packetsize_cb; + void *packetsize_payload; + unsigned rpc : 1, + have_refs : 1, + connected : 1; + gitno_buffer buffer; + char buffer_data[65536]; +} transport_smart; + +/* smart_protocol.c */ +int git_smart__store_refs(transport_smart *t, int flushes); +int git_smart__detect_caps(git_pkt_ref *pkt, transport_smart_caps *caps); +int git_smart__push(git_transport *transport, git_push *push); + +int git_smart__negotiate_fetch( + git_transport *transport, + git_repository *repo, + const git_remote_head * const *refs, + size_t count); + +int git_smart__download_pack( + git_transport *transport, + git_repository *repo, + git_transfer_progress *stats, + git_transfer_progress_callback progress_cb, + void *progress_payload); + +/* smart.c */ +int git_smart__negotiation_step(git_transport *transport, void *data, size_t len); +int git_smart__get_push_stream(transport_smart *t, git_smart_subtransport_stream **out); + +/* smart_pkt.c */ +int git_pkt_parse_line(git_pkt **head, const char *line, const char **out, size_t len); +int git_pkt_buffer_flush(git_buf *buf); +int git_pkt_send_flush(GIT_SOCKET s); +int git_pkt_buffer_done(git_buf *buf); +int git_pkt_buffer_wants(const git_remote_head * const *refs, size_t count, transport_smart_caps *caps, git_buf *buf); +int git_pkt_buffer_have(git_oid *oid, git_buf *buf); +void git_pkt_free(git_pkt *pkt); diff --git a/src/pkt.c b/src/transports/smart_pkt.c index 95430ddfc..99da37567 100644 --- a/src/pkt.c +++ b/src/transports/smart_pkt.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -12,7 +12,7 @@ #include "git2/refs.h" #include "git2/revwalk.h" -#include "pkt.h" +#include "smart.h" #include "util.h" #include "netops.h" #include "posix.h" @@ -42,15 +42,29 @@ static int flush_pkt(git_pkt **out) /* the rest of the line will be useful for multi_ack */ static int ack_pkt(git_pkt **out, const char *line, size_t len) { - git_pkt *pkt; + git_pkt_ack *pkt; GIT_UNUSED(line); GIT_UNUSED(len); - pkt = git__malloc(sizeof(git_pkt)); + pkt = git__calloc(1, sizeof(git_pkt_ack)); GITERR_CHECK_ALLOC(pkt); pkt->type = GIT_PKT_ACK; - *out = pkt; + line += 3; + len -= 3; + + if (len >= GIT_OID_HEXSZ) { + git_oid_fromstr(&pkt->oid, line + 1); + line += GIT_OID_HEXSZ + 1; + len -= GIT_OID_HEXSZ + 1; + } + + if (len >= 7) { + if (!git__prefixcmp(line + 1, "continue")) + pkt->status = GIT_ACK_CONTINUE; + } + + *out = (git_pkt *) pkt; return 0; } @@ -108,6 +122,7 @@ static int err_pkt(git_pkt **out, const char *line, size_t len) GITERR_CHECK_ALLOC(pkt); pkt->type = GIT_PKT_ERR; + pkt->len = (int)len; memcpy(pkt->error, line, len); pkt->error[len] = '\0'; @@ -116,6 +131,61 @@ static int err_pkt(git_pkt **out, const char *line, size_t len) return 0; } +static int data_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_data *pkt; + + line++; + len--; + pkt = git__malloc(sizeof(git_pkt_data) + len); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_DATA; + pkt->len = (int) len; + memcpy(pkt->data, line, len); + + *out = (git_pkt *) pkt; + + return 0; +} + +static int progress_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_progress *pkt; + + line++; + len--; + pkt = git__malloc(sizeof(git_pkt_progress) + len); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_PROGRESS; + pkt->len = (int) len; + memcpy(pkt->data, line, len); + + *out = (git_pkt *) pkt; + + return 0; +} + +static int sideband_error_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_err *pkt; + + line++; + len--; + pkt = git__malloc(sizeof(git_pkt_err) + len + 1); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_ERR; + pkt->len = (int)len; + memcpy(pkt->error, line, len); + pkt->error[len] = '\0'; + + *out = (git_pkt *)pkt; + + return 0; +} + /* * Parse an other-ref line. */ @@ -164,6 +234,83 @@ error_out: return error; } +static int ok_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_ok *pkt; + const char *ptr; + + pkt = git__malloc(sizeof(*pkt)); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_OK; + + line += 3; /* skip "ok " */ + ptr = strchr(line, '\n'); + len = ptr - line; + + pkt->ref = git__malloc(len + 1); + GITERR_CHECK_ALLOC(pkt->ref); + + memcpy(pkt->ref, line, len); + pkt->ref[len] = '\0'; + + *out = (git_pkt *)pkt; + return 0; +} + +static int ng_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_ng *pkt; + const char *ptr; + + pkt = git__malloc(sizeof(*pkt)); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_NG; + + line += 3; /* skip "ng " */ + ptr = strchr(line, ' '); + len = ptr - line; + + pkt->ref = git__malloc(len + 1); + GITERR_CHECK_ALLOC(pkt->ref); + + memcpy(pkt->ref, line, len); + pkt->ref[len] = '\0'; + + line = ptr + 1; + ptr = strchr(line, '\n'); + len = ptr - line; + + pkt->msg = git__malloc(len + 1); + GITERR_CHECK_ALLOC(pkt->msg); + + memcpy(pkt->msg, line, len); + pkt->msg[len] = '\0'; + + *out = (git_pkt *)pkt; + return 0; +} + +static int unpack_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_unpack *pkt; + + GIT_UNUSED(len); + + pkt = git__malloc(sizeof(*pkt)); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_UNPACK; + if (!git__prefixcmp(line, "unpack ok")) + pkt->unpack_ok = 1; + else + pkt->unpack_ok = 0; + + *out = (git_pkt *)pkt; + return 0; +} + static int32_t parse_len(const char *line) { char num[PKT_LEN_SIZE + 1]; @@ -249,8 +396,13 @@ int git_pkt_parse_line( len -= PKT_LEN_SIZE; /* the encoded length includes its own size */ - /* Assming the minimal size is actually 4 */ - if (!git__prefixcmp(line, "ACK")) + if (*line == GIT_SIDE_BAND_DATA) + ret = data_pkt(head, line, len); + else if (*line == GIT_SIDE_BAND_PROGRESS) + ret = progress_pkt(head, line, len); + else if (*line == GIT_SIDE_BAND_ERROR) + ret = sideband_error_pkt(head, line, len); + else if (!git__prefixcmp(line, "ACK")) ret = ack_pkt(head, line, len); else if (!git__prefixcmp(line, "NAK")) ret = nak_pkt(head); @@ -258,6 +410,12 @@ int git_pkt_parse_line( ret = err_pkt(head, line, len); else if (*line == '#') ret = comment_pkt(head, line, len); + else if (!git__prefixcmp(line, "ok")) + ret = ok_pkt(head, line, len); + else if (!git__prefixcmp(line, "ng")) + ret = ng_pkt(head, line, len); + else if (!git__prefixcmp(line, "unpack")) + ret = unpack_pkt(head, line, len); else ret = ref_pkt(head, line, len); @@ -273,6 +431,17 @@ void git_pkt_free(git_pkt *pkt) git__free(p->head.name); } + if (pkt->type == GIT_PKT_OK) { + git_pkt_ok *p = (git_pkt_ok *) pkt; + git__free(p->ref); + } + + if (pkt->type == GIT_PKT_NG) { + git_pkt_ng *p = (git_pkt_ng *) pkt; + git__free(p->ref); + git__free(p->msg); + } + git__free(pkt); } @@ -281,28 +450,40 @@ int git_pkt_buffer_flush(git_buf *buf) return git_buf_put(buf, pkt_flush_str, strlen(pkt_flush_str)); } -int git_pkt_send_flush(GIT_SOCKET s) -{ - - return gitno_send(s, pkt_flush_str, strlen(pkt_flush_str), 0); -} - -static int buffer_want_with_caps(git_remote_head *head, git_transport_caps *caps, git_buf *buf) +static int buffer_want_with_caps(const git_remote_head *head, transport_smart_caps *caps, git_buf *buf) { - char capstr[20]; + git_buf str = GIT_BUF_INIT; char oid[GIT_OID_HEXSZ +1] = {0}; unsigned int len; + /* Prefer side-band-64k if the server supports both */ + if (caps->side_band) { + if (caps->side_band_64k) + git_buf_printf(&str, "%s ", GIT_CAP_SIDE_BAND_64K); + else + git_buf_printf(&str, "%s ", GIT_CAP_SIDE_BAND); + } if (caps->ofs_delta) - strncpy(capstr, GIT_CAP_OFS_DELTA, sizeof(capstr)); + git_buf_puts(&str, GIT_CAP_OFS_DELTA " "); + + if (caps->multi_ack) + git_buf_puts(&str, GIT_CAP_MULTI_ACK " "); + + if (caps->include_tag) + git_buf_puts(&str, GIT_CAP_INCLUDE_TAG " "); + + if (git_buf_oom(&str)) + return -1; len = (unsigned int) (strlen("XXXXwant ") + GIT_OID_HEXSZ + 1 /* NUL */ + - strlen(capstr) + 1 /* LF */); + git_buf_len(&str) + 1 /* LF */); git_buf_grow(buf, git_buf_len(buf) + len); - git_oid_fmt(oid, &head->oid); - return git_buf_printf(buf, "%04xwant %s%c%s\n", len, oid, 0, capstr); + git_buf_printf(buf, "%04xwant %s %s\n", len, oid, git_buf_cstr(&str)); + git_buf_free(&str); + + return git_buf_oom(buf); } /* @@ -310,28 +491,32 @@ static int buffer_want_with_caps(git_remote_head *head, git_transport_caps *caps * is overwrite the OID each time. */ -int git_pkt_buffer_wants(const git_vector *refs, git_transport_caps *caps, git_buf *buf) +int git_pkt_buffer_wants( + const git_remote_head * const *refs, + size_t count, + transport_smart_caps *caps, + git_buf *buf) { - unsigned int i = 0; - git_remote_head *head; + size_t i = 0; + const git_remote_head *head; if (caps->common) { - for (; i < refs->length; ++i) { - head = refs->contents[i]; + for (; i < count; ++i) { + head = refs[i]; if (!head->local) break; } - if (buffer_want_with_caps(refs->contents[i], caps, buf) < 0) + if (buffer_want_with_caps(refs[i], caps, buf) < 0) return -1; i++; } - for (; i < refs->length; ++i) { + for (; i < count; ++i) { char oid[GIT_OID_HEXSZ]; - head = refs->contents[i]; + head = refs[i]; if (head->local) continue; diff --git a/src/transports/smart_protocol.c b/src/transports/smart_protocol.c new file mode 100644 index 000000000..8acedeb49 --- /dev/null +++ b/src/transports/smart_protocol.c @@ -0,0 +1,856 @@ +/* + * 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 "smart.h" +#include "refs.h" +#include "repository.h" +#include "push.h" +#include "pack-objects.h" +#include "remote.h" + +#define NETWORK_XFER_THRESHOLD (100*1024) + +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; + + /* Clear existing refs in case git_remote_connect() is called again + * after git_remote_disconnect(). + */ + git_vector_clear(refs); + + do { + if (buf->offset > 0) + error = git_pkt_parse_line(&pkt, buf->data, &line_end, buf->offset); + else + error = GIT_EBUFS; + + if (error < 0 && error != GIT_EBUFS) + return -1; + + if (error == GIT_EBUFS) { + if ((recvd = gitno_recv(buf)) < 0) + return -1; + + if (recvd == 0 && !flush) { + giterr_set(GITERR_NET, "Early EOF"); + return -1; + } + + continue; + } + + gitno_consume(buf, line_end); + if (pkt->type == GIT_PKT_ERR) { + giterr_set(GITERR_NET, "Remote error: %s", ((git_pkt_err *)pkt)->error); + git__free(pkt); + return -1; + } + + if (pkt->type != GIT_PKT_FLUSH && git_vector_insert(refs, pkt) < 0) + return -1; + + if (pkt->type == GIT_PKT_FLUSH) { + flush++; + git_pkt_free(pkt); + } + } while (flush < flushes); + + return flush; +} + +int git_smart__detect_caps(git_pkt_ref *pkt, transport_smart_caps *caps) +{ + const char *ptr; + + /* No refs or capabilites, odd but not a problem */ + if (pkt == NULL || pkt->capabilities == NULL) + return 0; + + ptr = pkt->capabilities; + while (ptr != NULL && *ptr != '\0') { + if (*ptr == ' ') + ptr++; + + if (!git__prefixcmp(ptr, GIT_CAP_OFS_DELTA)) { + caps->common = caps->ofs_delta = 1; + ptr += strlen(GIT_CAP_OFS_DELTA); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_MULTI_ACK)) { + caps->common = caps->multi_ack = 1; + ptr += strlen(GIT_CAP_MULTI_ACK); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_INCLUDE_TAG)) { + caps->common = caps->include_tag = 1; + ptr += strlen(GIT_CAP_INCLUDE_TAG); + continue; + } + + /* Keep side-band check after side-band-64k */ + if (!git__prefixcmp(ptr, GIT_CAP_SIDE_BAND_64K)) { + caps->common = caps->side_band_64k = 1; + ptr += strlen(GIT_CAP_SIDE_BAND_64K); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_SIDE_BAND)) { + caps->common = caps->side_band = 1; + ptr += strlen(GIT_CAP_SIDE_BAND); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_DELETE_REFS)) { + caps->common = caps->delete_refs = 1; + ptr += strlen(GIT_CAP_DELETE_REFS); + continue; + } + + /* We don't know this capability, so skip it */ + ptr = strchr(ptr, ' '); + } + + return 0; +} + +static int recv_pkt(git_pkt **out, gitno_buffer *buf) +{ + const char *ptr = buf->data, *line_end = ptr; + git_pkt *pkt; + int pkt_type, error = 0, ret; + + do { + if (buf->offset > 0) + error = git_pkt_parse_line(&pkt, ptr, &line_end, buf->offset); + else + error = GIT_EBUFS; + + if (error == 0) + break; /* return the pkt */ + + if (error < 0 && error != GIT_EBUFS) + return -1; + + if ((ret = gitno_recv(buf)) < 0) + return -1; + } while (error); + + gitno_consume(buf, line_end); + pkt_type = pkt->type; + if (out != NULL) + *out = pkt; + else + git__free(pkt); + + return pkt_type; +} + +static int store_common(transport_smart *t) +{ + git_pkt *pkt = NULL; + gitno_buffer *buf = &t->buffer; + + do { + if (recv_pkt(&pkt, buf) < 0) + return -1; + + if (pkt->type == GIT_PKT_ACK) { + if (git_vector_insert(&t->common, pkt) < 0) + return -1; + } else { + git__free(pkt); + return 0; + } + + } while (1); + + return 0; +} + +static int fetch_setup_walk(git_revwalk **out, git_repository *repo) +{ + git_revwalk *walk; + git_strarray refs; + unsigned int i; + git_reference *ref; + + if (git_reference_list(&refs, repo, GIT_REF_LISTALL) < 0) + return -1; + + if (git_revwalk_new(&walk, repo) < 0) + return -1; + + git_revwalk_sorting(walk, GIT_SORT_TIME); + + for (i = 0; i < refs.count; ++i) { + /* No tags */ + if (!git__prefixcmp(refs.strings[i], GIT_REFS_TAGS_DIR)) + continue; + + if (git_reference_lookup(&ref, repo, refs.strings[i]) < 0) + goto on_error; + + if (git_reference_type(ref) == GIT_REF_SYMBOLIC) + continue; + if (git_revwalk_push(walk, git_reference_target(ref)) < 0) + goto on_error; + + git_reference_free(ref); + } + + git_strarray_free(&refs); + *out = walk; + return 0; + +on_error: + git_reference_free(ref); + git_strarray_free(&refs); + return -1; +} + +int git_smart__negotiate_fetch(git_transport *transport, git_repository *repo, const git_remote_head * const *refs, size_t count) +{ + transport_smart *t = (transport_smart *)transport; + gitno_buffer *buf = &t->buffer; + git_buf data = GIT_BUF_INIT; + git_revwalk *walk = NULL; + int error = -1, pkt_type; + unsigned int i; + git_oid oid; + + /* No own logic, do our thing */ + if ((error = git_pkt_buffer_wants(refs, count, &t->caps, &data)) < 0) + return error; + + if ((error = fetch_setup_walk(&walk, repo)) < 0) + goto on_error; + /* + * We don't support any kind of ACK extensions, so the negotiation + * boils down to sending what we have and listening for an ACK + * every once in a while. + */ + i = 0; + while (true) { + error = git_revwalk_next(&oid, walk); + + if (error < 0) { + if (GIT_ITEROVER == error) + break; + + goto on_error; + } + + git_pkt_buffer_have(&oid, &data); + i++; + if (i % 20 == 0) { + if (t->cancelled.val) { + giterr_set(GITERR_NET, "The fetch was cancelled by the user"); + error = GIT_EUSER; + goto on_error; + } + + git_pkt_buffer_flush(&data); + if (git_buf_oom(&data)) { + error = -1; + goto on_error; + } + + if ((error = git_smart__negotiation_step(&t->parent, data.ptr, data.size)) < 0) + goto on_error; + + git_buf_clear(&data); + if (t->caps.multi_ack) { + if ((error = store_common(t)) < 0) + goto on_error; + } else { + pkt_type = recv_pkt(NULL, buf); + + if (pkt_type == GIT_PKT_ACK) { + break; + } else if (pkt_type == GIT_PKT_NAK) { + continue; + } else if (pkt_type < 0) { + /* recv_pkt returned an error */ + error = pkt_type; + goto on_error; + } else { + giterr_set(GITERR_NET, "Unexpected pkt type"); + error = -1; + goto on_error; + } + } + } + + if (t->common.length > 0) + break; + + if (i % 20 == 0 && t->rpc) { + git_pkt_ack *pkt; + unsigned int i; + + if ((error = git_pkt_buffer_wants(refs, count, &t->caps, &data)) < 0) + goto on_error; + + git_vector_foreach(&t->common, i, pkt) { + if ((error = git_pkt_buffer_have(&pkt->oid, &data)) < 0) + goto on_error; + } + + if (git_buf_oom(&data)) { + error = -1; + goto on_error; + } + } + } + + /* Tell the other end that we're done negotiating */ + if (t->rpc && t->common.length > 0) { + git_pkt_ack *pkt; + unsigned int i; + + if ((error = git_pkt_buffer_wants(refs, count, &t->caps, &data)) < 0) + goto on_error; + + git_vector_foreach(&t->common, i, pkt) { + if ((error = git_pkt_buffer_have(&pkt->oid, &data)) < 0) + goto on_error; + } + + if (git_buf_oom(&data)) { + error = -1; + goto on_error; + } + } + + if ((error = git_pkt_buffer_done(&data)) < 0) + goto on_error; + + if (t->cancelled.val) { + giterr_set(GITERR_NET, "The fetch was cancelled by the user"); + error = GIT_EUSER; + goto on_error; + } + if ((error = git_smart__negotiation_step(&t->parent, data.ptr, data.size)) < 0) + goto on_error; + + git_buf_free(&data); + git_revwalk_free(walk); + + /* Now let's eat up whatever the server gives us */ + if (!t->caps.multi_ack) { + pkt_type = recv_pkt(NULL, buf); + + if (pkt_type < 0) { + return pkt_type; + } else if (pkt_type != GIT_PKT_ACK && pkt_type != GIT_PKT_NAK) { + giterr_set(GITERR_NET, "Unexpected pkt type"); + return -1; + } + } else { + git_pkt_ack *pkt; + do { + if ((error = recv_pkt((git_pkt **)&pkt, buf)) < 0) + return error; + + if (pkt->type == GIT_PKT_NAK || + (pkt->type == GIT_PKT_ACK && pkt->status != GIT_ACK_CONTINUE)) { + git__free(pkt); + break; + } + + git__free(pkt); + } while (1); + } + + return 0; + +on_error: + git_revwalk_free(walk); + git_buf_free(&data); + return error; +} + +static int no_sideband(transport_smart *t, struct git_odb_writepack *writepack, gitno_buffer *buf, git_transfer_progress *stats) +{ + int recvd; + + do { + if (t->cancelled.val) { + giterr_set(GITERR_NET, "The fetch was cancelled by the user"); + return GIT_EUSER; + } + + if (writepack->add(writepack, buf->data, buf->offset, stats) < 0) + return -1; + + gitno_consume_n(buf, buf->offset); + + if ((recvd = gitno_recv(buf)) < 0) + return -1; + } while(recvd > 0); + + if (writepack->commit(writepack, stats)) + return -1; + + return 0; +} + +struct network_packetsize_payload +{ + git_transfer_progress_callback callback; + void *payload; + git_transfer_progress *stats; + size_t last_fired_bytes; +}; + +static void network_packetsize(size_t received, void *payload) +{ + struct network_packetsize_payload *npp = (struct network_packetsize_payload*)payload; + + /* Accumulate bytes */ + npp->stats->received_bytes += received; + + /* Fire notification if the threshold is reached */ + if ((npp->stats->received_bytes - npp->last_fired_bytes) > NETWORK_XFER_THRESHOLD) { + npp->last_fired_bytes = npp->stats->received_bytes; + npp->callback(npp->stats, npp->payload); + } +} + +int git_smart__download_pack( + git_transport *transport, + git_repository *repo, + git_transfer_progress *stats, + git_transfer_progress_callback progress_cb, + void *progress_payload) +{ + transport_smart *t = (transport_smart *)transport; + gitno_buffer *buf = &t->buffer; + git_odb *odb; + struct git_odb_writepack *writepack = NULL; + int error = -1; + struct network_packetsize_payload npp = {0}; + + memset(stats, 0, sizeof(git_transfer_progress)); + + if (progress_cb) { + npp.callback = progress_cb; + npp.payload = progress_payload; + npp.stats = stats; + t->packetsize_cb = &network_packetsize; + t->packetsize_payload = &npp; + + /* We might have something in the buffer already from negotiate_fetch */ + if (t->buffer.offset > 0) + t->packetsize_cb(t->buffer.offset, t->packetsize_payload); + } + + if ((error = git_repository_odb__weakptr(&odb, repo)) < 0 || + ((error = git_odb_write_pack(&writepack, odb, progress_cb, progress_payload)) < 0)) + goto on_error; + + /* + * If the remote doesn't support the side-band, we can feed + * the data directly to the pack writer. Otherwise, we need to + * check which one belongs there. + */ + if (!t->caps.side_band && !t->caps.side_band_64k) { + if (no_sideband(t, writepack, buf, stats) < 0) + goto on_error; + + goto on_success; + } + + do { + git_pkt *pkt; + + if (t->cancelled.val) { + giterr_set(GITERR_NET, "The fetch was cancelled by the user"); + error = GIT_EUSER; + goto on_error; + } + + if (recv_pkt(&pkt, buf) < 0) + goto on_error; + + if (pkt->type == GIT_PKT_PROGRESS) { + if (t->progress_cb) { + git_pkt_progress *p = (git_pkt_progress *) pkt; + t->progress_cb(p->data, p->len, t->message_cb_payload); + } + git__free(pkt); + } else if (pkt->type == GIT_PKT_DATA) { + git_pkt_data *p = (git_pkt_data *) pkt; + error = writepack->add(writepack, p->data, p->len, stats); + + git__free(pkt); + if (error < 0) + goto on_error; + } else if (pkt->type == GIT_PKT_FLUSH) { + /* A flush indicates the end of the packfile */ + git__free(pkt); + break; + } + } while (1); + + if (writepack->commit(writepack, stats) < 0) + goto on_error; + +on_success: + error = 0; + +on_error: + if (writepack) + writepack->free(writepack); + + /* Trailing execution of progress_cb, if necessary */ + if (npp.callback && npp.stats->received_bytes > npp.last_fired_bytes) + npp.callback(npp.stats, npp.payload); + + return error; +} + +static int gen_pktline(git_buf *buf, git_push *push) +{ + push_spec *spec; + size_t i, len; + char old_id[41], new_id[41]; + + old_id[40] = '\0'; new_id[40] = '\0'; + + git_vector_foreach(&push->specs, i, spec) { + len = 2*GIT_OID_HEXSZ + 7 + strlen(spec->rref); + + if (i == 0) { + ++len; /* '\0' */ + if (push->report_status) + len += strlen(GIT_CAP_REPORT_STATUS) + 1; + len += strlen(GIT_CAP_SIDE_BAND_64K) + 1; + } + + git_oid_fmt(old_id, &spec->roid); + git_oid_fmt(new_id, &spec->loid); + + git_buf_printf(buf, "%04"PRIxZ"%s %s %s", len, old_id, new_id, spec->rref); + + if (i == 0) { + git_buf_putc(buf, '\0'); + /* Core git always starts their capabilities string with a space */ + if (push->report_status) { + git_buf_putc(buf, ' '); + git_buf_printf(buf, GIT_CAP_REPORT_STATUS); + } + git_buf_putc(buf, ' '); + git_buf_printf(buf, GIT_CAP_SIDE_BAND_64K); + } + + git_buf_putc(buf, '\n'); + } + + git_buf_puts(buf, "0000"); + return git_buf_oom(buf) ? -1 : 0; +} + +static int add_push_report_pkt(git_push *push, git_pkt *pkt) +{ + push_status *status; + + switch (pkt->type) { + case GIT_PKT_OK: + status = git__malloc(sizeof(push_status)); + GITERR_CHECK_ALLOC(status); + status->msg = NULL; + status->ref = git__strdup(((git_pkt_ok *)pkt)->ref); + if (!status->ref || + git_vector_insert(&push->status, status) < 0) { + git_push_status_free(status); + return -1; + } + break; + case GIT_PKT_NG: + status = git__calloc(sizeof(push_status), 1); + GITERR_CHECK_ALLOC(status); + status->ref = git__strdup(((git_pkt_ng *)pkt)->ref); + status->msg = git__strdup(((git_pkt_ng *)pkt)->msg); + if (!status->ref || !status->msg || + git_vector_insert(&push->status, status) < 0) { + git_push_status_free(status); + return -1; + } + break; + case GIT_PKT_UNPACK: + push->unpack_ok = ((git_pkt_unpack *)pkt)->unpack_ok; + break; + case GIT_PKT_FLUSH: + return GIT_ITEROVER; + default: + giterr_set(GITERR_NET, "report-status: protocol error"); + return -1; + } + + return 0; +} + +static int add_push_report_sideband_pkt(git_push *push, git_pkt_data *data_pkt) +{ + git_pkt *pkt; + const char *line = data_pkt->data, *line_end; + size_t line_len = data_pkt->len; + int error; + + while (line_len > 0) { + error = git_pkt_parse_line(&pkt, line, &line_end, line_len); + + if (error < 0) + return error; + + /* Advance in the buffer */ + line_len -= (line_end - line); + line = line_end; + + error = add_push_report_pkt(push, pkt); + + git_pkt_free(pkt); + + if (error < 0 && error != GIT_ITEROVER) + return error; + } + + return 0; +} + +static int parse_report(gitno_buffer *buf, git_push *push) +{ + git_pkt *pkt; + const char *line_end; + int error, recvd; + + for (;;) { + if (buf->offset > 0) + error = git_pkt_parse_line(&pkt, buf->data, + &line_end, buf->offset); + else + error = GIT_EBUFS; + + if (error < 0 && error != GIT_EBUFS) + return -1; + + if (error == GIT_EBUFS) { + if ((recvd = gitno_recv(buf)) < 0) + return -1; + + if (recvd == 0) { + giterr_set(GITERR_NET, "Early EOF"); + return -1; + } + continue; + } + + gitno_consume(buf, line_end); + + error = 0; + + switch (pkt->type) { + case GIT_PKT_DATA: + /* This is a sideband packet which contains other packets */ + error = add_push_report_sideband_pkt(push, (git_pkt_data *)pkt); + break; + case GIT_PKT_ERR: + giterr_set(GITERR_NET, "report-status: Error reported: %s", + ((git_pkt_err *)pkt)->error); + error = -1; + break; + case GIT_PKT_PROGRESS: + break; + default: + error = add_push_report_pkt(push, pkt); + break; + } + + git_pkt_free(pkt); + + /* add_push_report_pkt returns GIT_ITEROVER when it receives a flush */ + if (error == GIT_ITEROVER) + return 0; + + if (error < 0) + return error; + } +} + +static int add_ref_from_push_spec(git_vector *refs, push_spec *push_spec) +{ + git_pkt_ref *added = git__calloc(1, sizeof(git_pkt_ref)); + GITERR_CHECK_ALLOC(added); + + added->type = GIT_PKT_REF; + git_oid_cpy(&added->head.oid, &push_spec->loid); + added->head.name = git__strdup(push_spec->rref); + + if (!added->head.name || + git_vector_insert(refs, added) < 0) { + git_pkt_free((git_pkt *)added); + return -1; + } + + return 0; +} + +static int update_refs_from_report( + git_vector *refs, + git_vector *push_specs, + git_vector *push_report) +{ + git_pkt_ref *ref; + push_spec *push_spec; + push_status *push_status; + size_t i, j, refs_len; + int cmp; + + /* For each push spec we sent to the server, we should have + * gotten back a status packet in the push report */ + if (push_specs->length != push_report->length) { + giterr_set(GITERR_NET, "report-status: protocol error"); + return -1; + } + + /* We require that push_specs be sorted with push_spec_rref_cmp, + * and that push_report be sorted with push_status_ref_cmp */ + git_vector_sort(push_specs); + git_vector_sort(push_report); + + git_vector_foreach(push_specs, i, push_spec) { + push_status = git_vector_get(push_report, i); + + /* For each push spec we sent to the server, we should have + * gotten back a status packet in the push report which matches */ + if (strcmp(push_spec->rref, push_status->ref)) { + giterr_set(GITERR_NET, "report-status: protocol error"); + return -1; + } + } + + /* We require that refs be sorted with ref_name_cmp */ + git_vector_sort(refs); + i = j = 0; + refs_len = refs->length; + + /* Merge join push_specs with refs */ + while (i < push_specs->length && j < refs_len) { + push_spec = git_vector_get(push_specs, i); + push_status = git_vector_get(push_report, i); + ref = git_vector_get(refs, j); + + cmp = strcmp(push_spec->rref, ref->head.name); + + /* Iterate appropriately */ + if (cmp <= 0) i++; + if (cmp >= 0) j++; + + /* Add case */ + if (cmp < 0 && + !push_status->msg && + add_ref_from_push_spec(refs, push_spec) < 0) + return -1; + + /* Update case, delete case */ + if (cmp == 0 && + !push_status->msg) + git_oid_cpy(&ref->head.oid, &push_spec->loid); + } + + for (; i < push_specs->length; i++) { + push_spec = git_vector_get(push_specs, i); + push_status = git_vector_get(push_report, i); + + /* Add case */ + if (!push_status->msg && + add_ref_from_push_spec(refs, push_spec) < 0) + return -1; + } + + /* Remove any refs which we updated to have a zero OID. */ + git_vector_rforeach(refs, i, ref) { + if (git_oid_iszero(&ref->head.oid)) { + git_vector_remove(refs, i); + git_pkt_free((git_pkt *)ref); + } + } + + git_vector_sort(refs); + + return 0; +} + +static int stream_thunk(void *buf, size_t size, void *data) +{ + git_smart_subtransport_stream *s = (git_smart_subtransport_stream *)data; + + return s->write(s, (const char *)buf, size); +} + +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; + +#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) { + git_oid_fmt(hex, &head->oid); + fprintf(stderr, "%s (%s)\n", hex, head->name); + } + + git_vector_foreach(&push->specs, i, spec) { + git_oid_fmt(hex, &spec->roid); + fprintf(stderr, "%s (%s) -> ", hex, spec->lref); + git_oid_fmt(hex, &spec->loid); + fprintf(stderr, "%s (%s)\n", hex, spec->rref ? + spec->rref : spec->lref); + } +} +#endif + + 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) + goto on_error; + + /* If we sent nothing or the server doesn't support report-status, then + * we consider the pack to have been unpacked successfully */ + if (!push->specs.length || !push->report_status) + push->unpack_ok = 1; + else if (parse_report(&t->buffer, push) < 0) + goto on_error; + + if (push->status.length && + update_refs_from_report(&t->refs, &push->specs, &push->status) < 0) + goto on_error; + + error = 0; + +on_error: + git_buf_free(&pktline); + + return error; +} diff --git a/src/transports/winhttp.c b/src/transports/winhttp.c new file mode 100644 index 000000000..e502001cb --- /dev/null +++ b/src/transports/winhttp.c @@ -0,0 +1,1136 @@ +/* + * 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. + */ + +#ifdef GIT_WINHTTP + +#include "git2.h" +#include "git2/transport.h" +#include "buffer.h" +#include "posix.h" +#include "netops.h" +#include "smart.h" +#include "remote.h" +#include "repository.h" + +#include <winhttp.h> +#pragma comment(lib, "winhttp") + +/* For UuidCreate */ +#pragma comment(lib, "rpcrt4") + +#define WIDEN2(s) L ## s +#define WIDEN(s) WIDEN2(s) + +#define MAX_CONTENT_TYPE_LEN 100 +#define WINHTTP_OPTION_PEERDIST_EXTENSION_STATE 109 +#define CACHED_POST_BODY_BUF_SIZE 4096 +#define UUID_LENGTH_CCH 32 + +static const char *prefix_http = "http://"; +static const char *prefix_https = "https://"; +static const char *upload_pack_service = "upload-pack"; +static const char *upload_pack_ls_service_url = "/info/refs?service=git-upload-pack"; +static const char *upload_pack_service_url = "/git-upload-pack"; +static const char *receive_pack_service = "receive-pack"; +static const char *receive_pack_ls_service_url = "/info/refs?service=git-receive-pack"; +static const char *receive_pack_service_url = "/git-receive-pack"; +static const wchar_t *get_verb = L"GET"; +static const wchar_t *post_verb = L"POST"; +static const wchar_t *pragma_nocache = L"Pragma: no-cache"; +static const wchar_t *transfer_encoding = L"Transfer-Encoding: chunked"; +static const int no_check_cert_flags = SECURITY_FLAG_IGNORE_CERT_CN_INVALID | + SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | + SECURITY_FLAG_IGNORE_UNKNOWN_CA; + +#define OWNING_SUBTRANSPORT(s) ((winhttp_subtransport *)(s)->parent.subtransport) + +typedef enum { + GIT_WINHTTP_AUTH_BASIC = 1, +} winhttp_authmechanism_t; + +typedef struct { + git_smart_subtransport_stream parent; + const char *service; + const char *service_url; + const wchar_t *verb; + HINTERNET request; + wchar_t *request_uri; + char *chunk_buffer; + unsigned chunk_buffer_len; + HANDLE post_body; + DWORD post_body_len; + unsigned sent_request : 1, + received_response : 1, + chunked : 1; +} winhttp_stream; + +typedef struct { + git_smart_subtransport parent; + transport_smart *owner; + const char *path; + char *host; + char *port; + char *user_from_url; + char *pass_from_url; + git_cred *cred; + git_cred *url_cred; + int auth_mechanism; + HINTERNET session; + HINTERNET connection; + unsigned use_ssl : 1; +} winhttp_subtransport; + +static int apply_basic_credential(HINTERNET request, git_cred *cred) +{ + git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred; + git_buf buf = GIT_BUF_INIT, raw = GIT_BUF_INIT; + wchar_t *wide = NULL; + int error = -1, wide_len = 0; + + git_buf_printf(&raw, "%s:%s", c->username, c->password); + + if (git_buf_oom(&raw) || + git_buf_puts(&buf, "Authorization: Basic ") < 0 || + git_buf_put_base64(&buf, git_buf_cstr(&raw), raw.size) < 0) + goto on_error; + + wide_len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + git_buf_cstr(&buf), -1, NULL, 0); + + if (!wide_len) { + giterr_set(GITERR_OS, "Failed to measure string for wide conversion"); + goto on_error; + } + + wide = git__malloc(wide_len * sizeof(wchar_t)); + + if (!wide) + goto on_error; + + if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + git_buf_cstr(&buf), -1, wide, wide_len)) { + giterr_set(GITERR_OS, "Failed to convert string to wide form"); + goto on_error; + } + + if (!WinHttpAddRequestHeaders(request, wide, (ULONG) -1L, WINHTTP_ADDREQ_FLAG_ADD)) { + giterr_set(GITERR_OS, "Failed to add a header to the request"); + goto on_error; + } + + error = 0; + +on_error: + /* We were dealing with plaintext passwords, so clean up after ourselves a bit. */ + if (wide) + memset(wide, 0x0, wide_len * sizeof(wchar_t)); + + if (buf.size) + memset(buf.ptr, 0x0, buf.size); + + if (raw.size) + memset(raw.ptr, 0x0, raw.size); + + git__free(wide); + git_buf_free(&buf); + git_buf_free(&raw); + return error; +} + +static int winhttp_stream_connect(winhttp_stream *s) +{ + winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); + git_buf buf = GIT_BUF_INIT; + char *proxy_url = NULL; + wchar_t ct[MAX_CONTENT_TYPE_LEN]; + wchar_t *types[] = { L"*/*", NULL }; + BOOL peerdist = FALSE; + int error = -1, wide_len; + + /* Prepare URL */ + git_buf_printf(&buf, "%s%s", t->path, s->service_url); + + if (git_buf_oom(&buf)) + return -1; + + /* Convert URL to wide characters */ + wide_len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + git_buf_cstr(&buf), -1, NULL, 0); + + if (!wide_len) { + giterr_set(GITERR_OS, "Failed to measure string for wide conversion"); + goto on_error; + } + + s->request_uri = git__malloc(wide_len * sizeof(wchar_t)); + + if (!s->request_uri) + goto on_error; + + if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + git_buf_cstr(&buf), -1, s->request_uri, wide_len)) { + giterr_set(GITERR_OS, "Failed to convert string to wide form"); + goto on_error; + } + + /* Establish request */ + s->request = WinHttpOpenRequest( + t->connection, + s->verb, + s->request_uri, + NULL, + WINHTTP_NO_REFERER, + types, + t->use_ssl ? WINHTTP_FLAG_SECURE : 0); + + if (!s->request) { + giterr_set(GITERR_OS, "Failed to open request"); + goto on_error; + } + + /* Set proxy if necessary */ + if (git_remote__get_http_proxy(t->owner->owner, t->use_ssl, &proxy_url) < 0) + goto on_error; + + if (proxy_url) { + WINHTTP_PROXY_INFO proxy_info; + wchar_t *proxy_wide; + + /* Convert URL to wide characters */ + wide_len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + proxy_url, -1, NULL, 0); + + if (!wide_len) { + giterr_set(GITERR_OS, "Failed to measure string for wide conversion"); + goto on_error; + } + + proxy_wide = git__malloc(wide_len * sizeof(wchar_t)); + + if (!proxy_wide) + goto on_error; + + if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + proxy_url, -1, proxy_wide, wide_len)) { + giterr_set(GITERR_OS, "Failed to convert string to wide form"); + git__free(proxy_wide); + goto on_error; + } + + /* Strip any trailing forward slash on the proxy URL; + * WinHTTP doesn't like it if one is present */ + if (wide_len > 1 && L'/' == proxy_wide[wide_len - 2]) + proxy_wide[wide_len - 2] = L'\0'; + + proxy_info.dwAccessType = WINHTTP_ACCESS_TYPE_NAMED_PROXY; + proxy_info.lpszProxy = proxy_wide; + proxy_info.lpszProxyBypass = NULL; + + if (!WinHttpSetOption(s->request, + WINHTTP_OPTION_PROXY, + &proxy_info, + sizeof(WINHTTP_PROXY_INFO))) { + giterr_set(GITERR_OS, "Failed to set proxy"); + git__free(proxy_wide); + goto on_error; + } + + git__free(proxy_wide); + } + + /* Strip unwanted headers (X-P2P-PeerDist, X-P2P-PeerDistEx) that WinHTTP + * adds itself. This option may not be supported by the underlying + * platform, so we do not error-check it */ + WinHttpSetOption(s->request, + WINHTTP_OPTION_PEERDIST_EXTENSION_STATE, + &peerdist, + sizeof(peerdist)); + + /* Send Pragma: no-cache header */ + if (!WinHttpAddRequestHeaders(s->request, pragma_nocache, (ULONG) -1L, WINHTTP_ADDREQ_FLAG_ADD)) { + giterr_set(GITERR_OS, "Failed to add a header to the request"); + goto on_error; + } + + /* Send Content-Type header -- only necessary on a POST */ + if (post_verb == s->verb) { + git_buf_clear(&buf); + if (git_buf_printf(&buf, "Content-Type: application/x-git-%s-request", s->service) < 0) + goto on_error; + + git__utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_buf_cstr(&buf)); + + if (!WinHttpAddRequestHeaders(s->request, ct, (ULONG) -1L, WINHTTP_ADDREQ_FLAG_ADD)) { + giterr_set(GITERR_OS, "Failed to add a header to the request"); + goto on_error; + } + } + + /* If requested, disable certificate validation */ + if (t->use_ssl) { + int flags; + + if (t->owner->parent.read_flags(&t->owner->parent, &flags) < 0) + goto on_error; + + if ((GIT_TRANSPORTFLAGS_NO_CHECK_CERT & flags) && + !WinHttpSetOption(s->request, WINHTTP_OPTION_SECURITY_FLAGS, + (LPVOID)&no_check_cert_flags, sizeof(no_check_cert_flags))) { + giterr_set(GITERR_OS, "Failed to set options to ignore cert errors"); + goto on_error; + } + } + + /* If we have a credential on the subtransport, apply it to the request */ + if (t->cred && + t->cred->credtype == GIT_CREDTYPE_USERPASS_PLAINTEXT && + t->auth_mechanism == GIT_WINHTTP_AUTH_BASIC && + apply_basic_credential(s->request, t->cred) < 0) + goto on_error; + + /* If no other credentials have been applied and the URL has username and + * password, use those */ + if (!t->cred && t->user_from_url && t->pass_from_url) { + if (!t->url_cred && + git_cred_userpass_plaintext_new(&t->url_cred, t->user_from_url, t->pass_from_url) < 0) + goto on_error; + if (apply_basic_credential(s->request, t->url_cred) < 0) + goto on_error; + } + + /* We've done everything up to calling WinHttpSendRequest. */ + + error = 0; + +on_error: + git__free(proxy_url); + git_buf_free(&buf); + return error; +} + +static int parse_unauthorized_response( + HINTERNET request, + int *allowed_types, + int *auth_mechanism) +{ + DWORD supported, first, target; + + *allowed_types = 0; + *auth_mechanism = 0; + + /* WinHttpQueryHeaders() must be called before WinHttpQueryAuthSchemes(). + * We can assume this was already done, since we know we are unauthorized. + */ + if (!WinHttpQueryAuthSchemes(request, &supported, &first, &target)) { + giterr_set(GITERR_OS, "Failed to parse supported auth schemes"); + return -1; + } + + if (WINHTTP_AUTH_SCHEME_BASIC & supported) { + *allowed_types |= GIT_CREDTYPE_USERPASS_PLAINTEXT; + *auth_mechanism = GIT_WINHTTP_AUTH_BASIC; + } + + return 0; +} + +static int write_chunk(HINTERNET request, const char *buffer, size_t len) +{ + DWORD bytes_written; + git_buf buf = GIT_BUF_INIT; + + /* Chunk header */ + git_buf_printf(&buf, "%X\r\n", len); + + if (git_buf_oom(&buf)) + return -1; + + if (!WinHttpWriteData(request, + git_buf_cstr(&buf), (DWORD)git_buf_len(&buf), + &bytes_written)) { + git_buf_free(&buf); + giterr_set(GITERR_OS, "Failed to write chunk header"); + return -1; + } + + git_buf_free(&buf); + + /* Chunk body */ + if (!WinHttpWriteData(request, + buffer, (DWORD)len, + &bytes_written)) { + giterr_set(GITERR_OS, "Failed to write chunk"); + return -1; + } + + /* Chunk footer */ + if (!WinHttpWriteData(request, + "\r\n", 2, + &bytes_written)) { + giterr_set(GITERR_OS, "Failed to write chunk footer"); + return -1; + } + + return 0; +} + +static int winhttp_stream_read( + git_smart_subtransport_stream *stream, + char *buffer, + size_t buf_size, + size_t *bytes_read) +{ + winhttp_stream *s = (winhttp_stream *)stream; + winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); + DWORD dw_bytes_read; + char replay_count = 0; + +replay: + /* Enforce a reasonable cap on the number of replays */ + if (++replay_count >= 7) { + giterr_set(GITERR_NET, "Too many redirects or authentication replays"); + return -1; + } + + /* Connect if necessary */ + if (!s->request && winhttp_stream_connect(s) < 0) + return -1; + + if (!s->received_response) { + DWORD status_code, status_code_length, content_type_length, bytes_written; + char expected_content_type_8[MAX_CONTENT_TYPE_LEN]; + wchar_t expected_content_type[MAX_CONTENT_TYPE_LEN], content_type[MAX_CONTENT_TYPE_LEN]; + + if (!s->sent_request) { + if (!WinHttpSendRequest(s->request, + WINHTTP_NO_ADDITIONAL_HEADERS, 0, + WINHTTP_NO_REQUEST_DATA, 0, + s->post_body_len, 0)) { + giterr_set(GITERR_OS, "Failed to send request"); + return -1; + } + + s->sent_request = 1; + } + + if (s->chunked) { + assert(s->verb == post_verb); + + /* Flush, if necessary */ + if (s->chunk_buffer_len > 0 && + write_chunk(s->request, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + + /* Write the final chunk. */ + if (!WinHttpWriteData(s->request, + "0\r\n\r\n", 5, + &bytes_written)) { + giterr_set(GITERR_OS, "Failed to write final chunk"); + return -1; + } + } + else if (s->post_body) { + char *buffer; + DWORD len = s->post_body_len, bytes_read; + + if (INVALID_SET_FILE_POINTER == SetFilePointer(s->post_body, + 0, 0, FILE_BEGIN) && + NO_ERROR != GetLastError()) { + giterr_set(GITERR_OS, "Failed to reset file pointer"); + return -1; + } + + buffer = git__malloc(CACHED_POST_BODY_BUF_SIZE); + + while (len > 0) { + DWORD bytes_written; + + if (!ReadFile(s->post_body, buffer, + min(CACHED_POST_BODY_BUF_SIZE, len), + &bytes_read, NULL) || + !bytes_read) { + git__free(buffer); + giterr_set(GITERR_OS, "Failed to read from temp file"); + return -1; + } + + if (!WinHttpWriteData(s->request, buffer, + bytes_read, &bytes_written)) { + git__free(buffer); + giterr_set(GITERR_OS, "Failed to write data"); + return -1; + } + + len -= bytes_read; + assert(bytes_read == bytes_written); + } + + git__free(buffer); + + /* Eagerly close the temp file */ + CloseHandle(s->post_body); + s->post_body = NULL; + } + + if (!WinHttpReceiveResponse(s->request, 0)) { + giterr_set(GITERR_OS, "Failed to receive response"); + return -1; + } + + /* Verify that we got a 200 back */ + status_code_length = sizeof(status_code); + + if (!WinHttpQueryHeaders(s->request, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, + &status_code, &status_code_length, + WINHTTP_NO_HEADER_INDEX)) { + giterr_set(GITERR_OS, "Failed to retrieve status code"); + return -1; + } + + /* The implementation of WinHTTP prior to Windows 7 will not + * redirect to an identical URI. Some Git hosters use self-redirects + * as part of their DoS mitigation strategy. Check first to see if we + * have a redirect status code, and that we haven't already streamed + * a post body. (We can't replay a streamed POST.) */ + if (!s->chunked && + (HTTP_STATUS_MOVED == status_code || + HTTP_STATUS_REDIRECT == status_code || + (HTTP_STATUS_REDIRECT_METHOD == status_code && + get_verb == s->verb) || + HTTP_STATUS_REDIRECT_KEEP_VERB == status_code)) { + + /* Check for Windows 7. This workaround is only necessary on + * Windows Vista and earlier. Windows 7 is version 6.1. */ + if (!git_has_win32_version(6, 1)) { + wchar_t *location; + DWORD location_length; + int redirect_cmp; + + /* OK, fetch the Location header from the redirect. */ + if (WinHttpQueryHeaders(s->request, + WINHTTP_QUERY_LOCATION, + WINHTTP_HEADER_NAME_BY_INDEX, + WINHTTP_NO_OUTPUT_BUFFER, + &location_length, + WINHTTP_NO_HEADER_INDEX) || + GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + giterr_set(GITERR_OS, "Failed to read Location header"); + return -1; + } + + location = git__malloc(location_length); + GITERR_CHECK_ALLOC(location); + + if (!WinHttpQueryHeaders(s->request, + WINHTTP_QUERY_LOCATION, + WINHTTP_HEADER_NAME_BY_INDEX, + location, + &location_length, + WINHTTP_NO_HEADER_INDEX)) { + giterr_set(GITERR_OS, "Failed to read Location header"); + git__free(location); + return -1; + } + + /* Compare the Location header with the request URI */ + redirect_cmp = wcscmp(location, s->request_uri); + git__free(location); + + if (!redirect_cmp) { + /* Replay the request */ + WinHttpCloseHandle(s->request); + s->request = NULL; + s->sent_request = 0; + + goto replay; + } + } + } + + /* Handle authentication failures */ + if (HTTP_STATUS_DENIED == status_code && + get_verb == s->verb && t->owner->cred_acquire_cb) { + int allowed_types; + + if (parse_unauthorized_response(s->request, &allowed_types, &t->auth_mechanism) < 0) + return -1; + + if (allowed_types && + (!t->cred || 0 == (t->cred->credtype & allowed_types))) { + + if (t->owner->cred_acquire_cb(&t->cred, t->owner->url, t->user_from_url, allowed_types, t->owner->cred_acquire_payload) < 0) + return -1; + + assert(t->cred); + + WinHttpCloseHandle(s->request); + s->request = NULL; + s->sent_request = 0; + + /* Successfully acquired a credential */ + goto replay; + } + } + + if (HTTP_STATUS_OK != status_code) { + giterr_set(GITERR_NET, "Request failed with status code: %d", status_code); + return -1; + } + + /* Verify that we got the correct content-type back */ + if (post_verb == s->verb) + snprintf(expected_content_type_8, MAX_CONTENT_TYPE_LEN, "application/x-git-%s-result", s->service); + else + snprintf(expected_content_type_8, MAX_CONTENT_TYPE_LEN, "application/x-git-%s-advertisement", s->service); + + git__utf8_to_16(expected_content_type, MAX_CONTENT_TYPE_LEN, expected_content_type_8); + content_type_length = sizeof(content_type); + + if (!WinHttpQueryHeaders(s->request, + WINHTTP_QUERY_CONTENT_TYPE, + WINHTTP_HEADER_NAME_BY_INDEX, + &content_type, &content_type_length, + WINHTTP_NO_HEADER_INDEX)) { + giterr_set(GITERR_OS, "Failed to retrieve response content-type"); + return -1; + } + + if (wcscmp(expected_content_type, content_type)) { + giterr_set(GITERR_NET, "Received unexpected content-type"); + return -1; + } + + s->received_response = 1; + } + + if (!WinHttpReadData(s->request, + (LPVOID)buffer, + (DWORD)buf_size, + &dw_bytes_read)) + { + giterr_set(GITERR_OS, "Failed to read data"); + return -1; + } + + *bytes_read = dw_bytes_read; + + return 0; +} + +static int winhttp_stream_write_single( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) +{ + winhttp_stream *s = (winhttp_stream *)stream; + winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); + DWORD bytes_written; + + if (!s->request && winhttp_stream_connect(s) < 0) + return -1; + + /* This implementation of write permits only a single call. */ + if (s->sent_request) { + giterr_set(GITERR_NET, "Subtransport configured for only one write"); + return -1; + } + + if (!WinHttpSendRequest(s->request, + WINHTTP_NO_ADDITIONAL_HEADERS, 0, + WINHTTP_NO_REQUEST_DATA, 0, + (DWORD)len, 0)) { + giterr_set(GITERR_OS, "Failed to send request"); + return -1; + } + + s->sent_request = 1; + + if (!WinHttpWriteData(s->request, + (LPCVOID)buffer, + (DWORD)len, + &bytes_written)) { + giterr_set(GITERR_OS, "Failed to write data"); + return -1; + } + + assert((DWORD)len == bytes_written); + + return 0; +} + +static int put_uuid_string(LPWSTR buffer, size_t buffer_len_cch) +{ + UUID uuid; + RPC_STATUS status = UuidCreate(&uuid); + HRESULT result; + + if (RPC_S_OK != status && + RPC_S_UUID_LOCAL_ONLY != status && + RPC_S_UUID_NO_ADDRESS != status) { + giterr_set(GITERR_NET, "Unable to generate name for temp file"); + return -1; + } + + if (buffer_len_cch < UUID_LENGTH_CCH + 1) { + giterr_set(GITERR_NET, "Buffer too small for name of temp file"); + return -1; + } + + result = StringCbPrintfW( + buffer, buffer_len_cch, + L"%08x%04x%04x%02x%02x%02x%02x%02x%02x%02x%02x", + uuid.Data1, uuid.Data2, uuid.Data3, + uuid.Data4[0], uuid.Data4[1], uuid.Data4[2], uuid.Data4[3], + uuid.Data4[4], uuid.Data4[5], uuid.Data4[6], uuid.Data4[7]); + + if (FAILED(result)) { + giterr_set(GITERR_OS, "Unable to generate name for temp file"); + return -1; + } + + return 0; +} + +static int get_temp_file(LPWSTR buffer, DWORD buffer_len_cch) +{ + size_t len; + + if (!GetTempPathW(buffer_len_cch, buffer)) { + giterr_set(GITERR_OS, "Failed to get temp path"); + return -1; + } + + len = wcslen(buffer); + + if (buffer[len - 1] != '\\' && len < buffer_len_cch) + buffer[len++] = '\\'; + + if (put_uuid_string(&buffer[len], (size_t)buffer_len_cch - len) < 0) + return -1; + + return 0; +} + +static int winhttp_stream_write_buffered( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) +{ + winhttp_stream *s = (winhttp_stream *)stream; + winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); + DWORD bytes_written; + + if (!s->request && winhttp_stream_connect(s) < 0) + return -1; + + /* Buffer the payload, using a temporary file so we delegate + * memory management of the data to the operating system. */ + if (!s->post_body) { + wchar_t temp_path[MAX_PATH + 1]; + + if (get_temp_file(temp_path, MAX_PATH + 1) < 0) + return -1; + + s->post_body = CreateFileW(temp_path, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_DELETE, NULL, + CREATE_NEW, + FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE | FILE_FLAG_SEQUENTIAL_SCAN, + NULL); + + if (INVALID_HANDLE_VALUE == s->post_body) { + s->post_body = NULL; + giterr_set(GITERR_OS, "Failed to create temporary file"); + return -1; + } + } + + if (!WriteFile(s->post_body, buffer, (DWORD)len, &bytes_written, NULL)) { + giterr_set(GITERR_OS, "Failed to write to temporary file"); + return -1; + } + + assert((DWORD)len == bytes_written); + + s->post_body_len += bytes_written; + + return 0; +} + +static int winhttp_stream_write_chunked( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) +{ + winhttp_stream *s = (winhttp_stream *)stream; + winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); + + if (!s->request && winhttp_stream_connect(s) < 0) + return -1; + + if (!s->sent_request) { + /* Send Transfer-Encoding: chunked header */ + if (!WinHttpAddRequestHeaders(s->request, + transfer_encoding, (ULONG) -1L, + WINHTTP_ADDREQ_FLAG_ADD)) { + giterr_set(GITERR_OS, "Failed to add a header to the request"); + return -1; + } + + if (!WinHttpSendRequest(s->request, + WINHTTP_NO_ADDITIONAL_HEADERS, 0, + WINHTTP_NO_REQUEST_DATA, 0, + WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH, 0)) { + giterr_set(GITERR_OS, "Failed to send request"); + return -1; + } + + s->sent_request = 1; + } + + if (len > CACHED_POST_BODY_BUF_SIZE) { + /* Flush, if necessary */ + if (s->chunk_buffer_len > 0) { + if (write_chunk(s->request, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + } + + /* Write chunk directly */ + if (write_chunk(s->request, buffer, len) < 0) + return -1; + } + else { + /* Append as much to the buffer as we can */ + int count = min(CACHED_POST_BODY_BUF_SIZE - s->chunk_buffer_len, (int)len); + + if (!s->chunk_buffer) + s->chunk_buffer = git__malloc(CACHED_POST_BODY_BUF_SIZE); + + memcpy(s->chunk_buffer + s->chunk_buffer_len, buffer, count); + s->chunk_buffer_len += count; + buffer += count; + len -= count; + + /* Is the buffer full? If so, then flush */ + if (CACHED_POST_BODY_BUF_SIZE == s->chunk_buffer_len) { + if (write_chunk(s->request, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + + /* Is there any remaining data from the source? */ + if (len > 0) { + memcpy(s->chunk_buffer, buffer, len); + s->chunk_buffer_len = (unsigned int)len; + } + } + } + + return 0; +} + +static void winhttp_stream_free(git_smart_subtransport_stream *stream) +{ + winhttp_stream *s = (winhttp_stream *)stream; + + if (s->chunk_buffer) { + git__free(s->chunk_buffer); + s->chunk_buffer = NULL; + } + + if (s->post_body) { + CloseHandle(s->post_body); + s->post_body = NULL; + } + + if (s->request_uri) { + git__free(s->request_uri); + s->request_uri = NULL; + } + + if (s->request) { + WinHttpCloseHandle(s->request); + s->request = NULL; + } + + git__free(s); +} + +static int winhttp_stream_alloc(winhttp_subtransport *t, winhttp_stream **stream) +{ + winhttp_stream *s; + + if (!stream) + return -1; + + s = git__calloc(sizeof(winhttp_stream), 1); + GITERR_CHECK_ALLOC(s); + + s->parent.subtransport = &t->parent; + s->parent.read = winhttp_stream_read; + s->parent.write = winhttp_stream_write_single; + s->parent.free = winhttp_stream_free; + + *stream = s; + + return 0; +} + +static int winhttp_connect( + winhttp_subtransport *t, + const char *url) +{ + 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; + int ret; + + if (!git__prefixcmp(url, prefix_http)) { + url = url + strlen(prefix_http); + default_port = "80"; + } + + if (!git__prefixcmp(url, prefix_https)) { + url += strlen(prefix_https); + default_port = "443"; + t->use_ssl = 1; + } + + if ((ret = gitno_extract_url_parts(&t->host, &t->port, &t->user_from_url, + &t->pass_from_url, url, default_port)) < 0) + return ret; + + t->path = strchr(url, '/'); + + /* Prepare port */ + if (git__strtol32(&port, t->port, NULL, 10) < 0) + return -1; + + /* Prepare host */ + git__utf8_to_16(host, GIT_WIN_PATH, t->host); + + /* Establish session */ + t->session = WinHttpOpen( + ua, + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0); + + if (!t->session) { + giterr_set(GITERR_OS, "Failed to init WinHTTP"); + return -1; + } + + /* Establish connection */ + t->connection = WinHttpConnect( + t->session, + host, + port, + 0); + + if (!t->connection) { + giterr_set(GITERR_OS, "Failed to connect to host"); + return -1; + } + + return 0; +} + +static int winhttp_uploadpack_ls( + winhttp_subtransport *t, + winhttp_stream *s) +{ + s->service = upload_pack_service; + s->service_url = upload_pack_ls_service_url; + s->verb = get_verb; + + return 0; +} + +static int winhttp_uploadpack( + winhttp_subtransport *t, + winhttp_stream *s) +{ + s->service = upload_pack_service; + s->service_url = upload_pack_service_url; + s->verb = post_verb; + + return 0; +} + +static int winhttp_receivepack_ls( + winhttp_subtransport *t, + winhttp_stream *s) +{ + s->service = receive_pack_service; + s->service_url = receive_pack_ls_service_url; + s->verb = get_verb; + + return 0; +} + +static int winhttp_receivepack( + winhttp_subtransport *t, + winhttp_stream *s) +{ + /* WinHTTP only supports Transfer-Encoding: chunked + * on Windows Vista (NT 6.0) and higher. */ + s->chunked = git_has_win32_version(6, 0); + + if (s->chunked) + s->parent.write = winhttp_stream_write_chunked; + else + s->parent.write = winhttp_stream_write_buffered; + + s->service = receive_pack_service; + s->service_url = receive_pack_service_url; + s->verb = post_verb; + + return 0; +} + +static int winhttp_action( + git_smart_subtransport_stream **stream, + git_smart_subtransport *subtransport, + const char *url, + git_smart_service_t action) +{ + winhttp_subtransport *t = (winhttp_subtransport *)subtransport; + winhttp_stream *s; + int ret = -1; + + if (!t->connection && + winhttp_connect(t, url) < 0) + return -1; + + if (winhttp_stream_alloc(t, &s) < 0) + return -1; + + if (!stream) + return -1; + + switch (action) + { + case GIT_SERVICE_UPLOADPACK_LS: + ret = winhttp_uploadpack_ls(t, s); + break; + + case GIT_SERVICE_UPLOADPACK: + ret = winhttp_uploadpack(t, s); + break; + + case GIT_SERVICE_RECEIVEPACK_LS: + ret = winhttp_receivepack_ls(t, s); + break; + + case GIT_SERVICE_RECEIVEPACK: + ret = winhttp_receivepack(t, s); + break; + + default: + assert(0); + } + + if (!ret) + *stream = &s->parent; + + return ret; +} + +static int winhttp_close(git_smart_subtransport *subtransport) +{ + winhttp_subtransport *t = (winhttp_subtransport *)subtransport; + int ret = 0; + + if (t->host) { + git__free(t->host); + t->host = NULL; + } + + if (t->port) { + git__free(t->port); + t->port = NULL; + } + + if (t->user_from_url) { + git__free(t->user_from_url); + t->user_from_url = NULL; + } + + if (t->pass_from_url) { + git__free(t->pass_from_url); + t->pass_from_url = NULL; + } + + if (t->cred) { + t->cred->free(t->cred); + t->cred = NULL; + } + + if (t->url_cred) { + t->url_cred->free(t->url_cred); + t->url_cred = NULL; + } + + if (t->connection) { + if (!WinHttpCloseHandle(t->connection)) { + giterr_set(GITERR_OS, "Unable to close connection"); + ret = -1; + } + + t->connection = NULL; + } + + if (t->session) { + if (!WinHttpCloseHandle(t->session)) { + giterr_set(GITERR_OS, "Unable to close session"); + ret = -1; + } + + t->session = NULL; + } + + return ret; +} + +static void winhttp_free(git_smart_subtransport *subtransport) +{ + winhttp_subtransport *t = (winhttp_subtransport *)subtransport; + + winhttp_close(subtransport); + + git__free(t); +} + +int git_smart_subtransport_http(git_smart_subtransport **out, git_transport *owner) +{ + winhttp_subtransport *t; + + if (!out) + return -1; + + t = git__calloc(sizeof(winhttp_subtransport), 1); + GITERR_CHECK_ALLOC(t); + + t->owner = (transport_smart *)owner; + t->parent.action = winhttp_action; + t->parent.close = winhttp_close; + t->parent.free = winhttp_free; + + *out = (git_smart_subtransport *) t; + return 0; +} + +#endif /* GIT_WINHTTP */ diff --git a/src/tree-cache.c b/src/tree-cache.c index ebc2c6807..97ffc2acf 100644 --- a/src/tree-cache.c +++ b/src/tree-cache.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -69,7 +69,7 @@ const git_tree_cache *git_tree_cache_get(const git_tree_cache *tree, const char return NULL; } - if (end == NULL || end + 1 == '\0') + if (end == NULL || *end + 1 == '\0') return tree; ptr = end + 1; @@ -104,7 +104,7 @@ static int read_tree_internal(git_tree_cache **out, tree->name[name_len] = '\0'; /* Blank-terminated ASCII decimal number of entries in this tree */ - if (git__strtol32(&count, buffer, &buffer, 10) < 0 || count < -1) + if (git__strtol32(&count, buffer, &buffer, 10) < 0) goto corrupted; tree->entries = count; diff --git a/src/tree-cache.h b/src/tree-cache.h index 41fde997a..805483a78 100644 --- a/src/tree-cache.h +++ b/src/tree-cache.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. diff --git a/src/tree.c b/src/tree.c index 92b1b1e39..17b3c378d 100644 --- a/src/tree.c +++ b/src/tree.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -12,29 +12,89 @@ #include "git2/object.h" #define DEFAULT_TREE_SIZE 16 -#define MAX_FILEMODE 0777777 #define MAX_FILEMODE_BYTES 6 -static int valid_attributes(const int attributes) +static bool valid_filemode(const int filemode) { - return attributes >= 0 && attributes <= MAX_FILEMODE; + return (filemode == GIT_FILEMODE_TREE + || filemode == GIT_FILEMODE_BLOB + || filemode == GIT_FILEMODE_BLOB_EXECUTABLE + || filemode == GIT_FILEMODE_LINK + || filemode == GIT_FILEMODE_COMMIT); +} + +GIT_INLINE(git_filemode_t) normalize_filemode(git_filemode_t filemode) +{ + /* Tree bits set, but it's not a commit */ + if (filemode & GIT_FILEMODE_TREE && !(filemode & 0100000)) + return GIT_FILEMODE_TREE; + + /* If any of the x bits is set */ + if (filemode & 0111) + return GIT_FILEMODE_BLOB_EXECUTABLE; + + /* 16XXXX means commit */ + if ((filemode & GIT_FILEMODE_COMMIT) == GIT_FILEMODE_COMMIT) + return GIT_FILEMODE_COMMIT; + + /* 12XXXX means commit */ + if ((filemode & GIT_FILEMODE_LINK) == GIT_FILEMODE_LINK) + return GIT_FILEMODE_LINK; + + /* Otherwise, return a blob */ + return GIT_FILEMODE_BLOB; } static int valid_entry_name(const char *filename) { - return strlen(filename) > 0 && strchr(filename, '/') == NULL; + return *filename != '\0' && + strchr(filename, '/') == NULL && + (*filename != '.' || + (strcmp(filename, ".") != 0 && + strcmp(filename, "..") != 0 && + strcmp(filename, DOT_GIT) != 0)); } static int entry_sort_cmp(const void *a, const void *b) { - const git_tree_entry *entry_a = (const git_tree_entry *)(a); - const git_tree_entry *entry_b = (const git_tree_entry *)(b); + const git_tree_entry *e1 = (const git_tree_entry *)a; + const git_tree_entry *e2 = (const git_tree_entry *)b; + + return git_path_cmp( + e1->filename, e1->filename_len, git_tree_entry__is_tree(e1), + e2->filename, e2->filename_len, git_tree_entry__is_tree(e2), + git__strncmp); +} + +int git_tree_entry_cmp(const git_tree_entry *e1, const git_tree_entry *e2) +{ + return entry_sort_cmp(e1, e2); +} +int git_tree_entry_icmp(const git_tree_entry *e1, const git_tree_entry *e2) +{ return git_path_cmp( - entry_a->filename, entry_a->filename_len, git_tree_entry__is_tree(entry_a), - entry_b->filename, entry_b->filename_len, git_tree_entry__is_tree(entry_b)); + e1->filename, e1->filename_len, git_tree_entry__is_tree(e1), + e2->filename, e2->filename_len, git_tree_entry__is_tree(e2), + git__strncasecmp); } +static git_tree_entry *alloc_entry(const char *filename) +{ + git_tree_entry *entry = NULL; + size_t filename_len = strlen(filename); + + entry = git__malloc(sizeof(git_tree_entry) + filename_len + 1); + if (!entry) + return NULL; + + memset(entry, 0x0, sizeof(git_tree_entry)); + memcpy(entry->filename, filename, filename_len); + entry->filename[filename_len] = 0; + entry->filename_len = filename_len; + + return entry; +} struct tree_key_search { const char *filename; @@ -76,74 +136,114 @@ static int homing_search_cmp(const void *key, const void *array_member) * ambiguous because of folder vs file sorting, we look linearly * around the area for our target file. */ -static int tree_key_search(git_vector *entries, const char *filename) +static int tree_key_search( + size_t *at_pos, git_vector *entries, const char *filename, size_t filename_len) { struct tree_key_search ksearch; const git_tree_entry *entry; - - int homing, i; + size_t homing, i; ksearch.filename = filename; - ksearch.filename_len = strlen(filename); + ksearch.filename_len = filename_len; /* Initial homing search; find an entry on the tree with * the same prefix as the filename we're looking for */ - homing = git_vector_bsearch2(entries, &homing_search_cmp, &ksearch); - if (homing < 0) - return homing; + if (git_vector_bsearch2(&homing, entries, &homing_search_cmp, &ksearch) < 0) + return GIT_ENOTFOUND; /* We found a common prefix. Look forward as long as * there are entries that share the common prefix */ - for (i = homing; i < (int)entries->length; ++i) { + for (i = homing; i < entries->length; ++i) { entry = entries->contents[i]; if (homing_search_cmp(&ksearch, entry) < 0) break; - if (strcmp(filename, entry->filename) == 0) - return i; + if (entry->filename_len == filename_len && + memcmp(filename, entry->filename, filename_len) == 0) { + if (at_pos) + *at_pos = i; + + return 0; + } } /* If we haven't found our filename yet, look backwards * too as long as we have entries with the same prefix */ - for (i = homing - 1; i >= 0; --i) { - entry = entries->contents[i]; + if (homing > 0) { + i = homing - 1; - if (homing_search_cmp(&ksearch, entry) > 0) - break; + do { + entry = entries->contents[i]; + + if (homing_search_cmp(&ksearch, entry) > 0) + break; - if (strcmp(filename, entry->filename) == 0) - return i; + if (entry->filename_len == filename_len && + memcmp(filename, entry->filename, filename_len) == 0) { + if (at_pos) + *at_pos = i; + + return 0; + } + } while (i-- > 0); } /* The filename doesn't exist at all */ return GIT_ENOTFOUND; } -void git_tree__free(git_tree *tree) +void git_tree_entry_free(git_tree_entry *entry) { - unsigned int i; + if (entry == NULL) + return; - for (i = 0; i < tree->entries.length; ++i) { - git_tree_entry *e; - e = git_vector_get(&tree->entries, i); + git__free(entry); +} - git__free(e->filename); - git__free(e); - } +git_tree_entry *git_tree_entry_dup(const git_tree_entry *entry) +{ + size_t total_size; + git_tree_entry *copy; + + assert(entry); + + total_size = sizeof(git_tree_entry) + entry->filename_len + 1; + + copy = git__malloc(total_size); + if (!copy) + return NULL; + + memcpy(copy, entry, total_size); + + return copy; +} + +void git_tree__free(git_tree *tree) +{ + size_t i; + git_tree_entry *e; + + git_vector_foreach(&tree->entries, i, e) + git_tree_entry_free(e); git_vector_free(&tree->entries); git__free(tree); } -const git_oid *git_tree_id(git_tree *c) +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_id((git_object *)c); + return git_object_owner((const git_object *)t); } -unsigned int git_tree_entry_attributes(const git_tree_entry *entry) +git_filemode_t git_tree_entry_filemode(const git_tree_entry *entry) { - return entry->attr; + return (git_filemode_t)entry->attr; } const char *git_tree_entry_name(const git_tree_entry *entry) @@ -179,36 +279,61 @@ int git_tree_entry_to_object( return git_object_lookup(object_out, repo, &entry->oid, GIT_OBJ_ANY); } -const git_tree_entry *git_tree_entry_byname(git_tree *tree, const char *filename) +static const git_tree_entry *entry_fromname( + git_tree *tree, const char *name, size_t name_len) { - int idx; - - assert(tree && filename); + size_t idx; - idx = tree_key_search(&tree->entries, filename); - if (idx == GIT_ENOTFOUND) + if (tree_key_search(&idx, &tree->entries, name, name_len) < 0) return NULL; return git_vector_get(&tree->entries, idx); } -const git_tree_entry *git_tree_entry_byindex(git_tree *tree, unsigned int idx) +const git_tree_entry *git_tree_entry_byname( + 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) { assert(tree); return git_vector_get(&tree->entries, idx); } +const git_tree_entry *git_tree_entry_byoid( + const git_tree *tree, const git_oid *oid) +{ + size_t i; + const git_tree_entry *e; + + assert(tree); + + git_vector_foreach(&tree->entries, i, e) { + if (memcmp(&e->oid.id, &oid->id, sizeof(oid->id)) == 0) + return e; + } + + return NULL; +} + int git_tree__prefix_position(git_tree *tree, const char *path) { git_vector *entries = &tree->entries; struct tree_key_search ksearch; - unsigned int at_pos; + size_t at_pos; + + if (!path) + return 0; ksearch.filename = path; ksearch.filename_len = strlen(path); /* Find tree entry with appropriate prefix */ - git_vector_bsearch3(&at_pos, entries, &homing_search_cmp, &ksearch); + git_vector_bsearch2(&at_pos, entries, &homing_search_cmp, &ksearch); for (; at_pos < entries->length; ++at_pos) { const git_tree_entry *entry = entries->contents[at_pos]; @@ -222,18 +347,27 @@ int git_tree__prefix_position(git_tree *tree, const char *path) break; } - return at_pos; + return (int)at_pos; } -unsigned int git_tree_entrycount(git_tree *tree) +size_t git_tree_entrycount(const git_tree *tree) { assert(tree); return tree->entries.length; } -static int tree_error(const char *str) +unsigned int git_treebuilder_entrycount(git_treebuilder *bld) +{ + assert(bld); + return (unsigned int)bld->entrycount; +} + +static int tree_error(const char *str, const char *path) { - giterr_set(GITERR_TREE, "%s", str); + if (path) + giterr_set(GITERR_TREE, "%s - %s", str, path); + else + giterr_set(GITERR_TREE, "%s", str); return -1; } @@ -244,28 +378,31 @@ static int tree_parse_buffer(git_tree *tree, const char *buffer, const char *buf while (buffer < buffer_end) { git_tree_entry *entry; - int tmp; - - entry = git__calloc(1, sizeof(git_tree_entry)); - GITERR_CHECK_ALLOC(entry); + int attr; - if (git_vector_insert(&tree->entries, entry) < 0) - return -1; - - if (git__strtol32(&tmp, buffer, &buffer, 8) < 0 || - !buffer || !valid_attributes(tmp)) - return tree_error("Failed to parse tree. Can't parse attributes"); + if (git__strtol32(&attr, buffer, &buffer, 8) < 0 || !buffer) + return tree_error("Failed to parse tree. Can't parse filemode", NULL); - entry->attr = tmp; + attr = normalize_filemode(attr); /* make sure to normalize the filemode */ if (*buffer++ != ' ') - return tree_error("Failed to parse tree. Object is corrupted"); + return tree_error("Failed to parse tree. Object is corrupted", NULL); if (memchr(buffer, 0, buffer_end - buffer) == NULL) - return tree_error("Failed to parse tree. Object is corrupted"); + return tree_error("Failed to parse tree. Object is corrupted", NULL); + + /** Allocate the entry and store it in the entries vector */ + { + entry = alloc_entry(buffer); + GITERR_CHECK_ALLOC(entry); - entry->filename = git__strdup(buffer); - entry->filename_len = strlen(buffer); + if (git_vector_insert(&tree->entries, entry) < 0) { + git__free(entry); + return -1; + } + + entry->attr = attr; + } while (buffer < buffer_end && *buffer != 0) buffer++; @@ -285,14 +422,13 @@ int git_tree__parse(git_tree *tree, git_odb_object *obj) return tree_parse_buffer(tree, (char *)obj->raw.data, (char *)obj->raw.data + obj->raw.len); } -static unsigned int find_next_dir(const char *dirname, git_index *index, unsigned int start) +static size_t find_next_dir(const char *dirname, git_index *index, size_t start) { - unsigned int i, entries = git_index_entrycount(index); - size_t dirlen; + size_t dirlen, i, entries = git_index_entrycount(index); dirlen = strlen(dirname); for (i = start; i < entries; ++i) { - git_index_entry *entry = git_index_get(index, i); + const git_index_entry *entry = git_index_get_byindex(index, i); if (strlen(entry->path) < dirlen || memcmp(entry->path, dirname, dirlen) || (dirlen > 0 && entry->path[dirlen] != '/')) { @@ -303,22 +439,29 @@ static unsigned int find_next_dir(const char *dirname, git_index *index, unsigne return i; } -static int append_entry(git_treebuilder *bld, const char *filename, const git_oid *id, unsigned int attributes) +static int append_entry( + git_treebuilder *bld, + const char *filename, + const git_oid *id, + git_filemode_t filemode) { git_tree_entry *entry; - entry = git__calloc(1, sizeof(git_tree_entry)); - GITERR_CHECK_ALLOC(entry); + if (!valid_entry_name(filename)) + return tree_error("Failed to insert entry. Invalid name for a tree entry", filename); - entry->filename = git__strdup(filename); - entry->filename_len = strlen(entry->filename); + entry = alloc_entry(filename); + GITERR_CHECK_ALLOC(entry); git_oid_cpy(&entry->oid, id); - entry->attr = attributes; + entry->attr = (uint16_t)filemode; - if (git_vector_insert(&bld->entries, entry) < 0) + if (git_vector_insert(&bld->entries, entry) < 0) { + git__free(entry); return -1; + } + bld->entrycount++; return 0; } @@ -327,11 +470,10 @@ static int write_tree( git_repository *repo, git_index *index, const char *dirname, - unsigned int start) + size_t start) { git_treebuilder *bld = NULL; - - unsigned int i, entries = git_index_entrycount(index); + size_t i, entries = git_index_entrycount(index); int error; size_t dirname_len = strlen(dirname); const git_tree_cache *cache; @@ -339,13 +481,11 @@ static int write_tree( cache = git_tree_cache_get(index->tree, dirname); if (cache != NULL && cache->entries >= 0){ git_oid_cpy(oid, &cache->oid); - return find_next_dir(dirname, index, start); + return (int)find_next_dir(dirname, index, start); } - error = git_treebuilder_create(&bld, NULL); - if (bld == NULL) { + if ((error = git_treebuilder_create(&bld, NULL)) < 0 || bld == NULL) return -1; - } /* * This loop is unfortunate, but necessary. The index doesn't have @@ -353,8 +493,8 @@ static int write_tree( * need to keep track of the current position. */ for (i = start; i < entries; ++i) { - git_index_entry *entry = git_index_get(index, i); - char *filename, *next_slash; + const git_index_entry *entry = git_index_get_byindex(index, i); + const char *filename, *next_slash; /* * If we've left our (sub)tree, exit the loop and return. The @@ -385,7 +525,8 @@ 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"); + tree_error("Failed to write subtree", subdir); + git__free(subdir); goto on_error; } else { i = written - 1; /* -1 because of the loop increment */ @@ -403,18 +544,15 @@ static int write_tree( } else { last_comp = subdir; } + error = append_entry(bld, last_comp, &sub_oid, S_IFDIR); git__free(subdir); - if (error < 0) { - tree_error("Failed to insert dir"); + if (error < 0) goto on_error; - } } else { error = append_entry(bld, filename, &entry->oid, entry->mode); - if (error < 0) { - tree_error("Failed to insert file"); + if (error < 0) goto on_error; - } } } @@ -422,43 +560,54 @@ static int write_tree( goto on_error; git_treebuilder_free(bld); - return i; + return (int)i; on_error: git_treebuilder_free(bld); return -1; } -int git_tree_create_fromindex(git_oid *oid, git_index *index) +int git_tree__write_index( + git_oid *oid, git_index *index, git_repository *repo) { int ret; - git_repository *repo; + bool old_ignore_case = false; - repo = (git_repository *)GIT_REFCOUNT_OWNER(index); + assert(oid && index && repo); - if (repo == NULL) - return tree_error("Failed to create tree. " - "The index file is not backed up by an existing repository"); + if (git_index_has_conflicts(index)) { + giterr_set(GITERR_INDEX, + "Cannot create a tree from a not fully merged index."); + return GIT_EUNMERGED; + } if (index->tree != NULL && index->tree->entries >= 0) { git_oid_cpy(oid, &index->tree->oid); return 0; } - /* The tree cache didn't help us */ + /* The tree cache didn't help us; we'll have to write + * out a tree. If the index is ignore_case, we must + * make it case-sensitive for the duration of the tree-write + * operation. */ + + if (index->ignore_case) { + old_ignore_case = true; + git_index__set_ignore_case(index, false); + } + ret = write_tree(oid, repo, index, "", 0); - return ret < 0 ? ret : 0; -} -static void sort_entries(git_treebuilder *bld) -{ - git_vector_sort(&bld->entries); + if (old_ignore_case) + git_index__set_ignore_case(index, true); + + return ret < 0 ? ret : 0; } int git_treebuilder_create(git_treebuilder **builder_p, const git_tree *source) { git_treebuilder *bld; - unsigned int i, source_entries = DEFAULT_TREE_SIZE; + size_t i, source_entries = DEFAULT_TREE_SIZE; assert(builder_p); @@ -472,10 +621,13 @@ int git_treebuilder_create(git_treebuilder **builder_p, const git_tree *source) goto on_error; if (source != NULL) { - for (i = 0; i < source->entries.length; ++i) { - git_tree_entry *entry_src = source->entries.contents[i]; + git_tree_entry *entry_src; - if (append_entry(bld, entry_src->filename, &entry_src->oid, entry_src->attr) < 0) + git_vector_foreach(&source->entries, i, entry_src) { + if (append_entry( + bld, entry_src->filename, + &entry_src->oid, + entry_src->attr) < 0) goto on_error; } } @@ -488,42 +640,46 @@ on_error: return -1; } -int git_treebuilder_insert(git_tree_entry **entry_out, git_treebuilder *bld, const char *filename, const git_oid *id, unsigned int attributes) +int git_treebuilder_insert( + const git_tree_entry **entry_out, + git_treebuilder *bld, + const char *filename, + const git_oid *id, + git_filemode_t filemode) { git_tree_entry *entry; - int pos; + size_t pos; assert(bld && id && filename); - if (!valid_attributes(attributes)) - return tree_error("Failed to insert entry. Invalid attributes"); + if (!valid_filemode(filemode)) + return tree_error("Failed to insert entry. Invalid filemode for file", filename); if (!valid_entry_name(filename)) - return tree_error("Failed to insert entry. Invalid name for a tree entry"); + return tree_error("Failed to insert entry. Invalid name for a tree entry", filename); - pos = tree_key_search(&bld->entries, filename); - - if (pos >= 0) { + if (!tree_key_search(&pos, &bld->entries, filename, strlen(filename))) { entry = git_vector_get(&bld->entries, pos); - if (entry->removed) + if (entry->removed) { entry->removed = 0; + bld->entrycount++; + } } else { - entry = git__calloc(1, sizeof(git_tree_entry)); + entry = alloc_entry(filename); GITERR_CHECK_ALLOC(entry); - entry->filename = git__strdup(filename); - entry->filename_len = strlen(entry->filename); + if (git_vector_insert(&bld->entries, entry) < 0) { + git__free(entry); + return -1; + } + + bld->entrycount++; } git_oid_cpy(&entry->oid, id); - entry->attr = attributes; - - if (pos == GIT_ENOTFOUND) { - if (git_vector_insert(&bld->entries, entry) < 0) - return -1; - } + entry->attr = filemode; - if (entry_out != NULL) + if (entry_out) *entry_out = entry; return 0; @@ -531,13 +687,12 @@ int git_treebuilder_insert(git_tree_entry **entry_out, git_treebuilder *bld, con static git_tree_entry *treebuilder_get(git_treebuilder *bld, const char *filename) { - int idx; + size_t idx; git_tree_entry *entry; assert(bld && filename); - idx = tree_key_search(&bld->entries, filename); - if (idx < 0) + if (tree_key_search(&idx, &bld->entries, filename, strlen(filename)) < 0) return NULL; entry = git_vector_get(&bld->entries, idx); @@ -557,27 +712,29 @@ int git_treebuilder_remove(git_treebuilder *bld, const char *filename) git_tree_entry *remove_ptr = treebuilder_get(bld, filename); if (remove_ptr == NULL || remove_ptr->removed) - return tree_error("Failed to remove entry. File isn't in the tree"); + return tree_error("Failed to remove entry. File isn't in the tree", filename); remove_ptr->removed = 1; + bld->entrycount--; return 0; } int git_treebuilder_write(git_oid *oid, git_repository *repo, git_treebuilder *bld) { - unsigned int i; + int error = 0; + size_t i; git_buf tree = GIT_BUF_INIT; git_odb *odb; assert(bld); - sort_entries(bld); + git_vector_sort(&bld->entries); /* Grow the buffer beforehand to an estimated size */ - git_buf_grow(&tree, bld->entries.length * 72); + error = git_buf_grow(&tree, bld->entries.length * 72); - for (i = 0; i < bld->entries.length; ++i) { - git_tree_entry *entry = bld->entries.contents[i]; + for (i = 0; i < bld->entries.length && !error; ++i) { + git_tree_entry *entry = git_vector_get(&bld->entries, i); if (entry->removed) continue; @@ -585,153 +742,154 @@ int git_treebuilder_write(git_oid *oid, git_repository *repo, git_treebuilder *b git_buf_printf(&tree, "%o ", entry->attr); git_buf_put(&tree, entry->filename, entry->filename_len + 1); git_buf_put(&tree, (char *)entry->oid.id, GIT_OID_RAWSZ); - } - if (git_buf_oom(&tree)) - goto on_error; - - if (git_repository_odb__weakptr(&odb, repo) < 0) - goto on_error; - - - if (git_odb_write(oid, odb, tree.ptr, tree.size, GIT_OBJ_TREE) < 0) - goto on_error; + if (git_buf_oom(&tree)) + error = -1; + } - git_buf_free(&tree); - return 0; + if (!error && + !(error = git_repository_odb__weakptr(&odb, repo))) + error = git_odb_write(oid, odb, tree.ptr, tree.size, GIT_OBJ_TREE); -on_error: git_buf_free(&tree); - return -1; + return error; } -void git_treebuilder_filter(git_treebuilder *bld, int (*filter)(const git_tree_entry *, void *), void *payload) +void git_treebuilder_filter( + git_treebuilder *bld, + git_treebuilder_filter_cb filter, + void *payload) { - unsigned int i; + size_t i; + git_tree_entry *entry; assert(bld && filter); - for (i = 0; i < bld->entries.length; ++i) { - git_tree_entry *entry = bld->entries.contents[i]; - if (!entry->removed && filter(entry, payload)) + git_vector_foreach(&bld->entries, i, entry) { + if (!entry->removed && filter(entry, payload)) { entry->removed = 1; + bld->entrycount--; + } } } void git_treebuilder_clear(git_treebuilder *bld) { - unsigned int i; + size_t i; + git_tree_entry *e; + assert(bld); - for (i = 0; i < bld->entries.length; ++i) { - git_tree_entry *e = bld->entries.contents[i]; - git__free(e->filename); - git__free(e); - } + git_vector_foreach(&bld->entries, i, e) + git_tree_entry_free(e); git_vector_clear(&bld->entries); + bld->entrycount = 0; } void git_treebuilder_free(git_treebuilder *bld) { + if (bld == NULL) + return; + git_treebuilder_clear(bld); git_vector_free(&bld->entries); git__free(bld); } -static int tree_frompath( - git_tree **parent_out, +static size_t subpath_len(const char *path) +{ + const char *slash_pos = strchr(path, '/'); + if (slash_pos == NULL) + return strlen(path); + + return slash_pos - path; +} + +int git_tree_entry_bypath( + git_tree_entry **entry_out, git_tree *root, - git_buf *treeentry_path, - size_t offset) + const char *path) { - char *slash_pos = NULL; - const git_tree_entry* entry; int error = 0; git_tree *subtree; + const git_tree_entry *entry; + size_t filename_len; - if (!*(treeentry_path->ptr + offset)) { - giterr_set(GITERR_INVALID, - "Invalid relative path to a tree entry '%s'.", treeentry_path->ptr); - return -1; - } - - slash_pos = (char *)strchr(treeentry_path->ptr + offset, '/'); + /* Find how long is the current path component (i.e. + * the filename between two slashes */ + filename_len = subpath_len(path); - if (slash_pos == NULL) - return git_tree_lookup( - parent_out, - root->object.repo, - git_object_id((const git_object *)root) - ); - - if (slash_pos == treeentry_path->ptr + offset) { - giterr_set(GITERR_INVALID, - "Invalid relative path to a tree entry '%s'.", treeentry_path->ptr); - return -1; + if (filename_len == 0) { + giterr_set(GITERR_TREE, "Invalid tree path given"); + return GIT_ENOTFOUND; } - *slash_pos = '\0'; - - entry = git_tree_entry_byname(root, treeentry_path->ptr + offset); - - if (slash_pos != NULL) - *slash_pos = '/'; + entry = entry_fromname(root, path, filename_len); if (entry == NULL) { giterr_set(GITERR_TREE, - "No tree entry can be found from " - "the given tree and relative path '%s'.", treeentry_path->ptr); + "The path '%s' does not exist in the given tree", path); return GIT_ENOTFOUND; } + switch (path[filename_len]) { + case '/': + /* If there are more components in the path... + * then this entry *must* be a tree */ + if (!git_tree_entry__is_tree(entry)) { + giterr_set(GITERR_TREE, + "The path '%s' does not exist in the given tree", path); + return GIT_ENOTFOUND; + } + + /* If there's only a slash left in the path, we + * return the current entry; otherwise, we keep + * walking down the path */ + if (path[filename_len + 1] != '\0') + break; + + case '\0': + /* If there are no more components in the path, return + * this entry */ + *entry_out = git_tree_entry_dup(entry); + return 0; + } if (git_tree_lookup(&subtree, root->object.repo, &entry->oid) < 0) - return error; + return -1; - error = tree_frompath( - parent_out, + error = git_tree_entry_bypath( + entry_out, subtree, - treeentry_path, - (slash_pos - treeentry_path->ptr) + 1 + path + filename_len + 1 ); git_tree_free(subtree); return error; } -int git_tree_get_subtree( - git_tree **subtree, - git_tree *root, - const char *subtree_path) -{ - int error; - git_buf buffer = GIT_BUF_INIT; - - assert(subtree && root && subtree_path); - - if ((error = git_buf_sets(&buffer, subtree_path)) == 0) - error = tree_frompath(subtree, root, &buffer, 0); - - git_buf_free(&buffer); - - return error; -} - -static int tree_walk_post( - git_tree *tree, +static int tree_walk( + const git_tree *tree, git_treewalk_cb callback, git_buf *path, - void *payload) + void *payload, + bool preorder) { int error = 0; - unsigned int i; - - for (i = 0; i < tree->entries.length; ++i) { - git_tree_entry *entry = tree->entries.contents[i]; + size_t i; + const git_tree_entry *entry; - if (callback(path->ptr, entry, payload) < 0) - continue; + git_vector_foreach(&tree->entries, i, entry) { + if (preorder) { + error = callback(path->ptr, entry, payload); + if (error > 0) + continue; + if (error < 0) { + giterr_clear(); + return GIT_EUSER; + } + } if (git_tree_entry__is_tree(entry)) { git_tree *subtree; @@ -748,36 +906,41 @@ static int tree_walk_post( if (git_buf_oom(path)) return -1; - if (tree_walk_post(subtree, callback, path, payload) < 0) - return -1; + error = tree_walk(subtree, callback, path, payload, preorder); + if (error != 0) + break; git_buf_truncate(path, path_len); git_tree_free(subtree); } + + if (!preorder && callback(path->ptr, entry, payload) < 0) { + giterr_clear(); + error = GIT_EUSER; + break; + } } - return 0; + return error; } -int git_tree_walk(git_tree *tree, git_treewalk_cb callback, int mode, void *payload) +int git_tree_walk( + const git_tree *tree, + git_treewalk_mode mode, + git_treewalk_cb callback, + void *payload) { int error = 0; git_buf root_path = GIT_BUF_INIT; - switch (mode) { - case GIT_TREEWALK_POST: - error = tree_walk_post(tree, callback, &root_path, payload); - break; - - case GIT_TREEWALK_PRE: - tree_error("Preorder tree walking is still not implemented"); - return -1; - - default: - giterr_set(GITERR_INVALID, "Invalid walking mode for tree walk"); - return -1; + if (mode != GIT_TREEWALK_POST && mode != GIT_TREEWALK_PRE) { + giterr_set(GITERR_INVALID, "Invalid walking mode for tree walk"); + return -1; } + error = tree_walk( + tree, callback, &root_path, payload, (mode == GIT_TREEWALK_PRE)); + git_buf_free(&root_path); return error; diff --git a/src/tree.h b/src/tree.h index 498a90d66..b77bfd961 100644 --- a/src/tree.h +++ b/src/tree.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -13,11 +13,11 @@ #include "vector.h" struct git_tree_entry { - unsigned int attr; - char *filename; + uint16_t removed; + uint16_t attr; git_oid oid; size_t filename_len; - int removed; + char filename[1]; }; struct git_tree { @@ -27,14 +27,16 @@ struct git_tree { struct git_treebuilder { git_vector entries; + size_t entrycount; /* vector may contain "removed" entries */ }; - GIT_INLINE(bool) git_tree_entry__is_tree(const struct git_tree_entry *e) { return (S_ISDIR(e->attr) && !S_ISGITLINK(e->attr)); } +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); @@ -48,4 +50,15 @@ int git_tree__parse(git_tree *tree, git_odb_object *obj); int git_tree__prefix_position(git_tree *tree, const char *prefix); +/** + * Write a tree to the given repository + */ +int git_tree__write_index( + git_oid *oid, git_index *index, git_repository *repo); + +/** + * Obsolete mode kept for compatibility reasons + */ +#define GIT_FILEMODE_BLOB_GROUP_WRITABLE 0100664 + #endif diff --git a/src/tsort.c b/src/tsort.c index f54c21e50..4885e435b 100644 --- a/src/tsort.c +++ b/src/tsort.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -23,9 +23,8 @@ # define MIN(x,y) (((x) < (y) ? (x) : (y))) #endif -typedef int (*cmp_ptr_t)(const void *, const void *); - -static int binsearch(void **dst, const void *x, size_t size, cmp_ptr_t cmp) +static int binsearch( + void **dst, const void *x, size_t size, git__sort_r_cmp cmp, void *payload) { int l, c, r; void *lx, *cx; @@ -38,12 +37,12 @@ static int binsearch(void **dst, const void *x, size_t size, cmp_ptr_t cmp) lx = dst[l]; /* check for beginning conditions */ - if (cmp(x, lx) < 0) + if (cmp(x, lx, payload) < 0) return 0; - else if (cmp(x, lx) == 0) { + else if (cmp(x, lx, payload) == 0) { int i = 1; - while (cmp(x, dst[i]) == 0) + while (cmp(x, dst[i], payload) == 0) i++; return i; } @@ -51,7 +50,7 @@ static int binsearch(void **dst, const void *x, size_t size, cmp_ptr_t cmp) /* guaranteed not to be >= rx */ cx = dst[c]; while (1) { - const int val = cmp(x, cx); + const int val = cmp(x, cx, payload); if (val < 0) { if (c - l <= 1) return c; r = c; @@ -62,7 +61,7 @@ static int binsearch(void **dst, const void *x, size_t size, cmp_ptr_t cmp) } else { do { cx = dst[++c]; - } while (cmp(x, cx) == 0); + } while (cmp(x, cx, payload) == 0); return c; } c = l + ((r - l) >> 1); @@ -71,7 +70,8 @@ static int binsearch(void **dst, const void *x, size_t size, cmp_ptr_t cmp) } /* Binary insertion sort, but knowing that the first "start" entries are sorted. Used in timsort. */ -static void bisort(void **dst, size_t start, size_t size, cmp_ptr_t cmp) +static void bisort( + void **dst, size_t start, size_t size, git__sort_r_cmp cmp, void *payload) { size_t i; void *x; @@ -80,12 +80,12 @@ static void bisort(void **dst, size_t start, size_t size, cmp_ptr_t cmp) for (i = start; i < size; i++) { int j; /* If this entry is already correct, just move along */ - if (cmp(dst[i - 1], dst[i]) <= 0) + if (cmp(dst[i - 1], dst[i], payload) <= 0) continue; /* Else we need to find the right place, shift everything over, and squeeze in */ x = dst[i]; - location = binsearch(dst, x, i, cmp); + location = binsearch(dst, x, i, cmp, payload); for (j = (int)i - 1; j >= location; j--) { dst[j + 1] = dst[j]; } @@ -102,7 +102,8 @@ struct tsort_run { struct tsort_store { size_t alloc; - cmp_ptr_t cmp; + git__sort_r_cmp cmp; + void *payload; void **storage; }; @@ -118,7 +119,8 @@ static void reverse_elements(void **dst, ssize_t start, ssize_t end) } } -static ssize_t count_run(void **dst, ssize_t start, ssize_t size, struct tsort_store *store) +static ssize_t count_run( + void **dst, ssize_t start, ssize_t size, struct tsort_store *store) { ssize_t curr = start + 2; @@ -126,7 +128,7 @@ static ssize_t count_run(void **dst, ssize_t start, ssize_t size, struct tsort_s return 1; if (start >= size - 2) { - if (store->cmp(dst[size - 2], dst[size - 1]) > 0) { + if (store->cmp(dst[size - 2], dst[size - 1], store->payload) > 0) { void *tmp = dst[size - 1]; dst[size - 1] = dst[size - 2]; dst[size - 2] = tmp; @@ -135,13 +137,15 @@ static ssize_t count_run(void **dst, ssize_t start, ssize_t size, struct tsort_s return 2; } - if (store->cmp(dst[start], dst[start + 1]) <= 0) { - while (curr < size - 1 && store->cmp(dst[curr - 1], dst[curr]) <= 0) + if (store->cmp(dst[start], dst[start + 1], store->payload) <= 0) { + while (curr < size - 1 && + store->cmp(dst[curr - 1], dst[curr], store->payload) <= 0) curr++; return curr - start; } else { - while (curr < size - 1 && store->cmp(dst[curr - 1], dst[curr]) > 0) + while (curr < size - 1 && + store->cmp(dst[curr - 1], dst[curr], store->payload) > 0) curr++; /* reverse in-place */ @@ -219,7 +223,7 @@ static void merge(void **dst, const struct tsort_run *stack, ssize_t stack_curr, for (k = curr; k < curr + A + B; k++) { if ((i < A) && (j < curr + A + B)) { - if (store->cmp(storage[i], dst[j]) <= 0) + if (store->cmp(storage[i], dst[j], store->payload) <= 0) dst[k] = storage[i++]; else dst[k] = dst[j++]; @@ -235,7 +239,7 @@ static void merge(void **dst, const struct tsort_run *stack, ssize_t stack_curr, for (k = curr + A + B - 1; k >= curr; k--) { if ((i >= 0) && (j >= curr)) { - if (store->cmp(dst[j], storage[i]) > 0) + if (store->cmp(dst[j], storage[i], store->payload) > 0) dst[k] = dst[j--]; else dst[k] = storage[i--]; @@ -307,7 +311,7 @@ static ssize_t collapse(void **dst, struct tsort_run *stack, ssize_t stack_curr, if (run < minrun) run = minrun;\ if (run > (ssize_t)size - curr) run = size - curr;\ if (run > len) {\ - bisort(&dst[curr], len, run, cmp);\ + bisort(&dst[curr], len, run, cmp, payload);\ len = run;\ }\ run_stack[stack_curr].start = curr;\ @@ -329,7 +333,8 @@ static ssize_t collapse(void **dst, struct tsort_run *stack, ssize_t stack_curr, }\ while (0) -void git__tsort(void **dst, size_t size, cmp_ptr_t cmp) +void git__tsort_r( + void **dst, size_t size, git__sort_r_cmp cmp, void *payload) { struct tsort_store _store, *store = &_store; struct tsort_run run_stack[128]; @@ -340,7 +345,7 @@ void git__tsort(void **dst, size_t size, cmp_ptr_t cmp) ssize_t minrun; if (size < 64) { - bisort(dst, 1, size, cmp); + bisort(dst, 1, size, cmp, payload); return; } @@ -351,6 +356,7 @@ void git__tsort(void **dst, size_t size, cmp_ptr_t cmp) store->alloc = 0; store->storage = NULL; store->cmp = cmp; + store->payload = payload; PUSH_NEXT(); PUSH_NEXT(); @@ -365,3 +371,13 @@ void git__tsort(void **dst, size_t size, cmp_ptr_t cmp) PUSH_NEXT(); } } + +static int tsort_r_cmp(const void *a, const void *b, void *payload) +{ + return ((git__tsort_cmp)payload)(a, b); +} + +void git__tsort(void **dst, size_t size, git__tsort_cmp cmp) +{ + git__tsort_r(dst, size, tsort_r_cmp, cmp); +} diff --git a/src/unix/map.c b/src/unix/map.c index 772f4e247..7de99c99d 100644 --- a/src/unix/map.c +++ b/src/unix/map.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -31,8 +31,11 @@ int p_mmap(git_map *out, size_t len, int prot, int flags, int fd, git_off_t offs mflag = MAP_SHARED; else if ((flags & GIT_MAP_TYPE) == GIT_MAP_PRIVATE) mflag = MAP_PRIVATE; + else + mflag = MAP_SHARED; out->data = mmap(NULL, len, mprot, mflag, fd, offset); + if (!out->data || out->data == MAP_FAILED) { giterr_set(GITERR_OS, "Failed to mmap. Could not write data"); return -1; @@ -47,6 +50,7 @@ int p_munmap(git_map *map) { assert(map != NULL); munmap(map->data, map->len); + return 0; } diff --git a/src/unix/posix.h b/src/unix/posix.h index 48b492941..f4886c5d1 100644 --- a/src/unix/posix.h +++ b/src/unix/posix.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -7,25 +7,29 @@ #ifndef INCLUDE_posix__w32_h__ #define INCLUDE_posix__w32_h__ -#ifndef __sun -# include <fnmatch.h> -# define p_fnmatch(p, s, f) fnmatch(p, s, f) -#else -# include "compat/fnmatch.h" -#endif - #include <stdio.h> +#include <sys/param.h> #define p_lstat(p,b) lstat(p,b) #define p_readlink(a, b, c) readlink(a, b, c) +#define p_symlink(o,n) symlink(o, n) #define p_link(o,n) link(o, n) #define p_unlink(p) unlink(p) #define p_mkdir(p,m) mkdir(p, m) #define p_fsync(fd) fsync(fd) -#define p_realpath(p, po) realpath(p, po) + +/* The OpenBSD realpath function behaves differently */ +#if !defined(__OpenBSD__) +# define p_realpath(p, po) realpath(p, po) +#endif + #define p_vsnprintf(b, c, f, a) vsnprintf(b, c, f, a) #define p_snprintf(b, c, f, ...) snprintf(b, c, f, __VA_ARGS__) #define p_mkstemp(p) mkstemp(p) #define p_setenv(n,v,o) setenv(n,v,o) +#define p_inet_pton(a, b, c) inet_pton(a, b, c) + +/* see win32/posix.h for explanation about why this exists */ +#define p_lstat_posixly(p,b) lstat(p,b) #endif diff --git a/src/unix/realpath.c b/src/unix/realpath.c new file mode 100644 index 000000000..15601bd22 --- /dev/null +++ b/src/unix/realpath.c @@ -0,0 +1,30 @@ +/* + * 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/common.h> + +#ifdef __OpenBSD__ + +#include <limits.h> +#include <stdlib.h> +#include <fcntl.h> +#include <unistd.h> + +char *p_realpath(const char *pathname, char *resolved) +{ + char *ret; + + if ((ret = realpath(pathname, resolved)) == NULL) + return NULL; + + /* Figure out if the file exists */ + if (!access(ret, F_OK)) + return ret; + + return NULL; +} + +#endif diff --git a/src/util.c b/src/util.c index ce770203a..8e83d298e 100644 --- a/src/util.c +++ b/src/util.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -10,6 +10,7 @@ #include <stdio.h> #include <ctype.h> #include "posix.h" +#include "fileops.h" #ifdef _MSC_VER # include <Shlwapi.h> @@ -22,6 +23,91 @@ void git_libgit2_version(int *major, int *minor, int *rev) *rev = LIBGIT2_VER_REVISION; } +int git_libgit2_capabilities() +{ + return 0 +#ifdef GIT_THREADS + | GIT_CAP_THREADS +#endif +#if defined(GIT_SSL) || defined(GIT_WINHTTP) + | GIT_CAP_HTTPS +#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) +{ + int val = -1; + + switch (config_level) { + case GIT_CONFIG_LEVEL_SYSTEM: val = GIT_FUTILS_DIR_SYSTEM; break; + case GIT_CONFIG_LEVEL_XDG: val = GIT_FUTILS_DIR_XDG; break; + case GIT_CONFIG_LEVEL_GLOBAL: val = GIT_FUTILS_DIR_GLOBAL; break; + default: + giterr_set( + GITERR_INVALID, "Invalid config path selector %d", config_level); + } + + return val; +} + +int git_libgit2_opts(int key, ...) +{ + int error = 0; + va_list ap; + + va_start(ap, key); + + switch (key) { + case GIT_OPT_SET_MWINDOW_SIZE: + git_mwindow__window_size = va_arg(ap, size_t); + break; + + case GIT_OPT_GET_MWINDOW_SIZE: + *(va_arg(ap, size_t *)) = git_mwindow__window_size; + break; + + case GIT_OPT_SET_MWINDOW_MAPPED_LIMIT: + git_mwindow__mapped_limit = va_arg(ap, size_t); + break; + + case GIT_OPT_GET_MWINDOW_MAPPED_LIMIT: + *(va_arg(ap, size_t *)) = git_mwindow__mapped_limit; + break; + + case GIT_OPT_GET_SEARCH_PATH: + if ((error = config_level_to_futils_dir(va_arg(ap, int))) >= 0) { + char *out = va_arg(ap, char *); + size_t outlen = va_arg(ap, size_t); + + error = git_futils_dirs_get_str(out, outlen, error); + } + break; + + case GIT_OPT_SET_SEARCH_PATH: + if ((error = config_level_to_futils_dir(va_arg(ap, int))) >= 0) + 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; + break; + + case GIT_OPT_SET_ODB_CACHE_SIZE: + git_odb__cache_size = va_arg(ap, size_t); + break; + } + + va_end(ap); + + return error; +} + void git_strarray_free(git_strarray *array) { size_t i; @@ -29,6 +115,8 @@ void git_strarray_free(git_strarray *array) git__free(array->strings[i]); git__free(array->strings); + + memset(array, 0, sizeof(*array)); } int git_strarray_copy(git_strarray *tgt, const git_strarray *src) @@ -46,8 +134,10 @@ int git_strarray_copy(git_strarray *tgt, const git_strarray *src) GITERR_CHECK_ALLOC(tgt->strings); for (i = 0; i < src->count; ++i) { - tgt->strings[tgt->count] = git__strdup(src->strings[i]); + if (!src->strings[i]) + continue; + tgt->strings[tgt->count] = git__strdup(src->strings[i]); if (!tgt->strings[tgt->count]) { git_strarray_free(tgt); memset(tgt, 0, sizeof(*tgt)); @@ -162,6 +252,42 @@ int git__strtol32(int32_t *result, const char *nptr, const char **endptr, int ba return error; } +int git__strcmp(const char *a, const char *b) +{ + while (*a && *b && *a == *b) + ++a, ++b; + return (int)(*(const unsigned char *)a) - (int)(*(const unsigned char *)b); +} + +int git__strcasecmp(const char *a, const char *b) +{ + while (*a && *b && tolower(*a) == tolower(*b)) + ++a, ++b; + return (tolower(*a) - tolower(*b)); +} + +int git__strncmp(const char *a, const char *b, size_t sz) +{ + while (sz && *a && *b && *a == *b) + --sz, ++a, ++b; + if (!sz) + return 0; + return (int)(*(const unsigned char *)a) - (int)(*(const unsigned char *)b); +} + +int git__strncasecmp(const char *a, const char *b, size_t sz) +{ + int al, bl; + + do { + al = (unsigned char)tolower(*a); + bl = (unsigned char)tolower(*b); + ++a, ++b; + } while (--sz && al && al == bl); + + return al - bl; +} + void git__strntolower(char *str, size_t len) { size_t i; @@ -179,7 +305,7 @@ void git__strtolower(char *str) int git__prefixcmp(const char *str, const char *prefix) { for (;;) { - char p = *(prefix++), s; + unsigned char p = *(prefix++), s; if (!p) return 0; if ((s = *(str++)) != p) @@ -187,6 +313,11 @@ int git__prefixcmp(const char *str, const char *prefix) } } +int git__prefixcmp_icase(const char *str, const char *prefix) +{ + return strncasecmp(str, prefix, strlen(prefix)); +} + int git__suffixcmp(const char *str, const char *suffix) { size_t a = strlen(str); @@ -221,6 +352,24 @@ char *git__strtok(char **end, const char *sep) return NULL; } +/* Similar to strtok, but does not collapse repeated tokens. */ +char *git__strsep(char **end, const char *sep) +{ + char *start = *end, *ptr = *end; + + while (*ptr && !strchr(sep, *ptr)) + ++ptr; + + if (*ptr) { + *end = ptr + 1; + *ptr = '\0'; + + return start; + } + + return NULL; +} + void git__hexdump(const char *buffer, size_t len) { static const size_t LINE_WIDTH = 16; @@ -367,6 +516,30 @@ uint32_t git__hash(const void *key, int len, uint32_t seed) * * Copyright (c) 1990 Regents of the University of California. * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. [rescinded 22 July 1999] + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. */ int git__bsearch( void **array, @@ -375,11 +548,11 @@ int git__bsearch( int (*compare)(const void *, const void *), size_t *position) { - unsigned int lim; + size_t lim; int cmp = -1; void **part, **base = array; - for (lim = (unsigned int)array_len; lim != 0; lim >>= 1) { + for (lim = array_len; lim != 0; lim >>= 1) { part = base + (lim >> 1); cmp = (*compare)(key, *part); if (cmp == 0) { @@ -395,12 +568,43 @@ int git__bsearch( if (position) *position = (base - array); - return (cmp == 0) ? 0 : -1; + return (cmp == 0) ? 0 : GIT_ENOTFOUND; +} + +int git__bsearch_r( + void **array, + size_t array_len, + const void *key, + int (*compare_r)(const void *, const void *, void *), + void *payload, + size_t *position) +{ + size_t lim; + int cmp = -1; + void **part, **base = array; + + for (lim = array_len; lim != 0; lim >>= 1) { + part = base + (lim >> 1); + cmp = (*compare_r)(key, *part, payload); + if (cmp == 0) { + base = part; + break; + } + if (cmp > 0) { /* key > p; take right partition */ + base = part + 1; + lim--; + } /* else take left partition */ + } + + if (position) + *position = (base - array); + + return (cmp == 0) ? 0 : GIT_ENOTFOUND; } /** * A strcmp wrapper - * + * * We don't want direct pointers to the CRT on Windows, we may * get stdcall conflicts. */ @@ -415,12 +619,8 @@ int git__strcmp_cb(const void *a, const void *b) int git__parse_bool(int *out, const char *value) { /* A missing value means true */ - if (value == NULL) { - *out = 1; - return 0; - } - - if (!strcasecmp(value, "true") || + if (value == NULL || + !strcasecmp(value, "true") || !strcasecmp(value, "yes") || !strcasecmp(value, "on")) { *out = 1; @@ -428,10 +628,82 @@ int git__parse_bool(int *out, const char *value) } if (!strcasecmp(value, "false") || !strcasecmp(value, "no") || - !strcasecmp(value, "off")) { + !strcasecmp(value, "off") || + value[0] == '\0') { *out = 0; return 0; } return -1; } + +size_t git__unescape(char *str) +{ + char *scan, *pos = str; + + for (scan = str; *scan; pos++, scan++) { + if (*scan == '\\' && *(scan + 1) != '\0') + scan++; /* skip '\' but include next char */ + if (pos != scan) + *pos = *scan; + } + + if (pos != scan) { + *pos = '\0'; + } + + return (pos - str); +} + +#if defined(GIT_WIN32) || defined(BSD) +typedef struct { + git__sort_r_cmp cmp; + void *payload; +} git__qsort_r_glue; + +static int GIT_STDLIB_CALL git__qsort_r_glue_cmp( + void *payload, const void *a, const void *b) +{ + git__qsort_r_glue *glue = payload; + return glue->cmp(a, b, glue->payload); +} +#endif + +void git__qsort_r( + void *els, size_t nel, size_t elsize, git__sort_r_cmp cmp, void *payload) +{ +#if defined(__MINGW32__) || defined(__OpenBSD__) + git__insertsort_r(els, nel, elsize, NULL, cmp, payload); +#elif defined(GIT_WIN32) + git__qsort_r_glue glue = { cmp, payload }; + qsort_s(els, nel, elsize, git__qsort_r_glue_cmp, &glue); +#elif defined(BSD) + git__qsort_r_glue glue = { cmp, payload }; + qsort_r(els, nel, elsize, &glue, git__qsort_r_glue_cmp); +#else + qsort_r(els, nel, elsize, cmp, payload); +#endif +} + +void git__insertsort_r( + void *els, size_t nel, size_t elsize, void *swapel, + git__sort_r_cmp cmp, void *payload) +{ + uint8_t *base = els; + uint8_t *end = base + nel * elsize; + uint8_t *i, *j; + bool freeswap = !swapel; + + if (freeswap) + swapel = git__malloc(elsize); + + for (i = base + elsize; i < end; i += elsize) + for (j = i; j > base && cmp(j, j - elsize, payload) < 0; j -= elsize) { + memcpy(swapel, j, elsize); + memcpy(j, j - elsize, elsize); + memcpy(j - elsize, swapel, elsize); + } + + if (freeswap) + git__free(swapel); +} diff --git a/src/util.h b/src/util.h index c6851ac7e..c0f271997 100644 --- a/src/util.h +++ b/src/util.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -13,6 +13,9 @@ #ifndef min # define min(a,b) ((a) < (b) ? (a) : (b)) #endif +#ifndef max +# define max(a,b) ((a) > (b) ? (a) : (b)) +#endif /* * Custom memory allocation wrappers @@ -42,25 +45,31 @@ GIT_INLINE(char *) git__strdup(const char *str) GIT_INLINE(char *) git__strndup(const char *str, size_t n) { - size_t length; + size_t length = 0; char *ptr; - length = strlen(str); - if (n < length) - length = n; + while (length < n && str[length]) + ++length; - ptr = (char*)malloc(length + 1); - if (!ptr) { - giterr_set_oom(); - return NULL; - } + ptr = (char*)git__malloc(length + 1); + + if (length) + memcpy(ptr, str, length); - memcpy(ptr, str, length); ptr[length] = '\0'; return ptr; } +/* NOTE: This doesn't do null or '\0' checking. Watch those boundaries! */ +GIT_INLINE(char *) git__substrdup(const char *start, size_t n) +{ + char *ptr = (char*)git__malloc(n+1); + memcpy(ptr, start, n); + ptr[n] = '\0'; + return ptr; +} + GIT_INLINE(void *) git__realloc(void *ptr, size_t size) { void *new_ptr = realloc(ptr, size); @@ -70,9 +79,21 @@ GIT_INLINE(void *) git__realloc(void *ptr, size_t size) #define git__free(ptr) free(ptr) +#define STRCMP_CASESELECT(IGNORE_CASE, STR1, STR2) \ + ((IGNORE_CASE) ? strcasecmp((STR1), (STR2)) : strcmp((STR1), (STR2))) + +#define CASESELECT(IGNORE_CASE, ICASE, CASE) \ + ((IGNORE_CASE) ? (ICASE) : (CASE)) + extern int git__prefixcmp(const char *str, const char *prefix); +extern int git__prefixcmp_icase(const char *str, const char *prefix); extern int git__suffixcmp(const char *str, const char *suffix); +GIT_INLINE(int) git__signum(int val) +{ + return ((val > 0) - (val < 0)); +} + extern int git__strtol32(int32_t *n, const char *buff, const char **end_buf, int base); extern int git__strtol64(int64_t *n, const char *buff, const char **end_buf, int base); @@ -94,6 +115,7 @@ GIT_INLINE(int) git__is_sizet(git_off_t p) #endif extern char *git__strtok(char **end, const char *sep); +extern char *git__strsep(char **end, const char *sep); extern void git__strntolower(char *str, size_t len); extern void git__strtolower(char *str); @@ -105,22 +127,64 @@ GIT_INLINE(const char *) git__next_line(const char *s) return s; } -extern void git__tsort(void **dst, size_t size, int (*cmp)(const void *, const void *)); +GIT_INLINE(const void *) git__memrchr(const void *s, int c, size_t n) +{ + const unsigned char *cp; + + if (n != 0) { + cp = (unsigned char *)s + n; + do { + if (*(--cp) == (unsigned char)c) + return cp; + } while (--n != 0); + } + + return NULL; +} + +typedef int (*git__tsort_cmp)(const void *a, const void *b); + +extern void git__tsort(void **dst, size_t size, git__tsort_cmp cmp); + +typedef int (*git__sort_r_cmp)(const void *a, const void *b, void *payload); + +extern void git__tsort_r( + void **dst, size_t size, git__sort_r_cmp cmp, void *payload); + +extern void git__qsort_r( + void *els, size_t nel, size_t elsize, git__sort_r_cmp cmp, void *payload); + +extern void git__insertsort_r( + void *els, size_t nel, size_t elsize, void *swapel, + git__sort_r_cmp cmp, void *payload); /** * @param position If non-NULL, this will be set to the position where the * element is or would be inserted if not found. - * @return pos (>=0) if found or -1 if not found + * @return 0 if found; GIT_ENOTFOUND if not found */ extern int git__bsearch( void **array, size_t array_len, const void *key, - int (*compare)(const void *, const void *), + int (*compare)(const void *key, const void *element), + size_t *position); + +extern int git__bsearch_r( + void **array, + size_t array_len, + const void *key, + int (*compare_r)(const void *key, const void *element, void *payload), + void *payload, size_t *position); extern int git__strcmp_cb(const void *a, const void *b); +extern int git__strcmp(const char *a, const char *b); +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); + typedef struct { short refcount; void *owner; @@ -204,9 +268,14 @@ GIT_INLINE(bool) git__isalpha(int c) return ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')); } +GIT_INLINE(bool) git__isdigit(int c) +{ + return (c >= '0' && c <= '9'); +} + GIT_INLINE(bool) git__isspace(int c) { - return (c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r' || c == '\v'); + return (c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r' || c == '\v' || c == 0x85 /* Unicode CR+LF */); } GIT_INLINE(bool) git__iswildcard(int c) @@ -223,4 +292,23 @@ GIT_INLINE(bool) git__iswildcard(int c) */ extern int git__parse_bool(int *out, const char *value); +/* + * Parse a string into a value as a git_time_t. + * + * Sample valid input: + * - "yesterday" + * - "July 17, 2003" + * - "2003-7-17 08:23" + */ +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); + #endif /* INCLUDE_util_h__ */ diff --git a/src/vector.c b/src/vector.c index 6f9aacccf..f4a818ed2 100644 --- a/src/vector.c +++ b/src/vector.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -9,18 +9,60 @@ #include "repository.h" #include "vector.h" -static const double resize_factor = 1.75; -static const unsigned int minimum_size = 8; +/* In elements, not bytes */ +#define MIN_ALLOCSIZE 8 -static int resize_vector(git_vector *v) +GIT_INLINE(size_t) compute_new_size(git_vector *v) { - v->_alloc_size = ((unsigned int)(v->_alloc_size * resize_factor)) + 1; - if (v->_alloc_size < minimum_size) - v->_alloc_size = minimum_size; + size_t new_size = v->_alloc_size; + + /* Use a resize factor of 1.5, which is quick to compute using integer + * instructions and less than the golden ratio (1.618...) */ + if (new_size < MIN_ALLOCSIZE) + new_size = MIN_ALLOCSIZE; + else if (new_size <= (SIZE_MAX / 3) * 2) + new_size += new_size / 2; + else + new_size = SIZE_MAX; + + return new_size; +} + +GIT_INLINE(int) resize_vector(git_vector *v, size_t new_size) +{ + size_t new_bytes = new_size * sizeof(void *); + void *new_contents; + + /* Check for overflow */ + if (new_bytes / sizeof(void *) != new_size) + GITERR_CHECK_ALLOC(NULL); - v->contents = git__realloc(v->contents, v->_alloc_size * sizeof(void *)); + new_contents = git__realloc(v->contents, new_bytes); + GITERR_CHECK_ALLOC(new_contents); + + v->_alloc_size = new_size; + v->contents = new_contents; + + return 0; +} + +int git_vector_dup(git_vector *v, const git_vector *src, git_vector_cmp cmp) +{ + size_t bytes; + + assert(v && src); + + bytes = src->length * sizeof(void *); + + v->_alloc_size = src->length; + v->_cmp = cmp; + v->length = src->length; + v->sorted = src->sorted && cmp == src->_cmp; + v->contents = git__malloc(bytes); GITERR_CHECK_ALLOC(v->contents); + memcpy(v->contents, src->contents, bytes); + return 0; } @@ -35,25 +77,17 @@ void git_vector_free(git_vector *v) v->_alloc_size = 0; } -int git_vector_init(git_vector *v, unsigned int initial_size, git_vector_cmp cmp) +int git_vector_init(git_vector *v, size_t initial_size, git_vector_cmp cmp) { assert(v); - memset(v, 0x0, sizeof(git_vector)); - - if (initial_size == 0) - initial_size = minimum_size; - - v->_alloc_size = initial_size; + v->_alloc_size = 0; v->_cmp = cmp; - v->length = 0; v->sorted = 1; + v->contents = NULL; - v->contents = git__malloc(v->_alloc_size * sizeof(void *)); - GITERR_CHECK_ALLOC(v->contents); - - return 0; + return resize_vector(v, max(initial_size, MIN_ALLOCSIZE)); } int git_vector_insert(git_vector *v, void *element) @@ -61,7 +95,7 @@ int git_vector_insert(git_vector *v, void *element) assert(v); if (v->length >= v->_alloc_size && - resize_vector(v) < 0) + resize_vector(v, compute_new_size(v)) < 0) return -1; v->contents[v->length++] = element; @@ -82,26 +116,25 @@ int git_vector_insert_sorted( git_vector_sort(v); if (v->length >= v->_alloc_size && - resize_vector(v) < 0) + resize_vector(v, compute_new_size(v)) < 0) return -1; /* If we find the element and have a duplicate handler callback, * invoke it. If it returns non-zero, then cancel insert, otherwise * proceed with normal insert. */ - if (git__bsearch(v->contents, v->length, element, v->_cmp, &pos) >= 0 && - on_dup != NULL && - (result = on_dup(&v->contents[pos], element)) < 0) + if (!git__bsearch(v->contents, v->length, element, v->_cmp, &pos) && + on_dup && (result = on_dup(&v->contents[pos], element)) < 0) return result; /* shift elements to the right */ - if (pos < v->length) { + if (pos < v->length) memmove(v->contents + pos + 1, v->contents + pos, (v->length - pos) * sizeof(void *)); - } v->contents[pos] = element; v->length++; + return 0; } @@ -109,47 +142,44 @@ void git_vector_sort(git_vector *v) { assert(v); - if (v->sorted || v->_cmp == NULL) + if (v->sorted || !v->_cmp) return; git__tsort(v->contents, v->length, v->_cmp); v->sorted = 1; } -int git_vector_bsearch3( - unsigned int *at_pos, +int git_vector_bsearch2( + size_t *at_pos, git_vector *v, git_vector_cmp key_lookup, const void *key) { - int rval; - size_t pos; - assert(v && key && key_lookup); /* need comparison function to sort the vector */ - assert(v->_cmp != NULL); + if (!v->_cmp) + return -1; git_vector_sort(v); - rval = git__bsearch(v->contents, v->length, key, key_lookup, &pos); - - if (at_pos != NULL) - *at_pos = (unsigned int)pos; - - return (rval >= 0) ? (int)pos : GIT_ENOTFOUND; + return git__bsearch(v->contents, v->length, key, key_lookup, at_pos); } int git_vector_search2( - git_vector *v, git_vector_cmp key_lookup, const void *key) + size_t *at_pos, const git_vector *v, git_vector_cmp key_lookup, const void *key) { - unsigned int i; + size_t i; assert(v && key && key_lookup); for (i = 0; i < v->length; ++i) { - if (key_lookup(key, v->contents[i]) == 0) - return i; + if (key_lookup(key, v->contents[i]) == 0) { + if (at_pos) + *at_pos = i; + + return 0; + } } return GIT_ENOTFOUND; @@ -160,22 +190,25 @@ static int strict_comparison(const void *a, const void *b) return (a == b) ? 0 : -1; } -int git_vector_search(git_vector *v, const void *entry) +int git_vector_search(size_t *at_pos, const git_vector *v, const void *entry) { - return git_vector_search2(v, v->_cmp ? v->_cmp : strict_comparison, entry); + return git_vector_search2(at_pos, v, v->_cmp ? v->_cmp : strict_comparison, entry); } -int git_vector_remove(git_vector *v, unsigned int idx) +int git_vector_remove(git_vector *v, size_t idx) { - unsigned int i; + size_t shift_count; assert(v); - if (idx >= v->length || v->length == 0) + if (idx >= v->length) return GIT_ENOTFOUND; - for (i = idx; i < v->length - 1; ++i) - v->contents[i] = v->contents[i + 1]; + shift_count = v->length - idx - 1; + + if (shift_count) + memmove(&v->contents[idx], &v->contents[idx + 1], + shift_count * sizeof(void *)); v->length--; return 0; @@ -190,7 +223,7 @@ void git_vector_pop(git_vector *v) void git_vector_uniq(git_vector *v) { git_vector_cmp cmp; - unsigned int i, j; + size_t i, j; if (v->length <= 1) return; @@ -207,6 +240,21 @@ void git_vector_uniq(git_vector *v) v->length -= j - i - 1; } +void git_vector_remove_matching( + git_vector *v, int (*match)(const git_vector *v, size_t idx)) +{ + size_t i, j; + + for (i = 0, j = 0; j < v->length; ++j) { + v->contents[i] = v->contents[j]; + + if (!match(v, i)) + i++; + } + + v->length = i; +} + void git_vector_clear(git_vector *v) { assert(v); @@ -218,10 +266,41 @@ void git_vector_swap(git_vector *a, git_vector *b) { git_vector t; - if (!a || !b || a == b) - return; + assert(a && b); - memcpy(&t, a, sizeof(t)); - memcpy(a, b, sizeof(t)); - memcpy(b, &t, sizeof(t)); + if (a != b) { + memcpy(&t, a, sizeof(t)); + memcpy(a, b, sizeof(t)); + memcpy(b, &t, sizeof(t)); + } +} + +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)); + + v->length = new_length; + + return 0; +} + +int git_vector_set(void **old, git_vector *v, size_t position, void *value) +{ + if (git_vector_resize_to(v, position + 1) < 0) + return -1; + + if (old != NULL) + *old = v->contents[position]; + + v->contents[position] = value; + + return 0; } diff --git a/src/vector.h b/src/vector.h index 9139db345..e2f729b83 100644 --- a/src/vector.h +++ b/src/vector.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -12,52 +12,50 @@ typedef int (*git_vector_cmp)(const void *, const void *); typedef struct git_vector { - unsigned int _alloc_size; + size_t _alloc_size; git_vector_cmp _cmp; void **contents; - unsigned int length; + size_t length; int sorted; } git_vector; #define GIT_VECTOR_INIT {0} -int git_vector_init(git_vector *v, unsigned int initial_size, git_vector_cmp cmp); +int git_vector_init(git_vector *v, size_t initial_size, git_vector_cmp cmp); void git_vector_free(git_vector *v); void git_vector_clear(git_vector *v); +int git_vector_dup(git_vector *v, const git_vector *src, git_vector_cmp cmp); void git_vector_swap(git_vector *a, git_vector *b); void git_vector_sort(git_vector *v); -int git_vector_search(git_vector *v, const void *entry); -int git_vector_search2(git_vector *v, git_vector_cmp cmp, const void *key); +/** Linear search for matching entry using internal comparison function */ +int git_vector_search(size_t *at_pos, const git_vector *v, const void *entry); -int git_vector_bsearch3( - unsigned int *at_pos, git_vector *v, git_vector_cmp cmp, const void *key); +/** Linear search for matching entry using explicit comparison function */ +int git_vector_search2(size_t *at_pos, const git_vector *v, git_vector_cmp cmp, const void *key); -GIT_INLINE(int) git_vector_bsearch(git_vector *v, const void *key) -{ - return git_vector_bsearch3(NULL, v, v->_cmp, key); -} - -GIT_INLINE(int) git_vector_bsearch2( - git_vector *v, git_vector_cmp cmp, const void *key) -{ - return git_vector_bsearch3(NULL, v, cmp, key); -} +/** + * Binary search for matching entry using explicit comparison function that + * returns position where item would go if not found. + */ +int git_vector_bsearch2( + size_t *at_pos, git_vector *v, git_vector_cmp cmp, const void *key); -GIT_INLINE(void *) git_vector_get(git_vector *v, unsigned int position) +/** Binary search for matching entry using internal comparison function */ +GIT_INLINE(int) git_vector_bsearch(size_t *at_pos, git_vector *v, const void *key) { - return (position < v->length) ? v->contents[position] : NULL; + return git_vector_bsearch2(at_pos, v, v->_cmp, key); } -GIT_INLINE(const void *) git_vector_get_const(const git_vector *v, unsigned int position) +GIT_INLINE(void *) git_vector_get(const git_vector *v, size_t position) { return (position < v->length) ? v->contents[position] : NULL; } #define GIT_VECTOR_GET(V,I) ((I) < (V)->length ? (V)->contents[(I)] : NULL) -GIT_INLINE(void *) git_vector_last(git_vector *v) +GIT_INLINE(void *) git_vector_last(const git_vector *v) { return (v->length > 0) ? git_vector_get(v, v->length - 1) : NULL; } @@ -66,13 +64,18 @@ GIT_INLINE(void *) git_vector_last(git_vector *v) for ((iter) = 0; (iter) < (v)->length && ((elem) = (v)->contents[(iter)], 1); (iter)++ ) #define git_vector_rforeach(v, iter, elem) \ - for ((iter) = (v)->length; (iter) > 0 && ((elem) = (v)->contents[(iter)-1], 1); (iter)-- ) + for ((iter) = (v)->length - 1; (iter) < SIZE_MAX && ((elem) = (v)->contents[(iter)], 1); (iter)-- ) int git_vector_insert(git_vector *v, void *element); int git_vector_insert_sorted(git_vector *v, void *element, int (*on_dup)(void **old, void *new)); -int git_vector_remove(git_vector *v, unsigned int idx); +int git_vector_remove(git_vector *v, size_t idx); void git_vector_pop(git_vector *v); void git_vector_uniq(git_vector *v); +void git_vector_remove_matching( + git_vector *v, int (*match)(const git_vector *v, size_t idx)); + +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); #endif diff --git a/src/win32/dir.c b/src/win32/dir.c index bc3d40fa5..95ae5060e 100644 --- a/src/win32/dir.c +++ b/src/win32/dir.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -7,7 +7,6 @@ #define GIT__WIN32_NO_WRAP_DIR #include "dir.h" #include "utf-conv.h" -#include "git2/windows.h" static int init_filter(char *filter, size_t n, const char *dir) { @@ -26,8 +25,8 @@ static int init_filter(char *filter, size_t n, const char *dir) git__DIR *git__opendir(const char *dir) { - char filter[4096]; - wchar_t* filter_w = NULL; + char filter[GIT_WIN_PATH]; + wchar_t filter_w[GIT_WIN_PATH]; git__DIR *new = NULL; if (!dir || !init_filter(filter, sizeof(filter), dir)) @@ -41,12 +40,8 @@ git__DIR *git__opendir(const char *dir) if (!new->dir) goto fail; - filter_w = gitwin_to_utf16(filter); - if (!filter_w) - goto fail; - + git__utf8_to_16(filter_w, GIT_WIN_PATH, filter); new->h = FindFirstFileW(filter_w, &new->f); - git__free(filter_w); if (new->h == INVALID_HANDLE_VALUE) { giterr_set(GITERR_OS, "Could not open directory '%s'", dir); @@ -85,16 +80,9 @@ int git__readdir_ext( if (wcslen(d->f.cFileName) >= sizeof(entry->d_name)) return -1; + git__utf16_to_8(entry->d_name, d->f.cFileName); entry->d_ino = 0; - if (WideCharToMultiByte( - gitwin_get_codepage(), 0, d->f.cFileName, -1, - entry->d_name, GIT_PATH_MAX, NULL, NULL) == 0) - { - giterr_set(GITERR_OS, "Could not convert filename to UTF-8"); - return -1; - } - *result = entry; if (is_dir != NULL) @@ -113,8 +101,8 @@ struct git__dirent *git__readdir(git__DIR *d) void git__rewinddir(git__DIR *d) { - char filter[4096]; - wchar_t* filter_w; + char filter[GIT_WIN_PATH]; + wchar_t filter_w[GIT_WIN_PATH]; if (!d) return; @@ -125,12 +113,11 @@ void git__rewinddir(git__DIR *d) d->first = 0; } - if (!init_filter(filter, sizeof(filter), d->dir) || - (filter_w = gitwin_to_utf16(filter)) == NULL) + if (!init_filter(filter, sizeof(filter), d->dir)) return; + git__utf8_to_16(filter_w, GIT_WIN_PATH, filter); d->h = FindFirstFileW(filter_w, &d->f); - git__free(filter_w); if (d->h == INVALID_HANDLE_VALUE) giterr_set(GITERR_OS, "Could not open directory '%s'", d->dir); diff --git a/src/win32/dir.h b/src/win32/dir.h index c816d79bb..7696d468e 100644 --- a/src/win32/dir.h +++ b/src/win32/dir.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. diff --git a/src/win32/error.c b/src/win32/error.c new file mode 100644 index 000000000..4a9a0631f --- /dev/null +++ b/src/win32/error.c @@ -0,0 +1,77 @@ +/* + * 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 "error.h" + +#ifdef GIT_WINHTTP +# include <winhttp.h> +#endif + +#define WC_ERR_INVALID_CHARS 0x80 + +char *git_win32_get_error_message(DWORD error_code) +{ + LPWSTR lpMsgBuf = NULL; + HMODULE hModule = NULL; + char *utf8_msg = NULL; + int utf8_size; + DWORD dwFlags = + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS; + + if (!error_code) + return NULL; + +#ifdef GIT_WINHTTP + /* Errors raised by WinHTTP are not in the system resource table */ + if (error_code >= WINHTTP_ERROR_BASE && + error_code <= WINHTTP_ERROR_LAST) + hModule = GetModuleHandleW(L"winhttp"); +#endif + + GIT_UNUSED(hModule); + + if (hModule) + dwFlags |= FORMAT_MESSAGE_FROM_HMODULE; + else + dwFlags |= FORMAT_MESSAGE_FROM_SYSTEM; + + if (FormatMessageW(dwFlags, hModule, error_code, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPWSTR)&lpMsgBuf, 0, NULL)) { + + /* Invalid code point check supported on Vista+ only */ + if (git_has_win32_version(6, 0)) + dwFlags = WC_ERR_INVALID_CHARS; + else + dwFlags = 0; + + utf8_size = WideCharToMultiByte(CP_UTF8, dwFlags, + lpMsgBuf, -1, NULL, 0, NULL, NULL); + + if (!utf8_size) { + assert(0); + goto on_error; + } + + utf8_msg = git__malloc(utf8_size); + + if (!utf8_msg) + goto on_error; + + if (!WideCharToMultiByte(CP_UTF8, dwFlags, + lpMsgBuf, -1, utf8_msg, utf8_size, NULL, NULL)) { + git__free(utf8_msg); + goto on_error; + } + +on_error: + LocalFree(lpMsgBuf); + } + + return utf8_msg; +} diff --git a/src/win32/error.h b/src/win32/error.h new file mode 100644 index 000000000..12947a2e6 --- /dev/null +++ b/src/win32/error.h @@ -0,0 +1,13 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_git_win32_error_h__ +#define INCLUDE_git_win32_error_h__ + +extern char *git_win32_get_error_message(DWORD error_code); + +#endif diff --git a/src/win32/findfile.c b/src/win32/findfile.c new file mode 100644 index 000000000..bc36b6b45 --- /dev/null +++ b/src/win32/findfile.c @@ -0,0 +1,238 @@ +/* + * 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 "utf-conv.h" +#include "path.h" +#include "findfile.h" + +#define REG_MSYSGIT_INSTALL_LOCAL L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Git_is1" + +#ifndef _WIN64 +#define REG_MSYSGIT_INSTALL REG_MSYSGIT_INSTALL_LOCAL +#else +#define REG_MSYSGIT_INSTALL L"SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Git_is1" +#endif + +int git_win32__expand_path(struct git_win32__path *s_root, const wchar_t *templ) +{ + s_root->len = ExpandEnvironmentStringsW(templ, s_root->path, MAX_PATH); + return s_root->len ? 0 : -1; +} + +static int win32_path_utf16_to_8(git_buf *path_utf8, const wchar_t *path_utf16) +{ + char temp_utf8[GIT_PATH_MAX]; + + git__utf16_to_8(temp_utf8, path_utf16); + git_path_mkposix(temp_utf8); + + return git_buf_sets(path_utf8, temp_utf8); +} + +int git_win32__find_file( + git_buf *path, const struct git_win32__path *root, const char *filename) +{ + size_t len, alloc_len; + wchar_t *file_utf16 = NULL; + + if (!root || !filename || (len = strlen(filename)) == 0) + return GIT_ENOTFOUND; + + /* allocate space for wchar_t path to file */ + alloc_len = root->len + len + 2; + file_utf16 = git__calloc(alloc_len, sizeof(wchar_t)); + GITERR_CHECK_ALLOC(file_utf16); + + /* append root + '\\' + filename as wchar_t */ + memcpy(file_utf16, root->path, root->len * sizeof(wchar_t)); + + if (*filename == '/' || *filename == '\\') + filename++; + + git__utf8_to_16(file_utf16 + root->len - 1, alloc_len, filename); + + /* check access */ + if (_waccess(file_utf16, F_OK) < 0) { + git__free(file_utf16); + return GIT_ENOTFOUND; + } + + win32_path_utf16_to_8(path, file_utf16); + git__free(file_utf16); + + return 0; +} + +static wchar_t* win32_walkpath(wchar_t *path, wchar_t *buf, size_t buflen) +{ + wchar_t term, *base = path; + + assert(path && buf && buflen); + + term = (*path == L'"') ? *path++ : L';'; + + for (buflen--; *path && *path != term && buflen; buflen--) + *buf++ = *path++; + + *buf = L'\0'; /* reserved a byte via initial subtract */ + + while (*path == term || *path == L';') + path++; + + return (path != base) ? path : NULL; +} + +static int win32_find_git_in_path(git_buf *buf, const wchar_t *gitexe) +{ + wchar_t *env = _wgetenv(L"PATH"), lastch; + struct git_win32__path root; + size_t gitexe_len = wcslen(gitexe); + + if (!env) + return -1; + + while ((env = win32_walkpath(env, root.path, MAX_PATH-1)) && *root.path) { + root.len = (DWORD)wcslen(root.path); + lastch = root.path[root.len - 1]; + + /* ensure trailing slash (MAX_PATH-1 to walkpath guarantees space) */ + if (lastch != L'/' && lastch != L'\\') { + root.path[root.len++] = L'\\'; + root.path[root.len] = L'\0'; + } + + if (root.len + gitexe_len >= MAX_PATH) + continue; + wcscpy(&root.path[root.len], gitexe); + + if (_waccess(root.path, F_OK) == 0 && root.len > 5) { + /* replace "bin\\" or "cmd\\" with "etc\\" */ + wcscpy(&root.path[root.len - 4], L"etc\\"); + + win32_path_utf16_to_8(buf, root.path); + return 0; + } + } + + return GIT_ENOTFOUND; +} + +static int win32_find_git_in_registry( + git_buf *buf, const HKEY hieve, const wchar_t *key) +{ + HKEY hKey; + DWORD dwType = REG_SZ; + struct git_win32__path path16; + + assert(buf); + + path16.len = 0; + + if (RegOpenKeyExW(hieve, key, 0, KEY_ALL_ACCESS, &hKey) == ERROR_SUCCESS) { + if (RegQueryValueExW(hKey, L"InstallLocation", NULL, &dwType, + (LPBYTE)&path16.path, &path16.len) == ERROR_SUCCESS) + { + /* InstallLocation points to the root of the git directory */ + + if (path16.len + 4 > MAX_PATH) { /* 4 = wcslen(L"etc\\") */ + giterr_set(GITERR_OS, "Cannot locate git - path too long"); + return -1; + } + + wcscat(path16.path, L"etc\\"); + path16.len += 4; + + win32_path_utf16_to_8(buf, path16.path); + } + + RegCloseKey(hKey); + } + + return path16.len ? 0 : GIT_ENOTFOUND; +} + +static int win32_find_existing_dirs( + git_buf *out, const wchar_t *tmpl[], char *temp[]) +{ + struct git_win32__path path16; + git_buf buf = GIT_BUF_INIT; + + git_buf_clear(out); + + for (; *tmpl != NULL; tmpl++) { + if (!git_win32__expand_path(&path16, *tmpl) && + path16.path[0] != L'%' && + !_waccess(path16.path, F_OK)) + { + win32_path_utf16_to_8(&buf, path16.path); + + if (buf.size) + git_buf_join(out, GIT_PATH_LIST_SEPARATOR, out->ptr, buf.ptr); + } + } + + git_buf_free(&buf); + + return (git_buf_oom(out) ? -1 : 0); +} + +int git_win32__find_system_dirs(git_buf *out) +{ + git_buf buf = GIT_BUF_INIT; + + /* directories where git.exe & git.cmd are found */ + if (!win32_find_git_in_path(&buf, L"git.exe") && buf.size) + git_buf_set(out, buf.ptr, buf.size); + else + git_buf_clear(out); + + if (!win32_find_git_in_path(&buf, L"git.cmd") && buf.size) + git_buf_join(out, GIT_PATH_LIST_SEPARATOR, out->ptr, buf.ptr); + + /* directories where git is installed according to registry */ + if (!win32_find_git_in_registry( + &buf, HKEY_CURRENT_USER, REG_MSYSGIT_INSTALL_LOCAL) && buf.size) + git_buf_join(out, GIT_PATH_LIST_SEPARATOR, out->ptr, buf.ptr); + + if (!win32_find_git_in_registry( + &buf, HKEY_LOCAL_MACHINE, REG_MSYSGIT_INSTALL) && buf.size) + git_buf_join(out, GIT_PATH_LIST_SEPARATOR, out->ptr, buf.ptr); + + git_buf_free(&buf); + + return (git_buf_oom(out) ? -1 : 0); +} + +int git_win32__find_global_dirs(git_buf *out) +{ + char *temp[3]; + static const wchar_t *global_tmpls[4] = { + L"%HOME%\\", + L"%HOMEDRIVE%%HOMEPATH%\\", + L"%USERPROFILE%\\", + NULL, + }; + + return win32_find_existing_dirs(out, global_tmpls, temp); +} + +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", + L"%LOCALAPPDATA%\\git", + L"%HOME%\\.config\\git", + L"%HOMEDRIVE%%HOMEPATH%\\.config\\git", + L"%USERPROFILE%\\.config\\git", + NULL, + }; + + return win32_find_existing_dirs(out, global_tmpls, temp); +} + diff --git a/src/win32/findfile.h b/src/win32/findfile.h new file mode 100644 index 000000000..fc79e1b72 --- /dev/null +++ b/src/win32/findfile.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_git_findfile_h__ +#define INCLUDE_git_findfile_h__ + +struct git_win32__path { + wchar_t path[MAX_PATH]; + DWORD len; +}; + +extern int git_win32__expand_path( + struct git_win32__path *s_root, const wchar_t *templ); + +extern int git_win32__find_file( + git_buf *path, const struct git_win32__path *root, const char *filename); + +extern int git_win32__find_system_dirs(git_buf *out); +extern int git_win32__find_global_dirs(git_buf *out); +extern int git_win32__find_xdg_dirs(git_buf *out); + +#endif + diff --git a/src/win32/git2.rc b/src/win32/git2.rc index 3a65c0a0f..436913228 100644 --- a/src/win32/git2.rc +++ b/src/win32/git2.rc @@ -12,13 +12,13 @@ VS_VERSION_INFO VERSIONINFO MOVEABLE IMPURE LOADONCALL DISCARDABLE PRODUCTVERSION LIBGIT2_VER_MAJOR,LIBGIT2_VER_MINOR,LIBGIT2_VER_REVISION,0 FILEFLAGSMASK VS_FFI_FILEFLAGSMASK #ifdef _DEBUG - FILEFLAGS 1 + FILEFLAGS VS_FF_DEBUG #else FILEFLAGS 0 #endif - FILEOS VOS__WINDOWS32 + FILEOS VOS_NT_WINDOWS32 FILETYPE VFT_DLL - FILESUBTYPE 0 // not used + FILESUBTYPE VFT2_UNKNOWN BEGIN BLOCK "StringFileInfo" BEGIN @@ -28,7 +28,7 @@ BEGIN VALUE "FileDescription", "libgit2 - the Git linkable library\0" VALUE "FileVersion", LIBGIT2_VERSION "\0" VALUE "InternalName", LIBGIT2_FILENAME "\0" - VALUE "LegalCopyright", "Copyright (C) 2009-2012 the libgit2 contributors\0" + VALUE "LegalCopyright", "Copyright (C) the libgit2 contributors. All rights reserved.\0" VALUE "OriginalFilename", LIBGIT2_FILENAME "\0" VALUE "ProductName", "libgit2\0" VALUE "ProductVersion", LIBGIT2_VERSION "\0" diff --git a/src/win32/map.c b/src/win32/map.c index f730120cc..44c6c4e2e 100644 --- a/src/win32/map.c +++ b/src/win32/map.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. diff --git a/src/win32/mingw-compat.h b/src/win32/mingw-compat.h index 6200dc094..7b97b48db 100644 --- a/src/win32/mingw-compat.h +++ b/src/win32/mingw-compat.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. diff --git a/src/win32/msvc-compat.h b/src/win32/msvc-compat.h index 3ef09c85b..50865ed17 100644 --- a/src/win32/msvc-compat.h +++ b/src/win32/msvc-compat.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -21,6 +21,7 @@ /* stat: file mode type testing macros */ # define _S_IFLNK 0120000 # define S_IFLNK _S_IFLNK +# define S_IXUSR 00100 # define S_ISDIR(m) (((m) & _S_IFMT) == _S_IFDIR) # define S_ISREG(m) (((m) & _S_IFMT) == _S_IFREG) @@ -36,6 +37,15 @@ /* MSVC doesn't define ssize_t at all */ typedef SSIZE_T ssize_t; +/* define snprintf using variadic macro support if available */ +#if _MSC_VER >= 1400 +# define snprintf(BUF, SZ, FMT, ...) _snprintf_s(BUF, SZ, _TRUNCATE, FMT, __VA_ARGS__) +#else +# define snprintf _snprintf #endif +#endif + +#define GIT_STDLIB_CALL __cdecl + #endif /* INCLUDE_msvc_compat__ */ diff --git a/src/win32/posix.h b/src/win32/posix.h index baa4a3b4e..c49c2175c 100644 --- a/src/win32/posix.h +++ b/src/win32/posix.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -8,7 +8,6 @@ #define INCLUDE_posix__w32_h__ #include "common.h" -#include "compat/fnmatch.h" #include "utf-conv.h" GIT_INLINE(int) p_link(const char *old, const char *new) @@ -21,18 +20,16 @@ GIT_INLINE(int) p_link(const char *old, const char *new) GIT_INLINE(int) p_mkdir(const char *path, mode_t mode) { - wchar_t* buf = gitwin_to_utf16(path); - int ret = _wmkdir(buf); - + wchar_t buf[GIT_WIN_PATH]; GIT_UNUSED(mode); - - git__free(buf); - return ret; + git__utf8_to_16(buf, GIT_WIN_PATH, path); + return _wmkdir(buf); } extern int p_unlink(const char *path); extern int p_lstat(const char *file_name, struct stat *buf); extern int p_readlink(const char *link, char *target, size_t target_len); +extern int p_symlink(const char *old, const char *new); extern int p_hide_directory__w32(const char *path); extern char *p_realpath(const char *orig_path, char *buffer); extern int p_vsnprintf(char *buffer, size_t count, const char *format, va_list argptr); @@ -51,5 +48,14 @@ extern int p_getcwd(char *buffer_out, size_t size); extern int p_rename(const char *from, const char *to); extern int p_recv(GIT_SOCKET socket, void *buffer, size_t length, int flags); extern int p_send(GIT_SOCKET socket, const void *buffer, size_t length, int flags); +extern int p_inet_pton(int af, const char* src, void* dst); + +/* p_lstat is almost but not quite POSIX correct. Specifically, the use of + * ENOTDIR is wrong, in that it does not mean precisely that a non-directory + * entry was encountered. Making it correct is potentially expensive, + * however, so this is a separate version of p_lstat to use when correct + * POSIX ENOTDIR semantics is required. + */ +extern int p_lstat_posixly(const char *filename, struct stat *buf); #endif diff --git a/src/win32/posix_w32.c b/src/win32/posix_w32.c index 10de70da8..4d56299f7 100644 --- a/src/win32/posix_w32.c +++ b/src/win32/posix_w32.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -7,23 +7,18 @@ #include "../posix.h" #include "path.h" #include "utf-conv.h" +#include "repository.h" #include <errno.h> #include <io.h> #include <fcntl.h> - +#include <ws2tcpip.h> int p_unlink(const char *path) { - int ret = 0; - wchar_t* buf; - - if ((buf = gitwin_to_utf16(path)) != NULL) { - _wchmod(buf, 0666); - ret = _wunlink(buf); - git__free(buf); - } - - return ret; + wchar_t buf[GIT_WIN_PATH]; + git__utf8_to_16(buf, GIT_WIN_PATH, path); + _wchmod(buf, 0666); + return _wunlink(buf); } int p_fsync(int fd) @@ -57,16 +52,32 @@ GIT_INLINE(time_t) filetime_to_time_t(const FILETIME *ft) return (time_t)winTime; } -static int do_lstat(const char *file_name, struct stat *buf) +#define WIN32_IS_WSEP(CH) ((CH) == L'/' || (CH) == L'\\') + +static int do_lstat( + const char *file_name, struct stat *buf, int posix_enotdir) { WIN32_FILE_ATTRIBUTE_DATA fdata; - wchar_t* fbuf = gitwin_to_utf16(file_name); - if (!fbuf) - return -1; + wchar_t fbuf[GIT_WIN_PATH], lastch; + int flen; + + flen = git__utf8_to_16(fbuf, GIT_WIN_PATH, file_name); + + /* truncate trailing slashes */ + for (; flen > 0; --flen) { + lastch = fbuf[flen - 1]; + if (WIN32_IS_WSEP(lastch)) + fbuf[flen - 1] = L'\0'; + else if (lastch != L'\0') + break; + } if (GetFileAttributesExW(fbuf, GetFileExInfoStandard, &fdata)) { int fMode = S_IREAD; + if (!buf) + return 0; + if (fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) fMode |= S_IFDIR; else @@ -89,44 +100,59 @@ static int do_lstat(const char *file_name, struct stat *buf) buf->st_mtime = filetime_to_time_t(&(fdata.ftLastWriteTime)); buf->st_ctime = filetime_to_time_t(&(fdata.ftCreationTime)); - git__free(fbuf); - return 0; - } + /* Windows symlinks have zero file size, call readlink to determine + * the length of the path pointed to, which we expect everywhere else + */ + if (S_ISLNK(fMode)) { + char target[GIT_WIN_PATH]; + int readlink_result; - git__free(fbuf); - return -1; -} + readlink_result = p_readlink(file_name, target, GIT_WIN_PATH); -int p_lstat(const char *file_name, struct stat *buf) -{ - int error; - size_t namelen; - char *alt_name; + if (readlink_result == -1) + return -1; - if (do_lstat(file_name, buf) == 0) - return 0; + buf->st_size = strlen(target); + } - /* if file_name ended in a '/', Windows returned ENOENT; - * try again without trailing slashes - */ - namelen = strlen(file_name); - if (namelen && file_name[namelen-1] != '/') - return -1; + return 0; + } - while (namelen && file_name[namelen-1] == '/') - --namelen; + errno = ENOENT; - if (!namelen) - return -1; + /* We need POSIX behavior, then ENOTDIR must set when any of the folders in the + * file path is a regular file,otherwise ENOENT must be set. + */ + if (posix_enotdir) { + /* scan up path until we find an existing item */ + while (1) { + /* remove last directory component */ + for (--flen; flen > 0 && !WIN32_IS_WSEP(fbuf[flen]); --flen); + + if (flen <= 0) + break; + + fbuf[flen] = L'\0'; + + if (GetFileAttributesExW(fbuf, GetFileExInfoStandard, &fdata)) { + if (!(fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + errno = ENOTDIR; + break; + } + } + } - alt_name = git__strndup(file_name, namelen); - if (!alt_name) - return -1; + return -1; +} - error = do_lstat(alt_name, buf); +int p_lstat(const char *filename, struct stat *buf) +{ + return do_lstat(filename, buf, 0); +} - git__free(alt_name); - return error; +int p_lstat_posixly(const char *filename, struct stat *buf) +{ + return do_lstat(filename, buf, 1); } int p_readlink(const char *link, char *target, size_t target_len) @@ -135,7 +161,7 @@ int p_readlink(const char *link, char *target, size_t target_len) static fpath_func pGetFinalPath = NULL; HANDLE hFile; DWORD dwRet; - wchar_t* link_w; + wchar_t link_w[GIT_WIN_PATH]; wchar_t* target_w; int error = 0; @@ -146,10 +172,10 @@ int p_readlink(const char *link, char *target, size_t target_len) * it is not available in platforms older than Vista */ if (pGetFinalPath == NULL) { - HINSTANCE library = LoadLibrary("kernel32"); + HMODULE module = GetModuleHandle("kernel32"); - if (library != NULL) - pGetFinalPath = (fpath_func)GetProcAddress(library, "GetFinalPathNameByHandleW"); + if (module != NULL) + pGetFinalPath = (fpath_func)GetProcAddress(module, "GetFinalPathNameByHandleW"); if (pGetFinalPath == NULL) { giterr_set(GITERR_OS, @@ -158,8 +184,7 @@ int p_readlink(const char *link, char *target, size_t target_len) } } - link_w = gitwin_to_utf16(link); - GITERR_CHECK_ALLOC(link_w); + git__utf8_to_16(link_w, GIT_WIN_PATH, link); hFile = CreateFileW(link_w, // file to open GENERIC_READ, // open for reading @@ -169,8 +194,6 @@ int p_readlink(const char *link, char *target, size_t target_len) FILE_FLAG_BACKUP_SEMANTICS, // normal file NULL); // no attr. template - git__free(link_w); - if (hFile == INVALID_HANDLE_VALUE) { giterr_set(GITERR_OS, "Cannot open '%s' for reading", link); return -1; @@ -217,46 +240,43 @@ int p_readlink(const char *link, char *target, size_t target_len) return dwRet; } +int p_symlink(const char *old, const char *new) +{ + /* Real symlinks on NTFS require admin privileges. Until this changes, + * libgit2 just creates a text file with the link target in the contents. + */ + return git_futils_fake_symlink(old, new); +} + int p_open(const char *path, int flags, ...) { - int fd; - wchar_t* buf; + wchar_t buf[GIT_WIN_PATH]; mode_t mode = 0; - buf = gitwin_to_utf16(path); - if (!buf) - return -1; + git__utf8_to_16(buf, GIT_WIN_PATH, path); - if (flags & O_CREAT) - { + if (flags & O_CREAT) { va_list arg_list; va_start(arg_list, flags); - mode = va_arg(arg_list, mode_t); + mode = (mode_t)va_arg(arg_list, int); va_end(arg_list); } - fd = _wopen(buf, flags | _O_BINARY, mode); - - git__free(buf); - return fd; + return _wopen(buf, flags | _O_BINARY, mode); } int p_creat(const char *path, mode_t mode) { - int fd; - wchar_t* buf = gitwin_to_utf16(path); - if (!buf) - return -1; - fd = _wopen(buf, _O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY, mode); - git__free(buf); - return fd; + wchar_t buf[GIT_WIN_PATH]; + git__utf8_to_16(buf, GIT_WIN_PATH, path); + return _wopen(buf, _O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY, mode); } int p_getcwd(char *buffer_out, size_t size) { int ret; - wchar_t* buf; + wchar_t *buf; if ((size_t)((int)size) != size) return -1; @@ -275,104 +295,78 @@ int p_getcwd(char *buffer_out, size_t size) int p_stat(const char* path, struct stat* buf) { - return do_lstat(path, buf); + return do_lstat(path, buf, 0); } int p_chdir(const char* path) { - wchar_t* buf = gitwin_to_utf16(path); - int ret; - if (!buf) - return -1; - ret = _wchdir(buf); - git__free(buf); - return ret; + wchar_t buf[GIT_WIN_PATH]; + git__utf8_to_16(buf, GIT_WIN_PATH, path); + return _wchdir(buf); } int p_chmod(const char* path, mode_t mode) { - wchar_t* buf = gitwin_to_utf16(path); - int ret; - if (!buf) - return -1; - ret = _wchmod(buf, mode); - git__free(buf); - return ret; + wchar_t buf[GIT_WIN_PATH]; + git__utf8_to_16(buf, GIT_WIN_PATH, path); + return _wchmod(buf, mode); } int p_rmdir(const char* path) { - wchar_t* buf = gitwin_to_utf16(path); - int ret; - if (!buf) - return -1; - ret = _wrmdir(buf); - git__free(buf); - return ret; + wchar_t buf[GIT_WIN_PATH]; + git__utf8_to_16(buf, GIT_WIN_PATH, path); + return _wrmdir(buf); } int p_hide_directory__w32(const char *path) { - int res; - wchar_t* buf = gitwin_to_utf16(path); - if (!buf) - return -1; - - res = SetFileAttributesW(buf, FILE_ATTRIBUTE_HIDDEN); - git__free(buf); - - return (res != 0) ? 0 : -1; /* MSDN states a "non zero" value indicates a success */ + wchar_t buf[GIT_WIN_PATH]; + git__utf8_to_16(buf, GIT_WIN_PATH, path); + return (SetFileAttributesW(buf, FILE_ATTRIBUTE_HIDDEN) != 0) ? 0 : -1; } char *p_realpath(const char *orig_path, char *buffer) { - int ret, buffer_sz = 0; - wchar_t* orig_path_w = gitwin_to_utf16(orig_path); - wchar_t* buffer_w = (wchar_t*)git__malloc(GIT_PATH_MAX * sizeof(wchar_t)); + int ret; + wchar_t orig_path_w[GIT_WIN_PATH]; + wchar_t buffer_w[GIT_WIN_PATH]; - if (!orig_path_w || !buffer_w) - return NULL; + git__utf8_to_16(orig_path_w, GIT_WIN_PATH, orig_path); - ret = GetFullPathNameW(orig_path_w, GIT_PATH_MAX, buffer_w, NULL); - git__free(orig_path_w); + /* Implicitly use GetCurrentDirectory which can be a threading issue */ + ret = GetFullPathNameW(orig_path_w, GIT_WIN_PATH, buffer_w, NULL); /* According to MSDN, a return value equals to zero means a failure. */ - if (ret == 0 || ret > GIT_PATH_MAX) { + if (ret == 0 || ret > GIT_WIN_PATH) buffer = NULL; - goto done; + + else if (GetFileAttributesW(buffer_w) == INVALID_FILE_ATTRIBUTES) { + buffer = NULL; + errno = ENOENT; } - if (buffer == NULL) { - buffer_sz = WideCharToMultiByte(CP_UTF8, 0, buffer_w, -1, NULL, 0, NULL, NULL); + else if (buffer == NULL) { + int buffer_sz = WideCharToMultiByte( + CP_UTF8, 0, buffer_w, -1, NULL, 0, NULL, NULL); if (!buffer_sz || !(buffer = (char *)git__malloc(buffer_sz)) || - !WideCharToMultiByte(CP_UTF8, 0, buffer_w, -1, buffer, buffer_sz, NULL, NULL)) + !WideCharToMultiByte( + CP_UTF8, 0, buffer_w, -1, buffer, buffer_sz, NULL, NULL)) { git__free(buffer); buffer = NULL; - goto done; - } - } else { - if (!WideCharToMultiByte(CP_UTF8, 0, buffer_w, -1, buffer, GIT_PATH_MAX, NULL, NULL)) { - buffer = NULL; - goto done; } } - if (!git_path_exists(buffer)) - { - if (buffer_sz > 0) - git__free(buffer); - + else if (!WideCharToMultiByte( + CP_UTF8, 0, buffer_w, -1, buffer, GIT_PATH_MAX, NULL, NULL)) buffer = NULL; - errno = ENOENT; - } -done: - git__free(buffer_w); if (buffer) git_path_mkposix(buffer); + return buffer; } @@ -381,7 +375,8 @@ int p_vsnprintf(char *buffer, size_t count, const char *format, va_list argptr) #ifdef _MSC_VER int len; - if (count == 0 || (len = _vsnprintf(buffer, count, format, argptr)) < 0) + if (count == 0 || + (len = _vsnprintf_s(buffer, count, _TRUNCATE, format, argptr)) < 0) return _vscprintf(format, argptr); return len; @@ -414,7 +409,7 @@ int p_mkstemp(char *tmp_path) return -1; #endif - return p_creat(tmp_path, 0744); + return p_creat(tmp_path, 0744); //-V536 } int p_setenv(const char* name, const char* value, int overwrite) @@ -427,32 +422,19 @@ int p_setenv(const char* name, const char* value, int overwrite) int p_access(const char* path, mode_t mode) { - wchar_t *buf = gitwin_to_utf16(path); - int ret; - if (!buf) - return -1; - - ret = _waccess(buf, mode); - git__free(buf); - - return ret; + wchar_t buf[GIT_WIN_PATH]; + git__utf8_to_16(buf, GIT_WIN_PATH, path); + return _waccess(buf, mode); } int p_rename(const char *from, const char *to) { - wchar_t *wfrom = gitwin_to_utf16(from); - wchar_t *wto = gitwin_to_utf16(to); - int ret; - - if (!wfrom || !wto) - return -1; - - ret = MoveFileExW(wfrom, wto, MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED) ? 0 : -1; - - git__free(wfrom); - git__free(wto); + wchar_t wfrom[GIT_WIN_PATH]; + wchar_t wto[GIT_WIN_PATH]; - return ret; + git__utf8_to_16(wfrom, GIT_WIN_PATH, from); + git__utf8_to_16(wto, GIT_WIN_PATH, to); + return MoveFileExW(wfrom, wto, MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED) ? 0 : -1; } int p_recv(GIT_SOCKET socket, void *buffer, size_t length, int flags) @@ -470,3 +452,124 @@ int p_send(GIT_SOCKET socket, const void *buffer, size_t length, int flags) return send(socket, buffer, (int)length, 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; +} + +#if defined(_MSC_VER) || defined(_MSC_EXTENSIONS) +#define DELTA_EPOCH_IN_MICROSECS 11644473600000000Ui64 +#else +#define DELTA_EPOCH_IN_MICROSECS 11644473600000000ULL +#endif + +#ifndef _TIMEZONE_DEFINED +#define _TIMEZONE_DEFINED +struct timezone +{ + 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; +} + +int p_inet_pton(int af, const char* src, void* dst) +{ + union { + struct sockaddr_in6 sin6; + struct sockaddr_in sin; + } sa; + int srcsize; + + switch(af) + { + case AF_INET: + sa.sin.sin_family = AF_INET; + srcsize = (int)sizeof(sa.sin); + break; + case AF_INET6: + sa.sin6.sin6_family = AF_INET6; + srcsize = (int)sizeof(sa.sin6); + break; + default: + errno = WSAEPFNOSUPPORT; + return -1; + } + + if (WSAStringToAddress((LPSTR)src, af, NULL, (struct sockaddr *) &sa, &srcsize) != 0) + { + errno = WSAGetLastError(); + return -1; + } + + switch(af) + { + case AF_INET: + memcpy(dst, &sa.sin.sin_addr, sizeof(sa.sin.sin_addr)); + break; + case AF_INET6: + memcpy(dst, &sa.sin6.sin6_addr, sizeof(sa.sin6.sin6_addr)); + break; + } + + return 1; +} diff --git a/src/win32/precompiled.c b/src/win32/precompiled.c new file mode 100644 index 000000000..5f656a45d --- /dev/null +++ b/src/win32/precompiled.c @@ -0,0 +1 @@ +#include "precompiled.h" diff --git a/src/win32/precompiled.h b/src/win32/precompiled.h new file mode 100644 index 000000000..5de7e6f34 --- /dev/null +++ b/src/win32/precompiled.h @@ -0,0 +1,19 @@ +#include "git2.h" + +#include <assert.h> +#include <errno.h> +#include <limits.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> + +#include <sys/types.h> +#include <sys/stat.h> + +#include <regex.h> + +#include <io.h> +#include <direct.h> +#ifdef GIT_THREADS + #include "win32/pthread.h" +#endif diff --git a/src/win32/pthread.c b/src/win32/pthread.c index 3a186c8d9..105f4b89e 100644 --- a/src/win32/pthread.c +++ b/src/win32/pthread.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -54,6 +54,74 @@ int pthread_mutex_unlock(pthread_mutex_t *mutex) return 0; } +int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr) +{ + /* We don't support non-default attributes. */ + if (attr) + return EINVAL; + + /* This is an auto-reset event. */ + *cond = CreateEventW(NULL, FALSE, FALSE, NULL); + assert(*cond); + + /* If we can't create the event, claim that the reason was out-of-memory. + * The actual reason can be fetched with GetLastError(). */ + return *cond ? 0 : ENOMEM; +} + +int pthread_cond_destroy(pthread_cond_t *cond) +{ + BOOL closed; + + if (!cond) + return EINVAL; + + closed = CloseHandle(*cond); + assert(closed); + GIT_UNUSED(closed); + + *cond = NULL; + return 0; +} + +int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) +{ + int error; + DWORD wait_result; + + if (!cond || !mutex) + return EINVAL; + + /* The caller must be holding the mutex. */ + error = pthread_mutex_unlock(mutex); + + if (error) + return error; + + wait_result = WaitForSingleObject(*cond, INFINITE); + assert(WAIT_OBJECT_0 == wait_result); + GIT_UNUSED(wait_result); + + return pthread_mutex_lock(mutex); +} + +int pthread_cond_signal(pthread_cond_t *cond) +{ + BOOL signaled; + + if (!cond) + return EINVAL; + + signaled = SetEvent(*cond); + assert(signaled); + GIT_UNUSED(signaled); + + return 0; +} + +/* pthread_cond_broadcast is not implemented because doing so with just Win32 events + * is quite complicated, and no caller in libgit2 uses it yet. */ + int pthread_num_processors_np(void) { DWORD_PTR p, s; diff --git a/src/win32/pthread.h b/src/win32/pthread.h index b194cbfa7..a219a0137 100644 --- a/src/win32/pthread.h +++ b/src/win32/pthread.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -21,6 +21,7 @@ typedef int pthread_condattr_t; typedef int pthread_attr_t; typedef CRITICAL_SECTION pthread_mutex_t; typedef HANDLE pthread_t; +typedef HANDLE pthread_cond_t; #define PTHREAD_MUTEX_INITIALIZER {(void*)-1}; @@ -35,6 +36,12 @@ int pthread_mutex_destroy(pthread_mutex_t *); int pthread_mutex_lock(pthread_mutex_t *); int pthread_mutex_unlock(pthread_mutex_t *); +int pthread_cond_init(pthread_cond_t *, const pthread_condattr_t *); +int pthread_cond_destroy(pthread_cond_t *); +int pthread_cond_wait(pthread_cond_t *, pthread_mutex_t *); +int pthread_cond_signal(pthread_cond_t *); +/* pthread_cond_broadcast is not supported on Win32 yet. */ + int pthread_num_processors_np(void); #endif diff --git a/src/win32/utf-conv.c b/src/win32/utf-conv.c index 76f1e4237..c06f3a8c2 100644 --- a/src/win32/utf-conv.c +++ b/src/win32/utf-conv.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -7,89 +7,75 @@ #include "common.h" #include "utf-conv.h" -#include "git2/windows.h" -/* - * Default codepage value - */ -static int _active_codepage = CP_UTF8; - -void gitwin_set_codepage(unsigned int codepage) -{ - _active_codepage = codepage; -} - -unsigned int gitwin_get_codepage(void) -{ - return _active_codepage; -} - -void gitwin_set_utf8(void) -{ - _active_codepage = CP_UTF8; -} +#define U16_LEAD(c) (wchar_t)(((c)>>10)+0xd7c0) +#define U16_TRAIL(c) (wchar_t)(((c)&0x3ff)|0xdc00) -wchar_t* gitwin_to_utf16(const char* str) +#if 0 +void git__utf8_to_16(wchar_t *dest, size_t length, const char *src) { - wchar_t* ret; - size_t cb; - - if (!str) - return NULL; - - cb = strlen(str) * sizeof(wchar_t); - if (cb == 0) - return (wchar_t *)git__calloc(1, sizeof(wchar_t)); - - /* Add space for null terminator */ - cb += sizeof(wchar_t); - - ret = (wchar_t *)git__malloc(cb); - if (!ret) - return NULL; - - if (MultiByteToWideChar(_active_codepage, 0, str, -1, ret, (int)cb) == 0) { - giterr_set(GITERR_OS, "Could not convert string to UTF-16"); - git__free(ret); - ret = NULL; + wchar_t *pDest = dest; + uint32_t ch; + const uint8_t* pSrc = (uint8_t*) src; + + assert(dest && src && length); + + length--; + + while(*pSrc && length > 0) { + ch = *pSrc++; + length--; + + if(ch < 0xc0) { + /* + * ASCII, or a trail byte in lead position which is treated like + * a single-byte sequence for better character boundary + * resynchronization after illegal sequences. + */ + *pDest++ = (wchar_t)ch; + continue; + } else if(ch < 0xe0) { /* U+0080..U+07FF */ + if (pSrc[0]) { + /* 0x3080 = (0xc0 << 6) + 0x80 */ + *pDest++ = (wchar_t)((ch << 6) + *pSrc++ - 0x3080); + continue; + } + } else if(ch < 0xf0) { /* U+0800..U+FFFF */ + if (pSrc[0] && pSrc[1]) { + /* no need for (ch & 0xf) because the upper bits are truncated after <<12 in the cast to (UChar) */ + /* 0x2080 = (0x80 << 6) + 0x80 */ + ch = (ch << 12) + (*pSrc++ << 6); + *pDest++ = (wchar_t)(ch + *pSrc++ - 0x2080); + continue; + } + } else /* f0..f4 */ { /* U+10000..U+10FFFF */ + if (length >= 1 && pSrc[0] && pSrc[1] && pSrc[2]) { + /* 0x3c82080 = (0xf0 << 18) + (0x80 << 12) + (0x80 << 6) + 0x80 */ + ch = (ch << 18) + (*pSrc++ << 12); + ch += *pSrc++ << 6; + ch += *pSrc++ - 0x3c82080; + *(pDest++) = U16_LEAD(ch); + *(pDest++) = U16_TRAIL(ch); + length--; /* two bytes for this character */ + continue; + } + } + + /* truncated character at the end */ + *pDest++ = 0xfffd; + break; } - return ret; + *pDest++ = 0x0; } +#endif -int gitwin_append_utf16(wchar_t *buffer, const char *str, size_t len) +int git__utf8_to_16(wchar_t *dest, size_t length, const char *src) { - int result = MultiByteToWideChar(_active_codepage, 0, str, -1, buffer, (int)len); - if (result == 0) - giterr_set(GITERR_OS, "Could not convert string to UTF-16"); - return result; + return MultiByteToWideChar(CP_UTF8, 0, src, -1, dest, (int)length); } -char* gitwin_from_utf16(const wchar_t* str) +int git__utf16_to_8(char *out, const wchar_t *input) { - char* ret; - size_t cb; - - if (!str) - return NULL; - - cb = wcslen(str) * sizeof(char); - if (cb == 0) - return (char *)git__calloc(1, sizeof(char)); - - /* Add space for null terminator */ - cb += sizeof(char); - - ret = (char*)git__malloc(cb); - if (!ret) - return NULL; - - if (WideCharToMultiByte(_active_codepage, 0, str, -1, ret, (int)cb, NULL, NULL) == 0) { - giterr_set(GITERR_OS, "Could not convert string to UTF-8"); - git__free(ret); - ret = NULL; - } - - return ret; - + return WideCharToMultiByte(CP_UTF8, 0, input, -1, out, GIT_WIN_PATH, NULL, NULL); } diff --git a/src/win32/utf-conv.h b/src/win32/utf-conv.h index ae9f29f6c..6cc9205f7 100644 --- a/src/win32/utf-conv.h +++ b/src/win32/utf-conv.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -10,9 +10,10 @@ #ifndef INCLUDE_git_utfconv_h__ #define INCLUDE_git_utfconv_h__ -wchar_t* gitwin_to_utf16(const char* str); -int gitwin_append_utf16(wchar_t *buffer, const char *str, size_t len); -char* gitwin_from_utf16(const wchar_t* str); +#define GIT_WIN_PATH (260 + 1) + +int git__utf8_to_16(wchar_t *dest, size_t length, const char *src); +int git__utf16_to_8(char *dest, const wchar_t *src); #endif diff --git a/src/win32/version.h b/src/win32/version.h new file mode 100644 index 000000000..483962b57 --- /dev/null +++ b/src/win32/version.h @@ -0,0 +1,20 @@ +/* + * 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_win32_version_h__ +#define INCLUDE_win32_version_h__ + +#include <windows.h> + +GIT_INLINE(int) git_has_win32_version(int major, int minor) +{ + WORD wVersion = LOWORD(GetVersion()); + + return LOBYTE(wVersion) > major || + (LOBYTE(wVersion) == major && HIBYTE(wVersion) >= minor); +} + +#endif |
