From 7cb904ba4443c22ff5396769b7d07a7f329c0102 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Tue, 1 Apr 2014 23:58:59 -0700 Subject: [PATCH 01/54] Introduce git_apply_patch The beginnings of patch application from an existing (diff-created) git_patch object: applies the hunks of a git_patch to a buffer. --- include/git2/errors.h | 3 +- src/apply.c | 282 ++++++++++++++++++++++++++++++++++++ src/apply.h | 21 +++ src/array.h | 1 - src/vector.c | 41 ++++++ src/vector.h | 3 + tests/apply/apply_common.h | 286 +++++++++++++++++++++++++++++++++++++ tests/apply/fromdiff.c | 176 +++++++++++++++++++++++ 8 files changed, 811 insertions(+), 2 deletions(-) create mode 100644 src/apply.c create mode 100644 src/apply.h create mode 100644 tests/apply/apply_common.h create mode 100644 tests/apply/fromdiff.c diff --git a/include/git2/errors.h b/include/git2/errors.h index 3ecea34bf58..e959ffd8add 100644 --- a/include/git2/errors.h +++ b/include/git2/errors.h @@ -98,7 +98,8 @@ typedef enum { GITERR_CHERRYPICK, GITERR_DESCRIBE, GITERR_REBASE, - GITERR_FILESYSTEM + GITERR_FILESYSTEM, + GITERR_PATCH, } git_error_t; /** diff --git a/src/apply.c b/src/apply.c new file mode 100644 index 00000000000..e75fa5b4d5e --- /dev/null +++ b/src/apply.c @@ -0,0 +1,282 @@ +/* + * 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 + +#include "git2/patch.h" +#include "git2/filter.h" +#include "array.h" +#include "diff_patch.h" +#include "fileops.h" +#include "apply.h" + +#define apply_err(...) \ + ( giterr_set(GITERR_PATCH, __VA_ARGS__), -1 ) + +typedef struct { + /* The lines that we allocate ourself are allocated out of the pool. + * (Lines may have been allocated out of the diff.) + */ + git_pool pool; + git_vector lines; +} patch_image; + +static void patch_line_init( + git_diff_line *out, + const char *in, + size_t in_len, + size_t in_offset) +{ + out->content = in; + out->content_len = in_len; + out->content_offset = in_offset; +} + +static unsigned int patch_image_init(patch_image *out) +{ + memset(out, 0x0, sizeof(patch_image)); + return 0; +} + +static int patch_image_init_fromstr( + patch_image *out, const char *in, size_t in_len) +{ + git_diff_line *line; + const char *start, *end; + + memset(out, 0x0, sizeof(patch_image)); + + git_pool_init(&out->pool, sizeof(git_diff_line)); + + for (start = in; start < in + in_len; start = end) { + end = memchr(start, '\n', in_len); + + if (end < in + in_len) + end++; + + line = git_pool_mallocz(&out->pool, 1); + GITERR_CHECK_ALLOC(line); + + if (git_vector_insert(&out->lines, line) < 0) + return -1; + + patch_line_init(line, start, (end - start), (start - in)); + } + + return 0; +} + +static void patch_image_free(patch_image *image) +{ + if (image == NULL) + return; + + git_pool_clear(&image->pool); + git_vector_free(&image->lines); +} + +static bool match_hunk( + patch_image *image, + patch_image *preimage, + size_t linenum) +{ + bool match = 0; + size_t i; + + /* Ensure this hunk is within the image boundaries. */ + if (git_vector_length(&preimage->lines) + linenum > + git_vector_length(&image->lines)) + return 0; + + match = 1; + + /* Check exact match. */ + for (i = 0; i < git_vector_length(&preimage->lines); i++) { + git_diff_line *preimage_line = git_vector_get(&preimage->lines, i); + git_diff_line *image_line = git_vector_get(&image->lines, linenum + i); + + if (preimage_line->content_len != preimage_line->content_len || + memcmp(preimage_line->content, image_line->content, image_line->content_len) != 0) { + match = 0; + break; + } + } + + return match; +} + +static bool find_hunk_linenum( + size_t *out, + patch_image *image, + patch_image *preimage, + size_t linenum) +{ + size_t max = git_vector_length(&image->lines); + bool match; + + if (linenum > max) + linenum = max; + + match = match_hunk(image, preimage, linenum); + + *out = linenum; + return match; +} + +static int update_hunk( + patch_image *image, + unsigned int linenum, + patch_image *preimage, + patch_image *postimage) +{ + size_t postlen = git_vector_length(&postimage->lines); + size_t prelen = git_vector_length(&preimage->lines); + size_t i; + int error = 0; + + if (postlen > prelen) + error = git_vector_grow_at( + &image->lines, linenum, (postlen - prelen)); + else if (prelen > postlen) + error = git_vector_shrink_at( + &image->lines, linenum, (prelen - postlen)); + + if (error) { + giterr_set_oom(); + return -1; + } + + for (i = 0; i < git_vector_length(&postimage->lines); i++) { + image->lines.contents[linenum + i] = + git_vector_get(&postimage->lines, i); + } + + return 0; +} + +static int apply_hunk( + patch_image *image, + git_patch *patch, + diff_patch_hunk *hunk) +{ + patch_image preimage, postimage; + size_t line_num, i; + int error = 0; + + if ((error = patch_image_init(&preimage)) < 0 || + (error = patch_image_init(&postimage)) < 0) + goto done; + + for (i = 0; i < hunk->line_count; i++) { + size_t linenum = hunk->line_start + i; + git_diff_line *line = git_array_get(patch->lines, linenum); + + if (!line) { + error = apply_err("Preimage does not contain line %d", linenum); + goto done; + } + + if (line->origin == GIT_DIFF_LINE_CONTEXT || + line->origin == GIT_DIFF_LINE_DELETION) { + if ((error = git_vector_insert(&preimage.lines, line)) < 0) + goto done; + } + + if (line->origin == GIT_DIFF_LINE_CONTEXT || + line->origin == GIT_DIFF_LINE_ADDITION) { + if ((error = git_vector_insert(&postimage.lines, line)) < 0) + goto done; + } + } + + line_num = hunk->hunk.new_start ? hunk->hunk.new_start - 1 : 0; + + if (!find_hunk_linenum(&line_num, image, &preimage, line_num)) { + error = apply_err("Hunk at line %d did not apply", + hunk->hunk.new_start); + goto done; + } + + error = update_hunk(image, line_num, &preimage, &postimage); + +done: + patch_image_free(&preimage); + patch_image_free(&postimage); + + return error; +} + +static int apply_hunks( + git_buf *out, + const char *source, + size_t source_len, + git_patch *patch) +{ + diff_patch_hunk *hunk; + git_diff_line *line; + patch_image image; + size_t i; + int error = 0; + + if ((error = patch_image_init_fromstr(&image, source, source_len)) < 0) + goto done; + + git_array_foreach(patch->hunks, i, hunk) { + if ((error = apply_hunk(&image, patch, hunk)) < 0) + goto done; + } + + git_vector_foreach(&image.lines, i, line) + git_buf_put(out, line->content, line->content_len); + +done: + patch_image_free(&image); + + return error; +} + +int git_apply__patch( + git_buf *contents_out, + char **filename_out, + unsigned int *mode_out, + const char *source, + size_t source_len, + git_patch *patch) +{ + char *filename = NULL; + unsigned int mode = 0; + int error = 0; + + assert(contents_out && filename_out && mode_out && (source || !source_len) && patch); + + *filename_out = NULL; + *mode_out = 0; + + if (patch->delta->status != GIT_DELTA_DELETED) { + filename = git__strdup(patch->nfile.file->path); + mode = patch->nfile.file->mode ? + patch->nfile.file->mode : GIT_FILEMODE_BLOB; + } + + if ((error = apply_hunks(contents_out, source, source_len, patch)) < 0) + goto done; + + if (patch->delta->status == GIT_DELTA_DELETED && + git_buf_len(contents_out) > 0) { + error = apply_err("removal patch leaves file contents"); + goto done; + } + + *filename_out = filename; + *mode_out = mode; + +done: + if (error < 0) + git__free(filename); + + return error; +} diff --git a/src/apply.h b/src/apply.h new file mode 100644 index 00000000000..96e0f55b5d3 --- /dev/null +++ b/src/apply.h @@ -0,0 +1,21 @@ +/* + * 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_apply_h__ +#define INCLUDE_apply_h__ + +#include "git2/patch.h" +#include "buffer.h" + +extern int git_apply__patch( + git_buf *out, + char **filename, + unsigned int *mode, + const char *source, + size_t source_len, + git_patch *patch); + +#endif diff --git a/src/array.h b/src/array.h index 78d321e8237..1d8a01c9626 100644 --- a/src/array.h +++ b/src/array.h @@ -85,7 +85,6 @@ GIT_INLINE(void *) git_array_grow(void *_a, size_t item_size) #define git_array_foreach(a, i, element) \ for ((i) = 0; (i) < (a).size && ((element) = &(a).ptr[(i)]); (i)++) - GIT_INLINE(int) git_array__search( size_t *out, void *array_ptr, diff --git a/src/vector.c b/src/vector.c index a81d463efa9..3684676920a 100644 --- a/src/vector.c +++ b/src/vector.c @@ -330,6 +330,47 @@ int git_vector_resize_to(git_vector *v, size_t new_length) return 0; } +int git_vector_grow_at(git_vector *v, size_t idx, size_t grow_len) +{ + size_t new_length = v->length + grow_len; + size_t new_idx = idx + grow_len; + + assert(grow_len > 0); + assert (idx <= v->length); + + if (new_length < v->length || + (new_length > v->_alloc_size && resize_vector(v, new_length) < 0)) + return -1; + + memmove(&v->contents[new_idx], &v->contents[idx], + sizeof(void *) * (v->length - idx)); + memset(&v->contents[idx], 0, sizeof(void *) * grow_len); + + v->length = new_length; + return 0; +} + +int git_vector_shrink_at(git_vector *v, size_t idx, size_t shrink_len) +{ + size_t new_length = v->length - shrink_len; + size_t end_idx = idx + shrink_len; + + assert(shrink_len > 0 && shrink_len <= v->length); + assert(idx <= v->length); + + if (new_length > v->length) + return -1; + + if (idx > v->length) + memmove(&v->contents[idx], &v->contents[end_idx], + sizeof(void *) * (v->length - idx)); + + memset(&v->contents[new_length], 0, sizeof(void *) * shrink_len); + + v->length = new_length; + return 0; +} + int git_vector_set(void **old, git_vector *v, size_t position, void *value) { if (position + 1 > v->length) { diff --git a/src/vector.h b/src/vector.h index b7500ded3a7..6399a84842e 100644 --- a/src/vector.h +++ b/src/vector.h @@ -93,6 +93,9 @@ void git_vector_remove_matching( void *payload); int git_vector_resize_to(git_vector *v, size_t new_length); +int git_vector_grow_at(git_vector *v, size_t idx, size_t grow_len); +int git_vector_shrink_at(git_vector *v, size_t idx, size_t shrink_len); + int git_vector_set(void **old, git_vector *v, size_t position, void *value); /** Check if vector is sorted */ diff --git a/tests/apply/apply_common.h b/tests/apply/apply_common.h new file mode 100644 index 00000000000..8226cc1da2c --- /dev/null +++ b/tests/apply/apply_common.h @@ -0,0 +1,286 @@ +/* The original file contents */ + +#define FILE_ORIGINAL \ + "hey!\n" \ + "this is some context!\n" \ + "around some lines\n" \ + "that will change\n" \ + "yes it is!\n" \ + "(this line is changed)\n" \ + "and this\n" \ + "is additional context\n" \ + "below it!\n" + +/* A change in the middle of the file (and the resultant patch) */ + +#define FILE_CHANGE_MIDDLE \ + "hey!\n" \ + "this is some context!\n" \ + "around some lines\n" \ + "that will change\n" \ + "yes it is!\n" \ + "(THIS line is changed!)\n" \ + "and this\n" \ + "is additional context\n" \ + "below it!\n" + +#define PATCH_ORIGINAL_TO_CHANGE_MIDDLE \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..cd8fd12 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -3,7 +3,7 @@ this is some context!\n" \ + " around some lines\n" \ + " that will change\n" \ + " yes it is!\n" \ + "-(this line is changed)\n" \ + "+(THIS line is changed!)\n" \ + " and this\n" \ + " is additional context\n" \ + " below it!\n" + +#define PATCH_ORIGINAL_TO_CHANGE_MIDDLE_NOCONTEXT \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..cd8fd12 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -6 +6 @@ yes it is!\n" \ + "-(this line is changed)\n" \ + "+(THIS line is changed!)\n" + +/* A change of the first line (and the resultant patch) */ + +#define FILE_CHANGE_FIRSTLINE \ + "hey, change in head!\n" \ + "this is some context!\n" \ + "around some lines\n" \ + "that will change\n" \ + "yes it is!\n" \ + "(this line is changed)\n" \ + "and this\n" \ + "is additional context\n" \ + "below it!\n" + +#define PATCH_ORIGINAL_TO_CHANGE_FIRSTLINE \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..c81df1d 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -1,4 +1,4 @@\n" \ + "-hey!\n" \ + "+hey, change in head!\n" \ + " this is some context!\n" \ + " around some lines\n" \ + " that will change\n" + +/* A change of the last line (and the resultant patch) */ + +#define FILE_CHANGE_LASTLINE \ + "hey!\n" \ + "this is some context!\n" \ + "around some lines\n" \ + "that will change\n" \ + "yes it is!\n" \ + "(this line is changed)\n" \ + "and this\n" \ + "is additional context\n" \ + "change to the last line.\n" + +#define PATCH_ORIGINAL_TO_CHANGE_LASTLINE \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..f70db1c 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -6,4 +6,4 @@ yes it is!\n" \ + " (this line is changed)\n" \ + " and this\n" \ + " is additional context\n" \ + "-below it!\n" \ + "+change to the last line.\n" + +/* An insertion at the beginning of the file (and the resultant patch) */ + +#define FILE_PREPEND \ + "insert at front\n" \ + "hey!\n" \ + "this is some context!\n" \ + "around some lines\n" \ + "that will change\n" \ + "yes it is!\n" \ + "(this line is changed)\n" \ + "and this\n" \ + "is additional context\n" \ + "below it!\n" + +#define PATCH_ORIGINAL_TO_PREPEND \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..0f39b9a 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -1,3 +1,4 @@\n" \ + "+insert at front\n" \ + " hey!\n" \ + " this is some context!\n" \ + " around some lines\n" + +#define PATCH_ORIGINAL_TO_PREPEND_NOCONTEXT \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..0f39b9a 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -0,0 +1 @@\n" \ + "+insert at front\n" + +/* An insertion at the end of the file (and the resultant patch) */ + +#define FILE_APPEND \ + "hey!\n" \ + "this is some context!\n" \ + "around some lines\n" \ + "that will change\n" \ + "yes it is!\n" \ + "(this line is changed)\n" \ + "and this\n" \ + "is additional context\n" \ + "below it!\n" \ + "insert at end\n" + +#define PATCH_ORIGINAL_TO_APPEND \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..72788bb 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -7,3 +7,4 @@ yes it is!\n" \ + " and this\n" \ + " is additional context\n" \ + " below it!\n" \ + "+insert at end\n" + +#define PATCH_ORIGINAL_TO_APPEND_NOCONTEXT \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..72788bb 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -9,0 +10 @@ below it!\n" \ + "+insert at end\n" + +/* An insertion at the beginning and end of file (and the resultant patch) */ + +#define FILE_PREPEND_AND_APPEND \ + "first and\n" \ + "this is some context!\n" \ + "around some lines\n" \ + "that will change\n" \ + "yes it is!\n" \ + "(this line is changed)\n" \ + "and this\n" \ + "is additional context\n" \ + "last lines\n" + +#define PATCH_ORIGINAL_TO_PREPEND_AND_APPEND \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..f282430 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -1,4 +1,4 @@\n" \ + "-hey!\n" \ + "+first and\n" \ + " this is some context!\n" \ + " around some lines\n" \ + " that will change\n" \ + "@@ -6,4 +6,4 @@ yes it is!\n" \ + " (this line is changed)\n" \ + " and this\n" \ + " is additional context\n" \ + "-below it!\n" \ + "+last lines\n" + +#define PATCH_ORIGINAL_TO_EMPTY_FILE \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..e69de29 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -1,9 +0,0 @@\n" \ + "-hey!\n" \ + "-this is some context!\n" \ + "-around some lines\n" \ + "-that will change\n" \ + "-yes it is!\n" \ + "-(this line is changed)\n" \ + "-and this\n" \ + "-is additional context\n" \ + "-below it!\n" + +#define PATCH_EMPTY_FILE_TO_ORIGINAL \ + "diff --git a/file.txt b/file.txt\n" \ + "index e69de29..9432026 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -0,0 +1,9 @@\n" \ + "+hey!\n" \ + "+this is some context!\n" \ + "+around some lines\n" \ + "+that will change\n" \ + "+yes it is!\n" \ + "+(this line is changed)\n" \ + "+and this\n" \ + "+is additional context\n" \ + "+below it!\n" + +#define PATCH_ADD_ORIGINAL \ + "diff --git a/file.txt b/file.txt\n" \ + "new file mode 100644\n" \ + "index 0000000..9432026\n" \ + "--- /dev/null\n" \ + "+++ b/file.txt\n" \ + "@@ -0,0 +1,9 @@\n" \ + "+hey!\n" \ + "+this is some context!\n" \ + "+around some lines\n" \ + "+that will change\n" \ + "+yes it is!\n" \ + "+(this line is changed)\n" \ + "+and this\n" \ + "+is additional context\n" \ + "+below it!\n" + +#define PATCH_DELETE_ORIGINAL \ + "diff --git a/file.txt b/file.txt\n" \ + "deleted file mode 100644\n" \ + "index 9432026..0000000\n" \ + "--- a/file.txt\n" \ + "+++ /dev/null\n" \ + "@@ -1,9 +0,0 @@\n" \ + "-hey!\n" \ + "-this is some context!\n" \ + "-around some lines\n" \ + "-that will change\n" \ + "-yes it is!\n" \ + "-(this line is changed)\n" \ + "-and this\n" \ + "-is additional context\n" \ + "-below it!\n" + +#define PATCH_RENAME_EXACT \ + "diff --git a/file.txt b/newfile.txt\n" \ + "similarity index 100%\n" \ + "rename from file.txt\n" \ + "rename to newfile.txt\n" + +#define PATCH_RENAME_SIMILAR \ + "diff --git a/file.txt b/newfile.txt\n" \ + "similarity index 77%\n" \ + "rename from file.txt\n" \ + "rename to newfile.txt\n" \ + "index 9432026..cd8fd12 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/newfile.txt\n" \ + "@@ -3,7 +3,7 @@ this is some context!\n" \ + " around some lines\n" \ + " that will change\n" \ + " yes it is!\n" \ + "-(this line is changed)\n" \ + "+(THIS line is changed!)\n" \ + " and this\n" \ + " is additional context\n" \ + " below it!\n" diff --git a/tests/apply/fromdiff.c b/tests/apply/fromdiff.c new file mode 100644 index 00000000000..64ed9de7932 --- /dev/null +++ b/tests/apply/fromdiff.c @@ -0,0 +1,176 @@ +#include "clar_libgit2.h" +#include "git2/sys/repository.h" + +#include "apply.h" +#include "repository.h" +#include "buf_text.h" + +#include "apply_common.h" + +static git_repository *repo = NULL; + +void test_apply_fromdiff__initialize(void) +{ + repo = cl_git_sandbox_init("renames"); +} + +void test_apply_fromdiff__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +static int apply_buf( + const char *old, + const char *oldname, + const char *new, + const char *newname, + const char *patch_expected, + const git_diff_options *diff_opts) +{ + git_patch *patch; + git_buf result = GIT_BUF_INIT; + git_buf patchbuf = GIT_BUF_INIT; + char *filename; + unsigned int mode; + int error; + + cl_git_pass(git_patch_from_buffers(&patch, + old, old ? strlen(old) : 0, oldname, + new, new ? strlen(new) : 0, newname, + diff_opts)); + cl_git_pass(git_patch_to_buf(&patchbuf, patch)); + + cl_assert_equal_s(patch_expected, patchbuf.ptr); + + error = git_apply__patch(&result, &filename, &mode, old, old ? strlen(old) : 0, patch); + + if (error == 0 && new == NULL) { + cl_assert_equal_i(0, result.size); + cl_assert_equal_p(NULL, filename); + cl_assert_equal_i(0, mode); + } else { + cl_assert_equal_s(new, result.ptr); + cl_assert_equal_s("file.txt", filename); + cl_assert_equal_i(0100644, mode); + } + + git__free(filename); + git_buf_free(&result); + git_buf_free(&patchbuf); + git_patch_free(patch); + + return error; +} + +void test_apply_fromdiff__change_middle(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_CHANGE_MIDDLE, "file.txt", + PATCH_ORIGINAL_TO_CHANGE_MIDDLE, NULL)); +} + +void test_apply_fromdiff__change_middle_nocontext(void) +{ + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + diff_opts.context_lines = 0; + + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_CHANGE_MIDDLE, "file.txt", + PATCH_ORIGINAL_TO_CHANGE_MIDDLE_NOCONTEXT, &diff_opts)); +} + +void test_apply_fromdiff__change_firstline(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_CHANGE_FIRSTLINE, "file.txt", + PATCH_ORIGINAL_TO_CHANGE_FIRSTLINE, NULL)); +} + +void test_apply_fromdiff__lastline(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_CHANGE_LASTLINE, "file.txt", + PATCH_ORIGINAL_TO_CHANGE_LASTLINE, NULL)); +} + +void test_apply_fromdiff__prepend(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_PREPEND, "file.txt", + PATCH_ORIGINAL_TO_PREPEND, NULL)); +} + +void test_apply_fromdiff__prepend_nocontext(void) +{ + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + diff_opts.context_lines = 0; + + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_PREPEND, "file.txt", + PATCH_ORIGINAL_TO_PREPEND_NOCONTEXT, &diff_opts)); +} + +void test_apply_fromdiff__append(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_APPEND, "file.txt", + PATCH_ORIGINAL_TO_APPEND, NULL)); +} + +void test_apply_fromdiff__append_nocontext(void) +{ + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + diff_opts.context_lines = 0; + + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_APPEND, "file.txt", + PATCH_ORIGINAL_TO_APPEND_NOCONTEXT, &diff_opts)); +} + +void test_apply_fromdiff__prepend_and_append(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_PREPEND_AND_APPEND, "file.txt", + PATCH_ORIGINAL_TO_PREPEND_AND_APPEND, NULL)); +} + +void test_apply_fromdiff__to_empty_file(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + "", NULL, + PATCH_ORIGINAL_TO_EMPTY_FILE, NULL)); +} + +void test_apply_fromdiff__from_empty_file(void) +{ + cl_git_pass(apply_buf( + "", NULL, + FILE_ORIGINAL, "file.txt", + PATCH_EMPTY_FILE_TO_ORIGINAL, NULL)); +} + +void test_apply_fromdiff__add(void) +{ + cl_git_pass(apply_buf( + NULL, NULL, + FILE_ORIGINAL, "file.txt", + PATCH_ADD_ORIGINAL, NULL)); +} + +void test_apply_fromdiff__delete(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + NULL, NULL, + PATCH_DELETE_ORIGINAL, NULL)); +} From d34f68261ef95b517944d4fa89ee13b4a68d3cb4 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Tue, 8 Apr 2014 17:18:47 -0700 Subject: [PATCH 02/54] Patch parsing from patch files --- include/git2/diff.h | 14 +- include/git2/patch.h | 13 + src/buffer.c | 75 ++++ src/buffer.h | 5 + src/patch.c | 758 +++++++++++++++++++++++++++++++++++++ src/path.c | 19 + src/path.h | 6 + src/util.c | 22 +- src/util.h | 10 + tests/apply/apply_common.h | 189 +++++++++ tests/apply/fromfile.c | 301 +++++++++++++++ tests/buf/quote.c | 57 +++ 12 files changed, 1461 insertions(+), 8 deletions(-) create mode 100644 src/patch.c create mode 100644 tests/apply/fromfile.c create mode 100644 tests/buf/quote.c diff --git a/include/git2/diff.h b/include/git2/diff.h index c35701a4692..f3bb337b7c1 100644 --- a/include/git2/diff.h +++ b/include/git2/diff.h @@ -448,6 +448,8 @@ typedef int (*git_diff_file_cb)( float progress, void *payload); +#define GIT_DIFF_HUNK_HEADER_SIZE 128 + /** * When producing a binary diff, the binary data returned will be * either the deflated full ("literal") contents of the file, or @@ -499,12 +501,12 @@ typedef int(*git_diff_binary_cb)( * Structure describing a hunk of a diff. */ typedef struct { - int old_start; /**< Starting line number in old_file */ - int old_lines; /**< Number of lines in old_file */ - int new_start; /**< Starting line number in new_file */ - int new_lines; /**< Number of lines in new_file */ - size_t header_len; /**< Number of bytes in header text */ - char header[128]; /**< Header text, NUL-byte terminated */ + int old_start; /** Starting line number in old_file */ + int old_lines; /** Number of lines in old_file */ + int new_start; /** Starting line number in new_file */ + int new_lines; /** Number of lines in new_file */ + size_t header_len; /** Number of bytes in header text */ + char header[GIT_DIFF_HUNK_HEADER_SIZE]; /** Header text, NUL-byte terminated */ } git_diff_hunk; /** diff --git a/include/git2/patch.h b/include/git2/patch.h index 790cb74fcce..aa8729c9ce7 100644 --- a/include/git2/patch.h +++ b/include/git2/patch.h @@ -267,6 +267,19 @@ GIT_EXTERN(int) git_patch_to_buf( git_buf *out, git_patch *patch); +/** + * Create a patch from the contents of a patch file. + * + * @param out The patch to be created + * @param patchfile The contents of a patch file + * @param patchfile_len The length of the patch file + * @return 0 on success, <0 on failure. + */ +GIT_EXTERN(int) git_patch_from_patchfile( + git_patch **out, + const char *patchfile, + size_t patchfile_len); + GIT_END_DECL /**@}*/ diff --git a/src/buffer.c b/src/buffer.c index 1a5809ccad2..5fafe69cb44 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -766,3 +766,78 @@ int git_buf_splice( buf->ptr[buf->size] = '\0'; return 0; } + +/* Unquote per http://marc.info/?l=git&m=112927316408690&w=2 */ +int git_buf_unquote(git_buf *buf) +{ + size_t i, j; + char ch; + + git_buf_rtrim(buf); + + if (buf->size < 2 || buf->ptr[0] != '"' || buf->ptr[buf->size-1] != '"') + goto invalid; + + for (i = 0, j = 1; j < buf->size-1; i++, j++) { + ch = buf->ptr[j]; + + if (ch == '\\') { + if (j == buf->size-2) + goto invalid; + + ch = buf->ptr[++j]; + + switch (ch) { + /* \" or \\ simply copy the char in */ + case '"': case '\\': + break; + + /* add the appropriate escaped char */ + case 'a': ch = '\a'; break; + case 'b': ch = '\b'; break; + case 'f': ch = '\f'; break; + case 'n': ch = '\n'; break; + case 'r': ch = '\r'; break; + case 't': ch = '\t'; break; + case 'v': ch = '\v'; break; + + /* \xyz digits convert to the char*/ + case '0': case '1': case '2': + if (j == buf->size-3) { + giterr_set(GITERR_INVALID, + "Truncated quoted character \\%c", ch); + return -1; + } + + if (buf->ptr[j+1] < '0' || buf->ptr[j+1] > '7' || + buf->ptr[j+2] < '0' || buf->ptr[j+2] > '7') { + giterr_set(GITERR_INVALID, + "Truncated quoted character \\%c%c%c", + buf->ptr[j], buf->ptr[j+1], buf->ptr[j+2]); + return -1; + } + + ch = ((buf->ptr[j] - '0') << 6) | + ((buf->ptr[j+1] - '0') << 3) | + (buf->ptr[j+2] - '0'); + j += 2; + break; + + default: + giterr_set(GITERR_INVALID, "Invalid quoted character \\%c", ch); + return -1; + } + } + + buf->ptr[i] = ch; + } + + buf->ptr[i] = '\0'; + buf->size = i; + + return 0; + +invalid: + giterr_set(GITERR_INVALID, "Invalid quoted line"); + return -1; +} diff --git a/src/buffer.h b/src/buffer.h index e46ee5dd748..d446e0487f6 100644 --- a/src/buffer.h +++ b/src/buffer.h @@ -173,6 +173,11 @@ void git_buf_rtrim(git_buf *buf); int git_buf_cmp(const git_buf *a, const git_buf *b); +/* Unquote a buffer as specified in + * http://marc.info/?l=git&m=112927316408690&w=2 + */ +int git_buf_unquote(git_buf *buf); + /* Write data as base64 encoded in buffer */ int git_buf_encode_base64(git_buf *buf, const char *data, size_t len); /* Decode the given bas64 and write the result to the buffer */ diff --git a/src/patch.c b/src/patch.c new file mode 100644 index 00000000000..9999fa24dc5 --- /dev/null +++ b/src/patch.c @@ -0,0 +1,758 @@ +#include "git2/patch.h" +#include "diff_patch.h" + +#define parse_err(...) \ + ( giterr_set(GITERR_PATCH, __VA_ARGS__), -1 ) + +typedef struct { + const char *content; + size_t content_len; + + const char *line; + size_t line_len; + size_t line_num; + + size_t remain; + + char *header_new_path; + char *header_old_path; +} patch_parse_ctx; + + +static void parse_advance_line(patch_parse_ctx *ctx) +{ + ctx->line += ctx->line_len; + ctx->remain -= ctx->line_len; + ctx->line_len = git__linenlen(ctx->line, ctx->remain); + ctx->line_num++; +} + +static void parse_advance_chars(patch_parse_ctx *ctx, size_t char_cnt) +{ + ctx->line += char_cnt; + ctx->remain -= char_cnt; + ctx->line_len -= char_cnt; +} + +static int parse_advance_expected( + patch_parse_ctx *ctx, + const char *expected, + size_t expected_len) +{ + if (ctx->line_len < expected_len) + return -1; + + if (memcmp(ctx->line, expected, expected_len) != 0) + return -1; + + parse_advance_chars(ctx, expected_len); + return 0; +} + +static int parse_advance_ws(patch_parse_ctx *ctx) +{ + int ret = -1; + + while (ctx->line_len > 0 && + ctx->line[0] != '\n' && + git__isspace(ctx->line[0])) { + ctx->line++; + ctx->line_len--; + ctx->remain--; + ret = 0; + } + + return ret; +} + +static int header_path_len(patch_parse_ctx *ctx) +{ + bool inquote = 0; + bool quoted = (ctx->line_len > 0 && ctx->line[0] == '"'); + size_t len; + + for (len = quoted; len < ctx->line_len; len++) { + if (!quoted && git__isspace(ctx->line[len])) + break; + else if (quoted && !inquote && ctx->line[len] == '"') { + len++; + break; + } + + inquote = (!inquote && ctx->line[len] == '\\'); + } + + return len; +} + +static int parse_header_path_buf(git_buf *path, patch_parse_ctx *ctx) +{ + int path_len, error = 0; + + path_len = header_path_len(ctx); + + if ((error = git_buf_put(path, ctx->line, path_len)) < 0) + goto done; + + parse_advance_chars(ctx, path_len); + + git_buf_rtrim(path); + + if (path->size > 0 && path->ptr[0] == '"') + error = git_buf_unquote(path); + + if (error < 0) + goto done; + + git_path_squash_slashes(path); + +done: + return error; +} + +static int parse_header_path(char **out, patch_parse_ctx *ctx) +{ + git_buf path = GIT_BUF_INIT; + int error = parse_header_path_buf(&path, ctx); + + *out = git_buf_detach(&path); + + return error; +} + +static int parse_header_git_oldpath(git_patch *patch, patch_parse_ctx *ctx) +{ + return parse_header_path((char **)&patch->ofile.file->path, ctx); +} + +static int parse_header_git_newpath(git_patch *patch, patch_parse_ctx *ctx) +{ + return parse_header_path((char **)&patch->nfile.file->path, ctx); +} + +static int parse_header_mode(uint16_t *mode, patch_parse_ctx *ctx) +{ + const char *end; + int32_t m; + int ret; + + if (ctx->line_len < 1 || !git__isdigit(ctx->line[0])) + return parse_err("invalid file mode at line %d", ctx->line_num); + + if ((ret = git__strntol32(&m, ctx->line, ctx->line_len, &end, 8)) < 0) + return ret; + + if (m > UINT16_MAX) + return -1; + + *mode = (uint16_t)m; + + parse_advance_chars(ctx, (end - ctx->line)); + + return ret; +} + +static int parse_header_oid( + git_oid *oid, + size_t *oid_len, + patch_parse_ctx *ctx) +{ + size_t len; + + for (len = 0; len < ctx->line_len && len < GIT_OID_HEXSZ; len++) { + if (!git__isxdigit(ctx->line[len])) + break; + } + + if (len < GIT_OID_MINPREFIXLEN || + git_oid_fromstrn(oid, ctx->line, len) < 0) + return parse_err("invalid hex formatted object id at line %d", + ctx->line_num); + + parse_advance_chars(ctx, len); + + *oid_len = len; + + return 0; +} + +static int parse_header_git_index(git_patch *patch, patch_parse_ctx *ctx) +{ + /* + * TODO: we read the prefix provided in the diff into the delta's id + * field, but do not mark is at an abbreviated id. + */ + size_t oid_len, nid_len; + + if (parse_header_oid(&patch->delta->old_file.id, &oid_len, ctx) < 0 || + parse_advance_expected(ctx, "..", 2) < 0 || + parse_header_oid(&patch->delta->new_file.id, &nid_len, ctx) < 0) + return -1; + + if (ctx->line_len > 0 && ctx->line[0] == ' ') { + uint16_t mode; + + parse_advance_chars(ctx, 1); + + if (parse_header_mode(&mode, ctx) < 0) + return -1; + + if (!patch->delta->new_file.mode) + patch->delta->new_file.mode = mode; + + if (!patch->delta->old_file.mode) + patch->delta->old_file.mode = mode; + } + + return 0; +} + +static int parse_header_git_oldmode(git_patch *patch, patch_parse_ctx *ctx) +{ + return parse_header_mode(&patch->ofile.file->mode, ctx); +} + +static int parse_header_git_newmode(git_patch *patch, patch_parse_ctx *ctx) +{ + return parse_header_mode(&patch->nfile.file->mode, ctx); +} + +static int parse_header_git_deletedfilemode( + git_patch *patch, + patch_parse_ctx *ctx) +{ + git__free((char *)patch->ofile.file->path); + + patch->ofile.file->path = NULL; + patch->delta->status = GIT_DELTA_DELETED; + + return parse_header_mode(&patch->ofile.file->mode, ctx); +} + +static int parse_header_git_newfilemode( + git_patch *patch, + patch_parse_ctx *ctx) +{ + git__free((char *)patch->nfile.file->path); + + patch->nfile.file->path = NULL; + patch->delta->status = GIT_DELTA_ADDED; + + return parse_header_mode(&patch->nfile.file->mode, ctx); +} + +static int parse_header_rename( + char **out, + char **header_path, + patch_parse_ctx *ctx) +{ + git_buf path = GIT_BUF_INIT; + size_t header_path_len, prefix_len; + + if (*header_path == NULL) + return parse_err("rename without proper git diff header at line %d", + ctx->line_num); + + header_path_len = strlen(*header_path); + + if (parse_header_path_buf(&path, ctx) < 0) + return -1; + + if (header_path_len < git_buf_len(&path)) + return parse_err("rename path is invalid at line %d", ctx->line_num); + + /* This sanity check exists because git core uses the data in the + * "rename from" / "rename to" lines, but it's formatted differently + * than the other paths and lacks the normal prefix. This irregularity + * causes us to ignore these paths (we always store the prefixed paths) + * but instead validate that they match the suffix of the paths we parsed + * since we would behave differently from git core if they ever differed. + * Instead, we raise an error, rather than parsing differently. + */ + prefix_len = header_path_len - path.size; + + if (strncmp(*header_path + prefix_len, path.ptr, path.size) != 0 || + (prefix_len > 0 && (*header_path)[prefix_len - 1] != '/')) + return parse_err("rename path does not match header at line %d", + ctx->line_num); + + *out = *header_path; + *header_path = NULL; + + git_buf_free(&path); + + return 0; +} + +static int parse_header_renamefrom(git_patch *patch, patch_parse_ctx *ctx) +{ + patch->delta->status |= GIT_DELTA_RENAMED; + + return parse_header_rename( + (char **)&patch->ofile.file->path, + &ctx->header_old_path, + ctx); +} + +static int parse_header_renameto(git_patch *patch, patch_parse_ctx *ctx) +{ + patch->delta->status |= GIT_DELTA_RENAMED; + + return parse_header_rename( + (char **)&patch->nfile.file->path, + &ctx->header_new_path, + ctx); +} + +static int parse_header_percent(uint16_t *out, patch_parse_ctx *ctx) +{ + int32_t val; + const char *end; + + if (ctx->line_len < 1 || !git__isdigit(ctx->line[0]) || + git__strntol32(&val, ctx->line, ctx->line_len, &end, 10) < 0) + return -1; + + parse_advance_chars(ctx, (end - ctx->line)); + + if (parse_advance_expected(ctx, "%", 1) < 0) + return -1; + + if (val > 100) + return -1; + + *out = val; + return 0; +} + +static int parse_header_similarity(git_patch *patch, patch_parse_ctx *ctx) +{ + if (parse_header_percent(&patch->delta->similarity, ctx) < 0) + return parse_err("invalid similarity percentage at line %d", + ctx->line_num); + + return 0; +} + +static int parse_header_dissimilarity(git_patch *patch, patch_parse_ctx *ctx) +{ + uint16_t dissimilarity; + + if (parse_header_percent(&dissimilarity, ctx) < 0) + return parse_err("invalid similarity percentage at line %d", + ctx->line_num); + + patch->delta->similarity = 100 - dissimilarity; + + return 0; +} + +typedef struct { + const char *str; + int (*fn)(git_patch *, patch_parse_ctx *); +} header_git_op; + +static const header_git_op header_git_ops[] = { + { "@@ -", NULL }, + { "--- ", parse_header_git_oldpath }, + { "+++ ", parse_header_git_newpath }, + { "index ", parse_header_git_index }, + { "old mode ", parse_header_git_oldmode }, + { "new mode ", parse_header_git_newmode }, + { "deleted file mode ", parse_header_git_deletedfilemode }, + { "new file mode ", parse_header_git_newfilemode }, + { "rename from ", parse_header_renamefrom }, + { "rename to ", parse_header_renameto }, + { "rename old ", parse_header_renamefrom }, + { "rename new ", parse_header_renameto }, + { "similarity index ", parse_header_similarity }, + { "dissimilarity index ", parse_header_dissimilarity }, +}; + +static int parse_header_git( + git_patch *patch, + patch_parse_ctx *ctx) +{ + size_t i; + int error = 0; + + /* Parse the diff --git line */ + if (parse_advance_expected(ctx, "diff --git ", 11) < 0) + return parse_err("corrupt git diff header at line %d", ctx->line_num); + + if (parse_header_path(&ctx->header_old_path, ctx) < 0) + return parse_err("corrupt old path in git diff header at line %d", + ctx->line_num); + + if (parse_advance_ws(ctx) < 0 || + parse_header_path(&ctx->header_new_path, ctx) < 0) + return parse_err("corrupt new path in git diff header at line %d", + ctx->line_num); + + /* Parse remaining header lines */ + for (parse_advance_line(ctx); ctx->remain > 0; parse_advance_line(ctx)) { + if (ctx->line_len == 0 || ctx->line[ctx->line_len - 1] != '\n') + break; + + for (i = 0; i < ARRAY_SIZE(header_git_ops); i++) { + const header_git_op *op = &header_git_ops[i]; + size_t op_len = strlen(op->str); + + if (memcmp(ctx->line, op->str, min(op_len, ctx->line_len)) != 0) + continue; + + /* Do not advance if this is the patch separator */ + if (op->fn == NULL) + goto done; + + parse_advance_chars(ctx, op_len); + + if ((error = op->fn(patch, ctx)) < 0) + goto done; + + parse_advance_ws(ctx); + parse_advance_expected(ctx, "\n", 1); + + if (ctx->line_len > 0) { + error = parse_err("trailing data at line %d", ctx->line_num); + goto done; + } + + break; + } + } + +done: + return error; +} + +static int parse_number(int *out, patch_parse_ctx *ctx) +{ + const char *end; + int64_t num; + + if (!git__isdigit(ctx->line[0])) + return -1; + + if (git__strntol64(&num, ctx->line, ctx->line_len, &end, 10) < 0) + return -1; + + if (num < 0) + return -1; + + *out = (int)num; + parse_advance_chars(ctx, (end - ctx->line)); + + return 0; +} + +static int parse_hunk_header( + diff_patch_hunk *hunk, + patch_parse_ctx *ctx) +{ + const char *header_start = ctx->line; + + hunk->hunk.old_lines = 1; + hunk->hunk.new_lines = 1; + + if (parse_advance_expected(ctx, "@@ -", 4) < 0 || + parse_number(&hunk->hunk.old_start, ctx) < 0) + goto fail; + + if (ctx->line_len > 0 && ctx->line[0] == ',') { + if (parse_advance_expected(ctx, ",", 1) < 0 || + parse_number(&hunk->hunk.old_lines, ctx) < 0) + goto fail; + } + + if (parse_advance_expected(ctx, " +", 2) < 0 || + parse_number(&hunk->hunk.new_start, ctx) < 0) + goto fail; + + if (ctx->line_len > 0 && ctx->line[0] == ',') { + if (parse_advance_expected(ctx, ",", 1) < 0 || + parse_number(&hunk->hunk.new_lines, ctx) < 0) + goto fail; + } + + if (parse_advance_expected(ctx, " @@", 3) < 0) + goto fail; + + parse_advance_line(ctx); + + if (!hunk->hunk.old_lines && !hunk->hunk.new_lines) + goto fail; + + hunk->hunk.header_len = ctx->line - header_start; + if (hunk->hunk.header_len > (GIT_DIFF_HUNK_HEADER_SIZE - 1)) + return parse_err("oversized patch hunk header at line %d", + ctx->line_num); + + memcpy(hunk->hunk.header, header_start, hunk->hunk.header_len); + hunk->hunk.header[hunk->hunk.header_len] = '\0'; + + return 0; + +fail: + giterr_set(GITERR_PATCH, "invalid patch hunk header at line %d", + ctx->line_num); + return -1; +} + +static int parse_hunk_body( + git_patch *patch, + diff_patch_hunk *hunk, + patch_parse_ctx *ctx) +{ + git_diff_line *line; + int error = 0; + + int oldlines = hunk->hunk.old_lines; + int newlines = hunk->hunk.new_lines; + + for (; + ctx->remain > 4 && (oldlines || newlines) && + memcmp(ctx->line, "@@ -", 4) != 0; + parse_advance_line(ctx)) { + + int origin; + int prefix = 1; + + if (ctx->line_len == 0 || ctx->line[ctx->line_len - 1] != '\n') { + error = parse_err("invalid patch instruction at line %d", + ctx->line_num); + goto done; + } + + switch (ctx->line[0]) { + case '\n': + prefix = 0; + + case ' ': + origin = GIT_DIFF_LINE_CONTEXT; + oldlines--; + newlines--; + break; + + case '-': + origin = GIT_DIFF_LINE_DELETION; + oldlines--; + break; + + case '+': + origin = GIT_DIFF_LINE_ADDITION; + newlines--; + break; + + default: + error = parse_err("invalid patch hunk at line %d", ctx->line_num); + goto done; + } + + line = git_array_alloc(patch->lines); + GITERR_CHECK_ALLOC(line); + + memset(line, 0x0, sizeof(git_diff_line)); + + line->content = ctx->line + prefix; + line->content_len = ctx->line_len - prefix; + line->content_offset = ctx->content_len - ctx->remain; + line->origin = origin; + + hunk->line_count++; + } + + if (oldlines || newlines) { + error = parse_err( + "invalid patch hunk, expected %d old lines and %d new lines", + hunk->hunk.old_lines, hunk->hunk.new_lines); + goto done; + } + + /* Handle "\ No newline at end of file". Only expect the leading + * backslash, though, because the rest of the string could be + * localized. Because `diff` optimizes for the case where you + * want to apply the patch by hand. + */ + if (ctx->line_len >= 2 && memcmp(ctx->line, "\\ ", 2) == 0 && + git_array_size(patch->lines) > 0) { + + line = git_array_get(patch->lines, git_array_size(patch->lines)-1); + + if (line->content_len < 1) { + error = parse_err("cannot trim trailing newline of empty line"); + goto done; + } + + line->content_len--; + + parse_advance_line(ctx); + } + +done: + return error; +} + +static int parse_header_traditional(git_patch *patch, patch_parse_ctx *ctx) +{ + GIT_UNUSED(patch); + GIT_UNUSED(ctx); + + return 1; +} + +static int parse_patch_header( + git_patch *patch, + patch_parse_ctx *ctx) +{ + int error = 0; + + for (ctx->line = ctx->content; ctx->remain > 0; parse_advance_line(ctx)) { + /* This line is too short to be a patch header. */ + if (ctx->line_len < 6) + continue; + + /* This might be a hunk header without a patch header, provide a + * sensible error message. */ + if (memcmp(ctx->line, "@@ -", 4) == 0) { + size_t line_num = ctx->line_num; + diff_patch_hunk hunk; + + /* If this cannot be parsed as a hunk header, it's just leading + * noise, continue. + */ + if (parse_hunk_header(&hunk, ctx) < 0) { + giterr_clear(); + continue; + } + + error = parse_err("invalid hunk header outside patch at line %d", + line_num); + goto done; + } + + /* This buffer is too short to contain a patch. */ + if (ctx->remain < ctx->line_len + 6) + break; + + /* A proper git patch */ + if (ctx->line_len >= 11 && memcmp(ctx->line, "diff --git ", 11) == 0) { + if ((error = parse_header_git(patch, ctx)) < 0) + goto done; + + /* For modechange only patches, it does not include filenames; + * instead we need to use the paths in the diff --git header. + */ + if (!patch->ofile.file->path && !patch->nfile.file->path) { + if (!ctx->header_old_path || !ctx->header_new_path) { + error = parse_err("git diff header lacks old / new paths"); + goto done; + } + + patch->ofile.file->path = ctx->header_old_path; + ctx->header_old_path = NULL; + + patch->nfile.file->path = ctx->header_new_path; + ctx->header_new_path = NULL; + } + + goto done; + } + + if ((error = parse_header_traditional(patch, ctx)) <= 0) + goto done; + + error = 0; + continue; + } + + error = parse_err("no header in patch file"); + +done: + return error; +} + +static int parse_patch_body( + git_patch *patch, + patch_parse_ctx *ctx) +{ + diff_patch_hunk *hunk; + int error = 0; + + for (; ctx->line_len > 4 && memcmp(ctx->line, "@@ -", 4) == 0; ) { + + hunk = git_array_alloc(patch->hunks); + GITERR_CHECK_ALLOC(hunk); + + memset(hunk, 0, sizeof(diff_patch_hunk)); + + hunk->line_start = git_array_size(patch->lines); + hunk->line_count = 0; + + if ((error = parse_hunk_header(hunk, ctx)) < 0 || + (error = parse_hunk_body(patch, hunk, ctx)) < 0) + goto done; + } + +done: + return error; +} + +static int check_patch(git_patch *patch) +{ + if (!patch->ofile.file->path && patch->delta->status != GIT_DELTA_ADDED) + return parse_err("missing old file path"); + + if (!patch->nfile.file->path && patch->delta->status != GIT_DELTA_DELETED) + return parse_err("missing new file path"); + + if (patch->ofile.file->path && patch->nfile.file->path) { + if (!patch->nfile.file->mode) + patch->nfile.file->mode = patch->ofile.file->mode; + } + + if (patch->delta->status == GIT_DELTA_MODIFIED && + patch->nfile.file->mode == patch->ofile.file->mode && + git_array_size(patch->hunks) == 0) + return parse_err("patch with no hunks"); + + return 0; +} + +int git_patch_from_patchfile( + git_patch **out, + const char *content, + size_t content_len) +{ + patch_parse_ctx ctx = {0}; + git_patch *patch; + int error = 0; + + *out = NULL; + + patch = git__calloc(1, sizeof(git_patch)); + GITERR_CHECK_ALLOC(patch); + + patch->delta = git__calloc(1, sizeof(git_diff_delta)); + patch->ofile.file = git__calloc(1, sizeof(git_diff_file)); + patch->nfile.file = git__calloc(1, sizeof(git_diff_file)); + + patch->delta->status = GIT_DELTA_MODIFIED; + + ctx.content = content; + ctx.content_len = content_len; + ctx.remain = content_len; + + if ((error = parse_patch_header(patch, &ctx)) < 0 || + (error = parse_patch_body(patch, &ctx)) < 0 || + (error = check_patch(patch)) < 0) + goto done; + + *out = patch; + +done: + git__free(ctx.header_old_path); + git__free(ctx.header_new_path); + + return error; +} diff --git a/src/path.c b/src/path.c index 4133985a45b..e5f04a56a3e 100644 --- a/src/path.c +++ b/src/path.c @@ -306,6 +306,25 @@ int git_path_join_unrooted( return 0; } +void git_path_squash_slashes(git_buf *path) +{ + char *p, *q; + + if (path->size == 0) + return; + + for (p = path->ptr, q = path->ptr; *q; p++, q++) { + *p = *q; + + while (*q == '/' && *(q+1) == '/') { + path->size--; + q++; + } + } + + *p = '\0'; +} + int git_path_prettify(git_buf *path_out, const char *path, const char *base) { char buf[GIT_PATH_MAX]; diff --git a/src/path.h b/src/path.h index f31cacc7079..fb45a6534bf 100644 --- a/src/path.h +++ b/src/path.h @@ -243,6 +243,12 @@ extern bool git_path_contains_file(git_buf *dir, const char *file); extern int git_path_join_unrooted( git_buf *path_out, const char *path, const char *base, ssize_t *root_at); +/** + * Removes multiple occurrences of '/' in a row, squashing them into a + * single '/'. + */ +extern void git_path_squash_slashes(git_buf *path); + /** * Clean up path, prepending base if it is not already rooted. */ diff --git a/src/util.c b/src/util.c index 9e67f434743..3090c7437e2 100644 --- a/src/util.c +++ b/src/util.c @@ -65,6 +65,12 @@ int git_strarray_copy(git_strarray *tgt, const git_strarray *src) } int git__strtol64(int64_t *result, const char *nptr, const char **endptr, int base) +{ + + return git__strntol64(result, nptr, (size_t)-1, endptr, base); +} + +int git__strntol64(int64_t *result, const char *nptr, size_t nptr_len, const char **endptr, int base) { const char *p; int64_t n, nn; @@ -111,7 +117,7 @@ int git__strtol64(int64_t *result, const char *nptr, const char **endptr, int ba /* * Non-empty sequence of digits */ - for (;; p++,ndig++) { + for (; nptr_len > 0; p++,ndig++,nptr_len--) { c = *p; v = base; if ('0'<=c && c<='9') @@ -147,12 +153,18 @@ int git__strtol64(int64_t *result, const char *nptr, const char **endptr, int ba } int git__strtol32(int32_t *result, const char *nptr, const char **endptr, int base) +{ + + return git__strntol32(result, nptr, (size_t)-1, endptr, base); +} + +int git__strntol32(int32_t *result, const char *nptr, size_t nptr_len, const char **endptr, int base) { int error; int32_t tmp_int; int64_t tmp_long; - if ((error = git__strtol64(&tmp_long, nptr, endptr, base)) < 0) + if ((error = git__strntol64(&tmp_long, nptr, nptr_len, endptr, base)) < 0) return error; tmp_int = tmp_long & 0xFFFFFFFF; @@ -321,6 +333,12 @@ char *git__strsep(char **end, const char *sep) return NULL; } +size_t git__linenlen(const char *buffer, size_t buffer_len) +{ + char *nl = memchr(buffer, '\n', buffer_len); + return nl ? (size_t)(nl - buffer) + 1 : buffer_len; +} + void git__hexdump(const char *buffer, size_t len) { static const size_t LINE_WIDTH = 16; diff --git a/src/util.h b/src/util.h index d0c3cd04aa2..eb15250d8dd 100644 --- a/src/util.h +++ b/src/util.h @@ -263,7 +263,10 @@ GIT_INLINE(int) git__signum(int val) } extern int git__strtol32(int32_t *n, const char *buff, const char **end_buf, int base); +extern int git__strntol32(int32_t *n, const char *buff, size_t buff_len, const char **end_buf, int base); extern int git__strtol64(int64_t *n, const char *buff, const char **end_buf, int base); +extern int git__strntol64(int64_t *n, const char *buff, size_t buff_len, const char **end_buf, int base); + extern void git__hexdump(const char *buffer, size_t n); extern uint32_t git__hash(const void *key, int len, uint32_t seed); @@ -290,6 +293,8 @@ GIT_INLINE(int) git__tolower(int c) # define git__tolower(a) tolower(a) #endif +extern size_t git__linenlen(const char *buffer, size_t buffer_len); + GIT_INLINE(const char *) git__next_line(const char *s) { while (*s && *s != '\n') s++; @@ -466,6 +471,11 @@ GIT_INLINE(bool) git__iswildcard(int c) return (c == '*' || c == '?' || c == '['); } +GIT_INLINE(bool) git__isxdigit(int c) +{ + return ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); +} + /* * Parse a string value as a boolean, just like Core Git does. * diff --git a/tests/apply/apply_common.h b/tests/apply/apply_common.h index 8226cc1da2c..595b96e3127 100644 --- a/tests/apply/apply_common.h +++ b/tests/apply/apply_common.h @@ -284,3 +284,192 @@ " and this\n" \ " is additional context\n" \ " below it!\n" + +#define PATCH_RENAME_EXACT_QUOTEDNAME \ + "diff --git a/file.txt \"b/foo\\\"bar.txt\"\n" \ + "similarity index 100%\n" \ + "rename from file.txt\n" \ + "rename to \"foo\\\"bar.txt\"\n" + +#define PATCH_RENAME_SIMILAR_QUOTEDNAME \ + "diff --git a/file.txt \"b/foo\\\"bar.txt\"\n" \ + "similarity index 77%\n" \ + "rename from file.txt\n" \ + "rename to \"foo\\\"bar.txt\"\n" \ + "index 9432026..cd8fd12 100644\n" \ + "--- a/file.txt\n" \ + "+++ \"b/foo\\\"bar.txt\"\n" \ + "@@ -3,7 +3,7 @@ this is some context!\n" \ + " around some lines\n" \ + " that will change\n" \ + " yes it is!\n" \ + "-(this line is changed)\n" \ + "+(THIS line is changed!)\n" \ + " and this\n" \ + " is additional context\n" \ + " below it!\n" + +#define PATCH_MODECHANGE_UNCHANGED \ + "diff --git a/file.txt b/file.txt\n" \ + "old mode 100644\n" \ + "new mode 100755\n" + +#define PATCH_MODECHANGE_MODIFIED \ + "diff --git a/file.txt b/file.txt\n" \ + "old mode 100644\n" \ + "new mode 100755\n" \ + "index 9432026..cd8fd12\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -3,7 +3,7 @@ this is some context!\n" \ + " around some lines\n" \ + " that will change\n" \ + " yes it is!\n" \ + "-(this line is changed)\n" \ + "+(THIS line is changed!)\n" \ + " and this\n" \ + " is additional context\n" \ + " below it!\n" + +#define PATCH_NOISY \ + "This is some\nleading noise\n@@ - that\nlooks like a hunk header\n" \ + "but actually isn't and should parse ok\n" \ + PATCH_ORIGINAL_TO_CHANGE_MIDDLE \ + "plus some trailing garbage for good measure\n" + +#define PATCH_NOISY_NOCONTEXT \ + "This is some\nleading noise\n@@ - that\nlooks like a hunk header\n" \ + "but actually isn't and should parse ok\n" \ + PATCH_ORIGINAL_TO_CHANGE_MIDDLE_NOCONTEXT \ + "plus some trailing garbage for good measure\n" + +#define PATCH_TRUNCATED_1 \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..cd8fd12 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -3,7 +3,7 @@ this is some context!\n" \ + " around some lines\n" \ + " that will change\n" \ + " yes it is!\n" \ + "-(this line is changed)\n" \ + "+(THIS line is changed!)\n" \ + " and this\n" + +#define PATCH_TRUNCATED_2 \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..cd8fd12 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -3,7 +3,7 @@ this is some context!\n" \ + " around some lines\n" \ + "-(this line is changed)\n" \ + "+(THIS line is changed!)\n" \ + " and this\n" \ + " is additional context\n" \ + " below it!\n" + +#define PATCH_TRUNCATED_3 \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..cd8fd12 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -3,7 +3,7 @@ this is some context!\n" \ + " around some lines\n" \ + " that will change\n" \ + " yes it is!\n" \ + "+(THIS line is changed!)\n" \ + " and this\n" \ + " is additional context\n" \ + " below it!\n" + +#define FILE_EMPTY_CONTEXT_ORIGINAL \ + "this\nhas\nan\n\nempty\ncontext\nline\n" + +#define FILE_EMPTY_CONTEXT_MODIFIED \ + "this\nhas\nan\n\nempty...\ncontext\nline\n" + +#define PATCH_EMPTY_CONTEXT \ + "diff --git a/file.txt b/file.txt\n" \ + "index 398d2df..bb15234 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -2,6 +2,6 @@ this\n" \ + " has\n" \ + " an\n" \ + "\n" \ + "-empty\n" \ + "+empty...\n" \ + " context\n" \ + " line\n" + +#define FILE_APPEND_NO_NL \ + "hey!\n" \ + "this is some context!\n" \ + "around some lines\n" \ + "that will change\n" \ + "yes it is!\n" \ + "(this line is changed)\n" \ + "and this\n" \ + "is additional context\n" \ + "below it!\n" \ + "added line with no nl" + +#define PATCH_APPEND_NO_NL \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..83759c0 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -7,3 +7,4 @@ yes it is!\n" \ + " and this\n" \ + " is additional context\n" \ + " below it!\n" \ + "+added line with no nl\n" \ + "\\ No newline at end of file\n" + +#define PATCH_CORRUPT_GIT_HEADER \ + "diff --git a/file.txt\n" \ + "index 9432026..0f39b9a 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -0,0 +1 @@\n" \ + "+insert at front\n" + +#define PATCH_CORRUPT_MISSING_NEW_FILE \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..cd8fd12 100644\n" \ + "--- a/file.txt\n" \ + "@@ -6 +6 @@ yes it is!\n" \ + "-(this line is changed)\n" \ + "+(THIS line is changed!)\n" + +#define PATCH_CORRUPT_MISSING_OLD_FILE \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..cd8fd12 100644\n" \ + "+++ b/file.txt\n" \ + "@@ -6 +6 @@ yes it is!\n" \ + "-(this line is changed)\n" \ + "+(THIS line is changed!)\n" + +#define PATCH_CORRUPT_NO_CHANGES \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..cd8fd12 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -0,0 +0,0 @@ yes it is!\n" + +#define PATCH_CORRUPT_MISSING_HUNK_HEADER \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..cd8fd12 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "-(this line is changed)\n" \ + "+(THIS line is changed!)\n" + +#define PATCH_NOT_A_PATCH \ + "+++this is not\n" \ + "--actually even\n" \ + " a legitimate \n" \ + "+patch file\n" \ + "-it's something else\n" \ + " entirely!" diff --git a/tests/apply/fromfile.c b/tests/apply/fromfile.c new file mode 100644 index 00000000000..cbf50d82d21 --- /dev/null +++ b/tests/apply/fromfile.c @@ -0,0 +1,301 @@ +#include "clar_libgit2.h" +#include "git2/sys/repository.h" + +#include "apply.h" +#include "repository.h" +#include "buf_text.h" + +#include "apply_common.h" + +static git_repository *repo = NULL; + +void test_apply_fromfile__initialize(void) +{ + repo = cl_git_sandbox_init("renames"); +} + +void test_apply_fromfile__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +static int apply_patchfile( + const char *old, + const char *new, + const char *patchfile, + const char *filename_expected, + unsigned int mode_expected) +{ + git_patch *patch; + git_buf result = GIT_BUF_INIT; + git_buf patchbuf = GIT_BUF_INIT; + char *filename; + unsigned int mode; + int error; + + cl_git_pass(git_patch_from_patchfile(&patch, patchfile, strlen(patchfile))); + + error = git_apply__patch(&result, &filename, &mode, old, old ? strlen(old) : 0, patch); + + if (error == 0) { + if (new == NULL) + cl_assert_equal_i(0, result.size); + else + cl_assert_equal_s(new, result.ptr); + + cl_assert_equal_s(filename_expected, filename); + cl_assert_equal_i(mode_expected, mode); + } + + git__free(filename); + git_buf_free(&result); + git_buf_free(&patchbuf); + git_patch_free(patch); + + return error; +} + +static int validate_and_apply_patchfile( + const char *old, + const char *new, + const char *patchfile, + const git_diff_options *diff_opts, + const char *filename_expected, + unsigned int mode_expected) +{ + git_patch *patch_fromdiff; + git_buf validated = GIT_BUF_INIT; + int error; + + cl_git_pass(git_patch_from_buffers(&patch_fromdiff, + old, old ? strlen(old) : 0, "file.txt", + new, new ? strlen(new) : 0, "file.txt", + diff_opts)); + cl_git_pass(git_patch_to_buf(&validated, patch_fromdiff)); + + cl_assert_equal_s(patchfile, validated.ptr); + + error = apply_patchfile(old, new, patchfile, filename_expected, mode_expected); + + git_buf_free(&validated); + git_patch_free(patch_fromdiff); + + return error; +} + +void test_apply_fromfile__change_middle(void) +{ + cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, + FILE_CHANGE_MIDDLE, PATCH_ORIGINAL_TO_CHANGE_MIDDLE, NULL, + "b/file.txt", 0100644)); +} + +void test_apply_fromfile__change_middle_nocontext(void) +{ + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + diff_opts.context_lines = 0; + + cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, + FILE_CHANGE_MIDDLE, PATCH_ORIGINAL_TO_CHANGE_MIDDLE_NOCONTEXT, + &diff_opts, "b/file.txt", 0100644)); +} + +void test_apply_fromfile__change_firstline(void) +{ + cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, + FILE_CHANGE_FIRSTLINE, PATCH_ORIGINAL_TO_CHANGE_FIRSTLINE, NULL, + "b/file.txt", 0100644)); +} + +void test_apply_fromfile__lastline(void) +{ + cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, + FILE_CHANGE_LASTLINE, PATCH_ORIGINAL_TO_CHANGE_LASTLINE, NULL, + "b/file.txt", 0100644)); +} + +void test_apply_fromfile__prepend(void) +{ + cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, FILE_PREPEND, + PATCH_ORIGINAL_TO_PREPEND, NULL, "b/file.txt", 0100644)); +} + +void test_apply_fromfile__prepend_nocontext(void) +{ + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + diff_opts.context_lines = 0; + + cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, FILE_PREPEND, + PATCH_ORIGINAL_TO_PREPEND_NOCONTEXT, &diff_opts, + "b/file.txt", 0100644)); +} + +void test_apply_fromfile__append(void) +{ + cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, FILE_APPEND, + PATCH_ORIGINAL_TO_APPEND, NULL, "b/file.txt", 0100644)); +} + +void test_apply_fromfile__append_nocontext(void) +{ + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + diff_opts.context_lines = 0; + + cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, FILE_APPEND, + PATCH_ORIGINAL_TO_APPEND_NOCONTEXT, &diff_opts, + "b/file.txt", 0100644)); +} + +void test_apply_fromfile__prepend_and_append(void) +{ + cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, + FILE_PREPEND_AND_APPEND, PATCH_ORIGINAL_TO_PREPEND_AND_APPEND, NULL, + "b/file.txt", 0100644)); +} + +void test_apply_fromfile__to_empty_file(void) +{ + cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, "", + PATCH_ORIGINAL_TO_EMPTY_FILE, NULL, "b/file.txt", 0100644)); +} + +void test_apply_fromfile__from_empty_file(void) +{ + cl_git_pass(validate_and_apply_patchfile("", FILE_ORIGINAL, + PATCH_EMPTY_FILE_TO_ORIGINAL, NULL, "b/file.txt", 0100644)); +} + +void test_apply_fromfile__add(void) +{ + cl_git_pass(validate_and_apply_patchfile(NULL, FILE_ORIGINAL, + PATCH_ADD_ORIGINAL, NULL, "b/file.txt", 0100644)); +} + +void test_apply_fromfile__delete(void) +{ + cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, NULL, + PATCH_DELETE_ORIGINAL, NULL, NULL, 0)); +} + + +void test_apply_fromfile__rename_exact(void) +{ + cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_ORIGINAL, + PATCH_RENAME_EXACT, "b/newfile.txt", 0100644)); +} + +void test_apply_fromfile__rename_similar(void) +{ + cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + PATCH_RENAME_SIMILAR, "b/newfile.txt", 0100644)); +} + +void test_apply_fromfile__rename_similar_quotedname(void) +{ + cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + PATCH_RENAME_SIMILAR_QUOTEDNAME, "b/foo\"bar.txt", 0100644)); +} + +void test_apply_fromfile__modechange(void) +{ + cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_ORIGINAL, + PATCH_MODECHANGE_UNCHANGED, "b/file.txt", 0100755)); +} + +void test_apply_fromfile__modechange_with_modification(void) +{ + cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + PATCH_MODECHANGE_MODIFIED, "b/file.txt", 0100755)); +} + +void test_apply_fromfile__noisy(void) +{ + cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + PATCH_NOISY, "b/file.txt", 0100644)); +} + +void test_apply_fromfile__noisy_nocontext(void) +{ + cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + PATCH_NOISY_NOCONTEXT, "b/file.txt", 0100644)); +} + +void test_apply_fromfile__fail_truncated_1(void) +{ + git_patch *patch; + cl_git_fail(git_patch_from_patchfile(&patch, PATCH_TRUNCATED_1, + strlen(PATCH_TRUNCATED_1))); +} + +void test_apply_fromfile__fail_truncated_2(void) +{ + git_patch *patch; + cl_git_fail(git_patch_from_patchfile(&patch, PATCH_TRUNCATED_2, + strlen(PATCH_TRUNCATED_2))); +} + +void test_apply_fromfile__fail_truncated_3(void) +{ + git_patch *patch; + cl_git_fail(git_patch_from_patchfile(&patch, PATCH_TRUNCATED_3, + strlen(PATCH_TRUNCATED_3))); +} + +void test_apply_fromfile__fail_corrupt_githeader(void) +{ + git_patch *patch; + cl_git_fail(git_patch_from_patchfile(&patch, PATCH_CORRUPT_GIT_HEADER, + strlen(PATCH_CORRUPT_GIT_HEADER))); +} + +void test_apply_fromfile__empty_context(void) +{ + cl_git_pass(apply_patchfile(FILE_EMPTY_CONTEXT_ORIGINAL, + FILE_EMPTY_CONTEXT_MODIFIED, PATCH_EMPTY_CONTEXT, + "b/file.txt", 0100644)); +} + +void test_apply_fromfile__append_no_nl(void) +{ + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, FILE_APPEND_NO_NL, PATCH_APPEND_NO_NL, NULL, "b/file.txt", 0100644)); +} + +void test_apply_fromfile__fail_missing_new_file(void) +{ + git_patch *patch; + cl_git_fail(git_patch_from_patchfile(&patch, + PATCH_CORRUPT_MISSING_NEW_FILE, + strlen(PATCH_CORRUPT_MISSING_NEW_FILE))); +} + +void test_apply_fromfile__fail_missing_old_file(void) +{ + git_patch *patch; + cl_git_fail(git_patch_from_patchfile(&patch, + PATCH_CORRUPT_MISSING_OLD_FILE, + strlen(PATCH_CORRUPT_MISSING_OLD_FILE))); +} + +void test_apply_fromfile__fail_no_changes(void) +{ + git_patch *patch; + cl_git_fail(git_patch_from_patchfile(&patch, + PATCH_CORRUPT_NO_CHANGES, + strlen(PATCH_CORRUPT_NO_CHANGES))); +} + +void test_apply_fromfile__fail_missing_hunk_header(void) +{ + git_patch *patch; + cl_git_fail(git_patch_from_patchfile(&patch, + PATCH_CORRUPT_MISSING_HUNK_HEADER, + strlen(PATCH_CORRUPT_MISSING_HUNK_HEADER))); +} + +void test_apply_fromfile__fail_not_a_patch(void) +{ + git_patch *patch; + cl_git_fail(git_patch_from_patchfile(&patch, PATCH_NOT_A_PATCH, + strlen(PATCH_NOT_A_PATCH))); +} diff --git a/tests/buf/quote.c b/tests/buf/quote.c new file mode 100644 index 00000000000..ef26116632f --- /dev/null +++ b/tests/buf/quote.c @@ -0,0 +1,57 @@ +#include "clar_libgit2.h" +#include "buffer.h" + +static void expect_pass(const char *expected, const char *quoted) +{ + git_buf buf = GIT_BUF_INIT; + + cl_git_pass(git_buf_puts(&buf, quoted)); + cl_git_pass(git_buf_unquote(&buf)); + + cl_assert_equal_s(expected, git_buf_cstr(&buf)); + cl_assert_equal_i(strlen(expected), git_buf_len(&buf)); + + git_buf_free(&buf); +} + +static void expect_fail(const char *quoted) +{ + git_buf buf = GIT_BUF_INIT; + + cl_git_pass(git_buf_puts(&buf, quoted)); + cl_git_fail(git_buf_unquote(&buf)); + + git_buf_free(&buf); +} + +void test_buf_quote__unquote_succeeds(void) +{ + expect_pass("", "\"\""); + expect_pass(" ", "\" \""); + expect_pass("foo", "\"foo\""); + expect_pass("foo bar", "\"foo bar\""); + expect_pass("foo\"bar", "\"foo\\\"bar\""); + expect_pass("foo\\bar", "\"foo\\\\bar\""); + expect_pass("foo\tbar", "\"foo\\tbar\""); + expect_pass("\vfoo\tbar\n", "\"\\vfoo\\tbar\\n\""); + expect_pass("foo\nbar", "\"foo\\012bar\""); + expect_pass("foo\r\nbar", "\"foo\\015\\012bar\""); + expect_pass("foo\r\nbar", "\"\\146\\157\\157\\015\\012\\142\\141\\162\""); + expect_pass("newline: \n", "\"newline: \\012\""); +} + +void test_buf_quote__unquote_fails(void) +{ + expect_fail("no quotes at all"); + expect_fail("\"no trailing quote"); + expect_fail("no leading quote\""); + expect_fail("\"invalid \\z escape char\""); + expect_fail("\"\\q invalid escape char\""); + expect_fail("\"invalid escape char \\p\""); + expect_fail("\"invalid \\1 escape char \""); + expect_fail("\"invalid \\14 escape char \""); + expect_fail("\"invalid \\411 escape char\""); + expect_fail("\"truncated escape char \\\""); + expect_fail("\"truncated escape char \\0\""); + expect_fail("\"truncated escape char \\01\""); +} From 0004386f29d1165d5dbd54b26170560a7a98e125 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 17 Jun 2015 06:03:01 -0700 Subject: [PATCH 03/54] apply: handle empty patches When a patch is empty, simply copy the source into the destination. --- src/apply.c | 5 ++++- tests/apply/fromdiff.c | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/apply.c b/src/apply.c index e75fa5b4d5e..f1bd9f4b575 100644 --- a/src/apply.c +++ b/src/apply.c @@ -262,7 +262,10 @@ int git_apply__patch( patch->nfile.file->mode : GIT_FILEMODE_BLOB; } - if ((error = apply_hunks(contents_out, source, source_len, patch)) < 0) + /* If the patch is empty, simply keep the source unchanged */ + if (patch->hunks.size == 0) + git_buf_put(contents_out, source, source_len); + else if ((error = apply_hunks(contents_out, source, source_len, patch)) < 0) goto done; if (patch->delta->status == GIT_DELTA_DELETED && diff --git a/tests/apply/fromdiff.c b/tests/apply/fromdiff.c index 64ed9de7932..af0541de8cb 100644 --- a/tests/apply/fromdiff.c +++ b/tests/apply/fromdiff.c @@ -174,3 +174,11 @@ void test_apply_fromdiff__delete(void) NULL, NULL, PATCH_DELETE_ORIGINAL, NULL)); } + +void test_apply_fromdiff__no_change(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_ORIGINAL, "file.txt", + "", NULL)); +} From 6a2d2f8aa14462396cbc7d3e408ed28430e212e2 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 17 Jun 2015 06:42:20 -0700 Subject: [PATCH 04/54] delta: move delta application to delta.c Move the delta application functions into `delta.c`, next to the similar delta creation functions. Make the `git__delta_apply` functions adhere to other naming and parameter style within the library. --- src/delta-apply.c | 166 --------------------------------------------- src/delta-apply.h | 62 ----------------- src/delta.c | 167 ++++++++++++++++++++++++++++++++++++++++++++++ src/delta.h | 51 +++++++++++++- src/odb.c | 2 +- src/odb_loose.c | 2 +- src/odb_pack.c | 2 +- src/pack.c | 7 +- 8 files changed, 223 insertions(+), 236 deletions(-) delete mode 100644 src/delta-apply.c delete mode 100644 src/delta-apply.h diff --git a/src/delta-apply.c b/src/delta-apply.c deleted file mode 100644 index 02ec7b75efb..00000000000 --- a/src/delta-apply.c +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (C) the libgit2 contributors. All rights reserved. - * - * This file is part of libgit2, distributed under the GNU GPL v2 with - * a Linking Exception. For full terms see the included COPYING file. - */ -#include "common.h" -#include "git2/odb.h" -#include "delta-apply.h" - -/* - * This file was heavily cribbed from BinaryDelta.java in JGit, which - * itself was heavily cribbed from patch-delta.c in the - * GIT project. The original delta patching code was written by - * Nicolas Pitre . - */ - -static int hdr_sz( - size_t *size, - const unsigned char **delta, - const unsigned char *end) -{ - const unsigned char *d = *delta; - size_t r = 0; - unsigned int c, shift = 0; - - do { - if (d == end) - return -1; - c = *d++; - r |= (c & 0x7f) << shift; - shift += 7; - } while (c & 0x80); - *delta = d; - *size = r; - 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; -} - -#define DELTA_HEADER_BUFFER_LEN 16 -int git__delta_read_header_fromstream(size_t *base_sz, size_t *res_sz, git_packfile_stream *stream) -{ - static const size_t buffer_len = DELTA_HEADER_BUFFER_LEN; - unsigned char buffer[DELTA_HEADER_BUFFER_LEN]; - const unsigned char *delta, *delta_end; - size_t len; - ssize_t read; - - len = read = 0; - while (len < buffer_len) { - read = git_packfile_stream_read(stream, &buffer[len], buffer_len - len); - - if (read == 0) - break; - - if (read == GIT_EBUFS) - continue; - - len += read; - } - - delta = buffer; - delta_end = 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, - size_t base_len, - const unsigned char *delta, - size_t delta_len) -{ - const unsigned char *delta_end = delta + delta_len; - size_t base_sz, res_sz, alloc_sz; - unsigned char *res_dp; - - /* Check that the base size matches the data we were given; - * if not we would underflow while accessing data from the - * base object, resulting in data corruption or segfault. - */ - if ((hdr_sz(&base_sz, &delta, delta_end) < 0) || (base_sz != base_len)) { - giterr_set(GITERR_INVALID, "Failed to apply delta. Base size does not match given data"); - return -1; - } - - if (hdr_sz(&res_sz, &delta, delta_end) < 0) { - giterr_set(GITERR_INVALID, "Failed to apply delta. Base size does not match given data"); - return -1; - } - - GITERR_CHECK_ALLOC_ADD(&alloc_sz, res_sz, 1); - res_dp = git__malloc(alloc_sz); - GITERR_CHECK_ALLOC(res_dp); - - res_dp[res_sz] = '\0'; - out->data = res_dp; - out->len = res_sz; - - while (delta < delta_end) { - unsigned char cmd = *delta++; - if (cmd & 0x80) { - /* cmd is a copy instruction; copy from the base. - */ - size_t off = 0, len = 0; - - if (cmd & 0x01) off = *delta++; - if (cmd & 0x02) off |= *delta++ << 8UL; - if (cmd & 0x04) off |= *delta++ << 16UL; - if (cmd & 0x08) off |= *delta++ << 24UL; - - if (cmd & 0x10) len = *delta++; - if (cmd & 0x20) len |= *delta++ << 8UL; - if (cmd & 0x40) len |= *delta++ << 16UL; - if (!len) len = 0x10000; - - if (base_len < off + len || res_sz < len) - goto fail; - memcpy(res_dp, base + off, len); - res_dp += len; - res_sz -= len; - - } else if (cmd) { - /* cmd is a literal insert instruction; copy from - * the delta stream itself. - */ - if (delta_end - delta < cmd || res_sz < cmd) - goto fail; - memcpy(res_dp, delta, cmd); - delta += cmd; - res_dp += cmd; - res_sz -= cmd; - - } else { - /* cmd == 0 is reserved for future encodings. - */ - goto fail; - } - } - - if (delta != delta_end || res_sz) - goto fail; - return 0; - -fail: - git__free(out->data); - out->data = NULL; - giterr_set(GITERR_INVALID, "Failed to apply delta"); - return -1; -} diff --git a/src/delta-apply.h b/src/delta-apply.h deleted file mode 100644 index eeeb7868234..00000000000 --- a/src/delta-apply.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) the libgit2 contributors. All rights reserved. - * - * This file is part of libgit2, distributed under the GNU GPL v2 with - * a Linking Exception. For full terms see the included COPYING file. - */ -#ifndef INCLUDE_delta_apply_h__ -#define INCLUDE_delta_apply_h__ - -#include "odb.h" -#include "pack.h" - -/** - * Apply a git binary delta to recover the original content. - * - * @param out the output buffer to receive the original data. - * Only out->data and out->len are populated, as this is - * the only information available in the delta. - * @param base the base to copy from during copy instructions. - * @param base_len number of bytes available at base. - * @param delta the delta to execute copy/insert instructions from. - * @param delta_len total number of bytes in the delta. - * @return - * - 0 on a successful delta unpack. - * - GIT_ERROR if the delta is corrupt or doesn't match the base. - */ -extern int git__delta_apply( - git_rawobj *out, - const unsigned char *base, - size_t base_len, - 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); - -/** - * Read the header of a git binary delta - * - * This variant reads just enough from the packfile stream to read the - * delta header. - */ -extern int git__delta_read_header_fromstream( - size_t *base_sz, - size_t *res_sz, - git_packfile_stream *stream); - -#endif diff --git a/src/delta.c b/src/delta.c index d72d820d842..8a4c2a10458 100644 --- a/src/delta.c +++ b/src/delta.c @@ -441,3 +441,170 @@ git_delta_create( *delta_size = outpos; return out; } + +/* +* Delta application was heavily cribbed from BinaryDelta.java in JGit, which +* itself was heavily cribbed from patch-delta.c in the +* GIT project. The original delta patching code was written by +* Nicolas Pitre . +*/ + +static int hdr_sz( + size_t *size, + const unsigned char **delta, + const unsigned char *end) +{ + const unsigned char *d = *delta; + size_t r = 0; + unsigned int c, shift = 0; + + do { + if (d == end) + return -1; + c = *d++; + r |= (c & 0x7f) << shift; + shift += 7; + } while (c & 0x80); + *delta = d; + *size = r; + return 0; +} + +int git_delta_read_header( + size_t *base_out, + size_t *result_out, + const unsigned char *delta, + size_t delta_len) +{ + const unsigned char *delta_end = delta + delta_len; + if ((hdr_sz(base_out, &delta, delta_end) < 0) || + (hdr_sz(result_out, &delta, delta_end) < 0)) + return -1; + return 0; +} + +#define DELTA_HEADER_BUFFER_LEN 16 +int git_delta_read_header_fromstream( + size_t *base_sz, size_t *res_sz, git_packfile_stream *stream) +{ + static const size_t buffer_len = DELTA_HEADER_BUFFER_LEN; + unsigned char buffer[DELTA_HEADER_BUFFER_LEN]; + const unsigned char *delta, *delta_end; + size_t len; + ssize_t read; + + len = read = 0; + while (len < buffer_len) { + read = git_packfile_stream_read(stream, &buffer[len], buffer_len - len); + + if (read == 0) + break; + + if (read == GIT_EBUFS) + continue; + + len += read; + } + + delta = buffer; + delta_end = 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( + void **out, + size_t *out_len, + const unsigned char *base, + size_t base_len, + const unsigned char *delta, + size_t delta_len) +{ + const unsigned char *delta_end = delta + delta_len; + size_t base_sz, res_sz, alloc_sz; + unsigned char *res_dp; + + *out = NULL; + *out_len = 0; + + /* Check that the base size matches the data we were given; + * if not we would underflow while accessing data from the + * base object, resulting in data corruption or segfault. + */ + if ((hdr_sz(&base_sz, &delta, delta_end) < 0) || (base_sz != base_len)) { + giterr_set(GITERR_INVALID, "Failed to apply delta. Base size does not match given data"); + return -1; + } + + if (hdr_sz(&res_sz, &delta, delta_end) < 0) { + giterr_set(GITERR_INVALID, "Failed to apply delta. Base size does not match given data"); + return -1; + } + + GITERR_CHECK_ALLOC_ADD(&alloc_sz, res_sz, 1); + res_dp = git__malloc(alloc_sz); + GITERR_CHECK_ALLOC(res_dp); + + res_dp[res_sz] = '\0'; + *out = res_dp; + *out_len = res_sz; + + while (delta < delta_end) { + unsigned char cmd = *delta++; + if (cmd & 0x80) { + /* cmd is a copy instruction; copy from the base. + */ + size_t off = 0, len = 0; + + if (cmd & 0x01) off = *delta++; + if (cmd & 0x02) off |= *delta++ << 8UL; + if (cmd & 0x04) off |= *delta++ << 16UL; + if (cmd & 0x08) off |= *delta++ << 24UL; + + if (cmd & 0x10) len = *delta++; + if (cmd & 0x20) len |= *delta++ << 8UL; + if (cmd & 0x40) len |= *delta++ << 16UL; + if (!len) len = 0x10000; + + if (base_len < off + len || res_sz < len) + goto fail; + memcpy(res_dp, base + off, len); + res_dp += len; + res_sz -= len; + + } + else if (cmd) { + /* cmd is a literal insert instruction; copy from + * the delta stream itself. + */ + if (delta_end - delta < cmd || res_sz < cmd) + goto fail; + memcpy(res_dp, delta, cmd); + delta += cmd; + res_dp += cmd; + res_sz -= cmd; + + } + else { + /* cmd == 0 is reserved for future encodings. + */ + goto fail; + } + } + + if (delta != delta_end || res_sz) + goto fail; + return 0; + +fail: + git__free(*out); + + *out = NULL; + *out_len = 0; + + giterr_set(GITERR_INVALID, "Failed to apply delta"); + return -1; +} diff --git a/src/delta.h b/src/delta.h index 4ca32799217..d9d1d0fa8a2 100644 --- a/src/delta.h +++ b/src/delta.h @@ -6,6 +6,7 @@ #define INCLUDE_git_delta_h__ #include "common.h" +#include "pack.h" /* opaque object for delta index */ struct git_delta_index; @@ -19,8 +20,8 @@ struct git_delta_index; * 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); +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() @@ -111,4 +112,50 @@ GIT_INLINE(unsigned long) git_delta_get_hdr_size( return size; } +/** +* Apply a git binary delta to recover the original content. +* The caller is responsible for freeing the returned buffer. +* +* @param out the output buffer +* @param out_len the length of the output buffer +* @param base the base to copy from during copy instructions. +* @param base_len number of bytes available at base. +* @param delta the delta to execute copy/insert instructions from. +* @param delta_len total number of bytes in the delta. +* @return 0 on success or an error code +*/ +extern int git_delta_apply( + void **out, + size_t *out_len, + const unsigned char *base, + size_t base_len, + const unsigned char *delta, + size_t delta_len); + +/** +* Read the header of a git binary delta. +* +* @param base_out pointer to store the base size field. +* @param result_out pointer to store the result size field. +* @param delta the delta to execute copy/insert instructions from. +* @param delta_len total number of bytes in the delta. +* @return 0 on success or an error code +*/ +extern int git_delta_read_header( + size_t *base_out, + size_t *result_out, + const unsigned char *delta, + size_t delta_len); + +/** + * Read the header of a git binary delta + * + * This variant reads just enough from the packfile stream to read the + * delta header. + */ +extern int git_delta_read_header_fromstream( + size_t *base_out, + size_t *result_out, + git_packfile_stream *stream); + #endif diff --git a/src/odb.c b/src/odb.c index 890e6e2f8b9..777e3dc5c44 100644 --- a/src/odb.c +++ b/src/odb.c @@ -12,7 +12,7 @@ #include "fileops.h" #include "hash.h" #include "odb.h" -#include "delta-apply.h" +#include "delta.h" #include "filter.h" #include "repository.h" diff --git a/src/odb_loose.c b/src/odb_loose.c index 3c33160d07d..228d4c3347a 100644 --- a/src/odb_loose.c +++ b/src/odb_loose.c @@ -12,7 +12,7 @@ #include "fileops.h" #include "hash.h" #include "odb.h" -#include "delta-apply.h" +#include "delta.h" #include "filebuf.h" #include "git2/odb_backend.h" diff --git a/src/odb_pack.c b/src/odb_pack.c index 5a57864ad71..244e12bc372 100644 --- a/src/odb_pack.c +++ b/src/odb_pack.c @@ -13,7 +13,7 @@ #include "fileops.h" #include "hash.h" #include "odb.h" -#include "delta-apply.h" +#include "delta.h" #include "sha1_lookup.h" #include "mwindow.h" #include "pack.h" diff --git a/src/pack.c b/src/pack.c index 6a700e29f66..310f00fa377 100644 --- a/src/pack.c +++ b/src/pack.c @@ -8,7 +8,7 @@ #include "common.h" #include "odb.h" #include "pack.h" -#include "delta-apply.h" +#include "delta.h" #include "sha1_lookup.h" #include "mwindow.h" #include "fileops.h" @@ -505,7 +505,7 @@ int git_packfile_resolve_header( git_mwindow_close(&w_curs); if ((error = git_packfile_stream_open(&stream, p, curpos)) < 0) return error; - error = git__delta_read_header_fromstream(&base_size, size_p, &stream); + error = git_delta_read_header_fromstream(&base_size, size_p, &stream); git_packfile_stream_free(&stream); if (error < 0) return error; @@ -730,8 +730,9 @@ int git_packfile_unpack( obj->len = 0; obj->type = GIT_OBJ_BAD; - error = git__delta_apply(obj, base.data, base.len, delta.data, delta.len); + error = git_delta_apply(&obj->data, &obj->len, base.data, base.len, delta.data, delta.len); obj->type = base_type; + /* * We usually don't want to free the base at this * point, as we put it into the cache in the previous From 1cd6599142ec89f9c7eeb8b302e8877c71e1ab4b Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 17 Jun 2015 07:31:47 -0700 Subject: [PATCH 05/54] delta: refactor git_delta functions for consistency Refactor the git_delta functions to have consistent naming and parameters with the rest of the library. --- src/delta.c | 135 ++++++++++++++++++++++++--------------------- src/delta.h | 94 ++++++++++++------------------- src/diff_patch.c | 20 ++++--- src/pack-objects.c | 33 ++++++----- 4 files changed, 138 insertions(+), 144 deletions(-) diff --git a/src/delta.c b/src/delta.c index 8a4c2a10458..dc45697b6e3 100644 --- a/src/delta.c +++ b/src/delta.c @@ -114,7 +114,7 @@ struct index_entry { struct git_delta_index { unsigned long memsize; const void *src_buf; - unsigned long src_size; + size_t src_size; unsigned int hash_mask; struct index_entry *hash[GIT_FLEX_ARRAY]; }; @@ -142,8 +142,8 @@ static int lookup_index_alloc( return 0; } -struct git_delta_index * -git_delta_create_index(const void *buf, unsigned long bufsize) +int git_delta_index_init( + git_delta_index **out, const void *buf, size_t bufsize) { unsigned int i, hsize, hmask, entries, prev_val, *hash_count; const unsigned char *data, *buffer = buf; @@ -152,8 +152,10 @@ git_delta_create_index(const void *buf, unsigned long bufsize) void *mem; unsigned long memsize; + *out = NULL; + if (!buf || !bufsize) - return NULL; + return 0; /* Determine index hash size. Note that indexing skips the first byte to allow for optimizing the rabin polynomial @@ -172,7 +174,7 @@ git_delta_create_index(const void *buf, unsigned long bufsize) hmask = hsize - 1; if (lookup_index_alloc(&mem, &memsize, entries, hsize) < 0) - return NULL; + return -1; index = mem; mem = index->hash; @@ -190,7 +192,7 @@ git_delta_create_index(const void *buf, unsigned long bufsize) hash_count = git__calloc(hsize, sizeof(*hash_count)); if (!hash_count) { git__free(index); - return NULL; + return -1; } /* then populate the index */ @@ -243,20 +245,20 @@ git_delta_create_index(const void *buf, unsigned long bufsize) } git__free(hash_count); - return index; + *out = index; + return 0; } -void git_delta_free_index(struct git_delta_index *index) +void git_delta_index_free(git_delta_index *index) { git__free(index); } -unsigned long git_delta_sizeof_index(struct git_delta_index *index) +size_t git_delta_index_size(git_delta_index *index) { - if (index) - return index->memsize; - else - return 0; + assert(index); + + return index->memsize; } /* @@ -265,55 +267,57 @@ unsigned long git_delta_sizeof_index(struct git_delta_index *index) */ #define MAX_OP_SIZE (5 + 5 + 1 + RABIN_WINDOW + 7) -void * -git_delta_create( +int git_delta_create_from_index( + void **out, + size_t *out_len, const struct git_delta_index *index, const void *trg_buf, - unsigned long trg_size, - unsigned long *delta_size, - unsigned long max_size) + size_t trg_size, + size_t max_size) { - unsigned int i, outpos, outsize, moff, msize, val; + unsigned int i, bufpos, bufsize, moff, msize, val; int inscnt; const unsigned char *ref_data, *ref_top, *data, *top; - unsigned char *out; + unsigned char *buf; + + *out = NULL; + *out_len = 0; if (!trg_buf || !trg_size) - return NULL; + return 0; - 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; + bufpos = 0; + bufsize = 8192; + if (max_size && bufsize >= max_size) + bufsize = (unsigned int)(max_size + MAX_OP_SIZE + 1); + buf = git__malloc(bufsize); + GITERR_CHECK_ALLOC(buf); /* store reference buffer size */ i = index->src_size; while (i >= 0x80) { - out[outpos++] = i | 0x80; + buf[bufpos++] = i | 0x80; i >>= 7; } - out[outpos++] = i; + buf[bufpos++] = i; /* store target buffer size */ i = trg_size; while (i >= 0x80) { - out[outpos++] = i | 0x80; + buf[bufpos++] = i | 0x80; i >>= 7; } - out[outpos++] = i; + buf[bufpos++] = 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++; + bufpos++; val = 0; for (i = 0; i < RABIN_WINDOW && data < top; i++, data++) { - out[outpos++] = *data; + buf[bufpos++] = *data; val = ((val << 8) | *data) ^ T[val >> RABIN_SHIFT]; } inscnt = i; @@ -350,11 +354,11 @@ git_delta_create( if (msize < 4) { if (!inscnt) - outpos++; - out[outpos++] = *data++; + bufpos++; + buf[bufpos++] = *data++; inscnt++; if (inscnt == 0x7f) { - out[outpos - inscnt - 1] = inscnt; + buf[bufpos - inscnt - 1] = inscnt; inscnt = 0; } msize = 0; @@ -368,14 +372,14 @@ git_delta_create( msize++; moff--; data--; - outpos--; + bufpos--; if (--inscnt) continue; - outpos--; /* remove count slot */ + bufpos--; /* remove count slot */ inscnt--; /* make it -1 */ break; } - out[outpos - inscnt - 1] = inscnt; + buf[bufpos - inscnt - 1] = inscnt; inscnt = 0; } @@ -383,22 +387,22 @@ git_delta_create( left = (msize < 0x10000) ? 0 : (msize - 0x10000); msize -= left; - op = out + outpos++; + op = buf + bufpos++; i = 0x80; if (moff & 0x000000ff) - out[outpos++] = moff >> 0, i |= 0x01; + buf[bufpos++] = moff >> 0, i |= 0x01; if (moff & 0x0000ff00) - out[outpos++] = moff >> 8, i |= 0x02; + buf[bufpos++] = moff >> 8, i |= 0x02; if (moff & 0x00ff0000) - out[outpos++] = moff >> 16, i |= 0x04; + buf[bufpos++] = moff >> 16, i |= 0x04; if (moff & 0xff000000) - out[outpos++] = moff >> 24, i |= 0x08; + buf[bufpos++] = moff >> 24, i |= 0x08; if (msize & 0x00ff) - out[outpos++] = msize >> 0, i |= 0x10; + buf[bufpos++] = msize >> 0, i |= 0x10; if (msize & 0xff00) - out[outpos++] = msize >> 8, i |= 0x20; + buf[bufpos++] = msize >> 8, i |= 0x20; *op = i; @@ -415,31 +419,33 @@ git_delta_create( } } - 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) + if (bufpos >= bufsize - MAX_OP_SIZE) { + void *tmp = buf; + bufsize = bufsize * 3 / 2; + if (max_size && bufsize >= max_size) + bufsize = max_size + MAX_OP_SIZE + 1; + if (max_size && bufpos > max_size) break; - out = git__realloc(out, outsize); - if (!out) { + buf = git__realloc(buf, bufsize); + if (!buf) { git__free(tmp); - return NULL; + return -1; } } } if (inscnt) - out[outpos - inscnt - 1] = inscnt; + buf[bufpos - inscnt - 1] = inscnt; - if (max_size && outpos > max_size) { - git__free(out); - return NULL; + if (max_size && bufpos > max_size) { + giterr_set(GITERR_NOMEMORY, "delta would be larger than maximum size"); + git__free(buf); + return GIT_EBUFS; } - *delta_size = outpos; - return out; + *out_len = bufpos; + *out = buf; + return 0; } /* @@ -459,8 +465,11 @@ static int hdr_sz( unsigned int c, shift = 0; do { - if (d == end) + if (d == end) { + giterr_set(GITERR_INVALID, "truncated delta"); return -1; + } + c = *d++; r |= (c & 0x7f) << shift; shift += 7; diff --git a/src/delta.h b/src/delta.h index d9d1d0fa8a2..cc937292236 100644 --- a/src/delta.h +++ b/src/delta.h @@ -8,11 +8,10 @@ #include "common.h" #include "pack.h" -/* opaque object for delta index */ -struct git_delta_index; +typedef struct git_delta_index git_delta_index; /* - * create_delta_index: compute index data from given buffer + * git_delta_index_init: 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 @@ -20,22 +19,18 @@ struct git_delta_index; * 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); +extern int git_delta_index_init( + git_delta_index **out, const void *buf, size_t bufsize); /* - * free_delta_index: free the index created by create_delta_index() - * - * Given pointer must be what create_delta_index() returned, or NULL. + * Free the index created by git_delta_index_init() */ -extern void git_delta_free_index(struct git_delta_index *index); +extern void git_delta_index_free(git_delta_index *index); /* - * sizeof_delta_index: returns memory usage of delta index - * - * Given pointer must be what create_delta_index() returned, or NULL. + * Returns memory usage of delta index. */ -extern unsigned long git_delta_sizeof_index(struct git_delta_index *index); +extern size_t git_delta_index_size(git_delta_index *index); /* * create_delta: create a delta from given index for the given buffer @@ -47,71 +42,50 @@ extern unsigned long git_delta_sizeof_index(struct git_delta_index *index); * returned and *delta_size is updated with its size. The returned buffer * must be freed by the caller. */ -extern void *git_delta_create( +extern int git_delta_create_from_index( + void **out, + size_t *out_size, const struct git_delta_index *index, const void *buf, - unsigned long bufsize, - unsigned long *delta_size, - unsigned long max_delta_size); + size_t bufsize, + size_t 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 + * than max_delta_size then GIT_EBUFS 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) +GIT_INLINE(int) git_delta( + void **out, size_t *out_len, + const void *src_buf, size_t src_bufsize, + const void *trg_buf, size_t trg_bufsize, + size_t max_delta_size) { - struct git_delta_index *index = git_delta_create_index(src_buf, src_bufsize); + git_delta_index *index; + int error = 0; + + *out = NULL; + *out_len = 0; + + if ((error = git_delta_index_init(&index, src_buf, src_bufsize)) < 0) + return error; + if (index) { - void *delta = git_delta_create( - index, trg_buf, trg_bufsize, delta_size, max_delta_size); - git_delta_free_index(index); - return delta; + error = git_delta_create_from_index(out, out_len, + index, trg_buf, trg_bufsize, max_delta_size); + + git_delta_index_free(index); } - 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); + return error; +} /* 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; -} - /** * Apply a git binary delta to recover the original content. * The caller is responsible for freeing the returned buffer. diff --git a/src/diff_patch.c b/src/diff_patch.c index 50faa3b3f7a..20a84388f29 100644 --- a/src/diff_patch.c +++ b/src/diff_patch.c @@ -270,20 +270,24 @@ static int create_binary( } if (a_datalen && b_datalen) { - void *delta_data = git_delta( - a_data, (unsigned long)a_datalen, - b_data, (unsigned long)b_datalen, - &delta_data_len, (unsigned long)deflate.size); + void *delta_data; - if (delta_data) { + error = git_delta(&delta_data, &delta_data_len, + a_data, a_datalen, + b_data, b_datalen, + deflate.size); + + if (error == 0) { error = git_zstream_deflatebuf( &delta, delta_data, (size_t)delta_data_len); git__free(delta_data); - - if (error < 0) - goto done; + } else if (error == GIT_EBUFS) { + error = 0; } + + if (error < 0) + goto done; } if (delta.size && delta.size < deflate.size) { diff --git a/src/pack-objects.c b/src/pack-objects.c index 11e13f7d45d..6f86deb07b1 100644 --- a/src/pack-objects.c +++ b/src/pack-objects.c @@ -274,6 +274,7 @@ 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; + int error; *out = NULL; @@ -281,12 +282,15 @@ static int get_delta(void **out, git_odb *odb, git_pobject *po) 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); + error = git_delta(&delta_buf, &delta_size, + git_odb_object_data(src), git_odb_object_size(src), + git_odb_object_data(trg), git_odb_object_size(trg), + 0); + + if (error < 0 && error != GIT_EBUFS) + goto on_error; - if (!delta_buf || delta_size != po->delta_size) { + if (error == GIT_EBUFS || delta_size != po->delta_size) { giterr_set(GITERR_INVALID, "Delta size changed"); goto on_error; } @@ -815,16 +819,14 @@ static int try_delta(git_packbuilder *pb, struct unpacked *trg, *mem_usage += sz; } if (!src->index) { - src->index = git_delta_create_index(src->data, src_size); - if (!src->index) + if (git_delta_index_init(&src->index, src->data, src_size) < 0) return 0; /* suboptimal pack - out of memory */ - *mem_usage += git_delta_sizeof_index(src->index); + *mem_usage += git_delta_index_size(src->index); } - delta_buf = git_delta_create(src->index, trg->data, trg_size, - &delta_size, max_size); - if (!delta_buf) + if (git_delta_create_from_index(&delta_buf, &delta_size, src->index, trg->data, trg_size, + max_size) < 0) return 0; if (trg_object->delta) { @@ -885,9 +887,14 @@ static unsigned int check_delta_limit(git_pobject *me, unsigned int n) static unsigned long free_unpacked(struct unpacked *n) { - unsigned long freed_mem = git_delta_sizeof_index(n->index); - git_delta_free_index(n->index); + unsigned long freed_mem = 0; + + if (n->index) { + freed_mem += git_delta_index_size(n->index); + git_delta_index_free(n->index); + } n->index = NULL; + if (n->data) { freed_mem += (unsigned long)n->object->size; git__free(n->data); From b88f1713d01e5cca5a296d564ae094dd8bc6a1f2 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 17 Jun 2015 08:07:34 -0700 Subject: [PATCH 06/54] zstream: offer inflating, `git_zstream_inflatebuf` Introduce `git_zstream_inflatebuf` for simple uses. --- src/pack-objects.c | 2 +- src/zstream.c | 38 +++++++++++++++++++++++++++++++------- src/zstream.h | 9 ++++++++- tests/core/zstream.c | 16 +++++++++++----- 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/pack-objects.c b/src/pack-objects.c index 6f86deb07b1..ec15970f301 100644 --- a/src/pack-objects.c +++ b/src/pack-objects.c @@ -144,7 +144,7 @@ int git_packbuilder_new(git_packbuilder **out, git_repository *repo) pb->nr_threads = 1; /* do not spawn any thread by default */ if (git_hash_ctx_init(&pb->ctx) < 0 || - git_zstream_init(&pb->zstream) < 0 || + git_zstream_init(&pb->zstream, GIT_ZSTREAM_DEFLATE) < 0 || git_repository_odb(&pb->odb, repo) < 0 || packbuilder_config(pb) < 0) goto on_error; diff --git a/src/zstream.c b/src/zstream.c index 2130bc3ca7c..6533449e83a 100644 --- a/src/zstream.c +++ b/src/zstream.c @@ -28,20 +28,31 @@ static int zstream_seterr(git_zstream *zs) return -1; } -int git_zstream_init(git_zstream *zstream) +int git_zstream_init(git_zstream *zstream, git_zstream_t type) { - zstream->zerr = deflateInit(&zstream->z, Z_DEFAULT_COMPRESSION); + zstream->type = type; + + if (zstream->type == GIT_ZSTREAM_INFLATE) + zstream->zerr = inflateInit(&zstream->z); + else + zstream->zerr = deflateInit(&zstream->z, Z_DEFAULT_COMPRESSION); return zstream_seterr(zstream); } void git_zstream_free(git_zstream *zstream) { - deflateEnd(&zstream->z); + if (zstream->type == GIT_ZSTREAM_INFLATE) + inflateEnd(&zstream->z); + else + deflateEnd(&zstream->z); } void git_zstream_reset(git_zstream *zstream) { - deflateReset(&zstream->z); + if (zstream->type == GIT_ZSTREAM_INFLATE) + inflateReset(&zstream->z); + else + deflateReset(&zstream->z); zstream->in = NULL; zstream->in_len = 0; zstream->zerr = Z_STREAM_END; @@ -97,7 +108,10 @@ int git_zstream_get_output(void *out, size_t *out_len, git_zstream *zstream) out_queued = (size_t)zstream->z.avail_out; /* compress next chunk */ - zstream->zerr = deflate(&zstream->z, zflush); + if (zstream->type == GIT_ZSTREAM_INFLATE) + zstream->zerr = inflate(&zstream->z, zflush); + else + zstream->zerr = deflate(&zstream->z, zflush); if (zstream->zerr == Z_STREAM_ERROR) return zstream_seterr(zstream); @@ -120,12 +134,12 @@ int git_zstream_get_output(void *out, size_t *out_len, git_zstream *zstream) return 0; } -int git_zstream_deflatebuf(git_buf *out, const void *in, size_t in_len) +static int zstream_buf(git_buf *out, const void *in, size_t in_len, git_zstream_t type) { git_zstream zs = GIT_ZSTREAM_INIT; int error = 0; - if ((error = git_zstream_init(&zs)) < 0) + if ((error = git_zstream_init(&zs, type)) < 0) return error; if ((error = git_zstream_set_input(&zs, in, in_len)) < 0) @@ -154,3 +168,13 @@ int git_zstream_deflatebuf(git_buf *out, const void *in, size_t in_len) git_zstream_free(&zs); return error; } + +int git_zstream_deflatebuf(git_buf *out, const void *in, size_t in_len) +{ + return zstream_buf(out, in, in_len, GIT_ZSTREAM_DEFLATE); +} + +int git_zstream_inflatebuf(git_buf *out, const void *in, size_t in_len) +{ + return zstream_buf(out, in, in_len, GIT_ZSTREAM_INFLATE); +} diff --git a/src/zstream.h b/src/zstream.h index 9b5bf6acec7..f0006d32e36 100644 --- a/src/zstream.h +++ b/src/zstream.h @@ -12,8 +12,14 @@ #include "common.h" #include "buffer.h" +typedef enum { + GIT_ZSTREAM_INFLATE, + GIT_ZSTREAM_DEFLATE, +} git_zstream_t; + typedef struct { z_stream z; + git_zstream_t type; const char *in; size_t in_len; int zerr; @@ -21,7 +27,7 @@ typedef struct { #define GIT_ZSTREAM_INIT {{0}} -int git_zstream_init(git_zstream *zstream); +int git_zstream_init(git_zstream *zstream, git_zstream_t type); void git_zstream_free(git_zstream *zstream); int git_zstream_set_input(git_zstream *zstream, const void *in, size_t in_len); @@ -35,5 +41,6 @@ bool git_zstream_done(git_zstream *zstream); void git_zstream_reset(git_zstream *zstream); int git_zstream_deflatebuf(git_buf *out, const void *in, size_t in_len); +int git_zstream_inflatebuf(git_buf *out, const void *in, size_t in_len); #endif /* INCLUDE_zstream_h__ */ diff --git a/tests/core/zstream.c b/tests/core/zstream.c index 7ba9424ba37..b13429b0a8b 100644 --- a/tests/core/zstream.c +++ b/tests/core/zstream.c @@ -48,7 +48,7 @@ void test_core_zstream__basic(void) char out[128]; size_t outlen = sizeof(out); - cl_git_pass(git_zstream_init(&z)); + cl_git_pass(git_zstream_init(&z, GIT_ZSTREAM_DEFLATE)); cl_git_pass(git_zstream_set_input(&z, data, strlen(data) + 1)); cl_git_pass(git_zstream_get_output(out, &outlen, &z)); cl_assert(git_zstream_done(&z)); @@ -68,9 +68,10 @@ void test_core_zstream__buffer(void) #define BIG_STRING_PART "Big Data IS Big - Long Data IS Long - We need a buffer larger than 1024 x 1024 to make sure we trigger chunked compression - Big Big Data IS Bigger than Big - Long Long Data IS Longer than Long" -static void compress_input_various_ways(git_buf *input) +static void compress_and_decompress_input_various_ways(git_buf *input) { git_buf out1 = GIT_BUF_INIT, out2 = GIT_BUF_INIT; + git_buf inflated = GIT_BUF_INIT; size_t i, fixed_size = max(input->size / 2, 256); char *fixed = git__malloc(fixed_size); cl_assert(fixed); @@ -93,7 +94,7 @@ static void compress_input_various_ways(git_buf *input) } cl_assert(use_fixed_size <= fixed_size); - cl_git_pass(git_zstream_init(&zs)); + cl_git_pass(git_zstream_init(&zs, GIT_ZSTREAM_DEFLATE)); cl_git_pass(git_zstream_set_input(&zs, input->ptr, input->size)); while (!git_zstream_done(&zs)) { @@ -112,7 +113,12 @@ static void compress_input_various_ways(git_buf *input) git_buf_free(&out2); } + cl_git_pass(git_zstream_inflatebuf(&inflated, out1.ptr, out1.size)); + cl_assert_equal_i(input->size, inflated.size); + cl_assert(memcmp(input->ptr, inflated.ptr, inflated.size) == 0); + git_buf_free(&out1); + git_buf_free(&inflated); git__free(fixed); } @@ -129,14 +135,14 @@ void test_core_zstream__big_data(void) cl_git_pass( git_buf_put(&in, BIG_STRING_PART, strlen(BIG_STRING_PART))); - compress_input_various_ways(&in); + compress_and_decompress_input_various_ways(&in); /* make a big string that's hard to compress */ srand(0xabad1dea); for (scan = 0; scan < in.size; ++scan) in.ptr[scan] = (char)rand(); - compress_input_various_ways(&in); + compress_and_decompress_input_various_ways(&in); } git_buf_free(&in); From 3149ff6f663bf234679e02976d160917a6c70261 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 17 Jun 2015 18:13:10 -0700 Subject: [PATCH 07/54] patch application: apply binary patches Handle the application of binary patches. Include tests that produce a binary patch (an in-memory `git_patch` object), then enusre that the patch applies correctly. --- src/apply.c | 95 +++++++++++++++++++++++++-- src/diff_print.c | 3 + tests/apply/apply_common.h | 99 +++++++++++++++++++++++++++++ tests/apply/fromdiff.c | 127 +++++++++++++++++++++++++++++++++---- tests/apply/fromfile.c | 27 ++++++++ 5 files changed, 336 insertions(+), 15 deletions(-) diff --git a/src/apply.c b/src/apply.c index f1bd9f4b575..c667c6308de 100644 --- a/src/apply.c +++ b/src/apply.c @@ -13,6 +13,8 @@ #include "diff_patch.h" #include "fileops.h" #include "apply.h" +#include "delta.h" +#include "zstream.h" #define apply_err(...) \ ( giterr_set(GITERR_PATCH, __VA_ARGS__), -1 ) @@ -239,6 +241,87 @@ static int apply_hunks( return error; } +static int apply_binary_delta( + git_buf *out, + const char *source, + size_t source_len, + git_diff_binary_file *binary_file) +{ + git_buf inflated = GIT_BUF_INIT; + int error = 0; + + /* no diff means identical contents */ + if (binary_file->datalen == 0) + return git_buf_put(out, source, source_len); + + error = git_zstream_inflatebuf(&inflated, + binary_file->data, binary_file->datalen); + + if (!error && inflated.size != binary_file->inflatedlen) { + error = apply_err("inflated delta does not match expected length"); + git_buf_free(out); + } + + if (error < 0) + goto done; + + if (binary_file->type == GIT_DIFF_BINARY_DELTA) { + void *data; + size_t data_len; + + error = git_delta_apply(&data, &data_len, (void *)source, source_len, + (void *)inflated.ptr, inflated.size); + + out->ptr = data; + out->size = data_len; + out->asize = data_len; + } + else if (binary_file->type == GIT_DIFF_BINARY_LITERAL) { + git_buf_swap(out, &inflated); + } + else { + error = apply_err("unknown binary delta type"); + goto done; + } + +done: + git_buf_free(&inflated); + return error; +} + +static int apply_binary( + git_buf *out, + const char *source, + size_t source_len, + git_patch *patch) +{ + git_buf reverse = GIT_BUF_INIT; + int error; + + /* first, apply the new_file delta to the given source */ + if ((error = apply_binary_delta(out, source, source_len, + &patch->binary.new_file)) < 0) + goto done; + + /* second, apply the old_file delta to sanity check the result */ + if ((error = apply_binary_delta(&reverse, out->ptr, out->size, + &patch->binary.old_file)) < 0) + goto done; + + if (source_len != reverse.size || + memcmp(source, reverse.ptr, source_len) != 0) { + error = apply_err("binary patch did not apply cleanly"); + goto done; + } + +done: + if (error < 0) + git_buf_free(out); + + git_buf_free(&reverse); + return error; +} + int git_apply__patch( git_buf *contents_out, char **filename_out, @@ -262,10 +345,14 @@ int git_apply__patch( patch->nfile.file->mode : GIT_FILEMODE_BLOB; } - /* If the patch is empty, simply keep the source unchanged */ - if (patch->hunks.size == 0) - git_buf_put(contents_out, source, source_len); - else if ((error = apply_hunks(contents_out, source, source_len, patch)) < 0) + if (patch->delta->flags & GIT_DIFF_FLAG_BINARY) + error = apply_binary(contents_out, source, source_len, patch); + else if (patch->hunks.size) + error = apply_hunks(contents_out, source, source_len, patch); + else + error = git_buf_put(contents_out, source, source_len); + + if (error) goto done; if (patch->delta->status == GIT_DELTA_DELETED && diff --git a/src/diff_print.c b/src/diff_print.c index dae9e341d36..d1834846254 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -415,6 +415,9 @@ static int diff_print_patch_file_binary( (error = diff_print_load_content(pi, delta)) < 0) return error; + if (binary->new_file.datalen == 0 && binary->old_file.datalen == 0) + return 0; + pre_binary_size = pi->buf->size; git_buf_printf(pi->buf, "GIT binary patch\n"); pi->line.num_lines++; diff --git a/tests/apply/apply_common.h b/tests/apply/apply_common.h index 595b96e3127..f4cb2ff84ef 100644 --- a/tests/apply/apply_common.h +++ b/tests/apply/apply_common.h @@ -473,3 +473,102 @@ "+patch file\n" \ "-it's something else\n" \ " entirely!" + +/* binary contents */ + +#define FILE_BINARY_LITERAL_ORIGINAL "\x00\x00\x0a" +#define FILE_BINARY_LITERAL_ORIGINAL_LEN 3 + +#define FILE_BINARY_LITERAL_MODIFIED "\x00\x00\x01\x02\x0a" +#define FILE_BINARY_LITERAL_MODIFIED_LEN 5 + +#define PATCH_BINARY_LITERAL \ + "diff --git a/binary.bin b/binary.bin\n" \ + "index bd474b2519cc15eab801ff851cc7d50f0dee49a1..9ac35ff15cd8864aeafd889e4826a3150f0b06c4 100644\n" \ + "GIT binary patch\n" \ + "literal 5\n" \ + "Mc${NkU}WL~000&M4gdfE\n" \ + "\n" \ + "literal 3\n" \ + "Kc${Nk-~s>u4FC%O\n\n" + +#define FILE_BINARY_DELTA_ORIGINAL \ + "\x00\x00\x01\x02\x00\x00\x01\x02\x00\x00\x01\x02\x0a\x54\x68\x69" \ + "\x73\x20\x69\x73\x20\x61\x20\x62\x69\x6e\x61\x72\x79\x20\x66\x69" \ + "\x6c\x65\x2c\x20\x62\x79\x20\x76\x69\x72\x74\x75\x65\x20\x6f\x66" \ + "\x20\x68\x61\x76\x69\x6e\x67\x20\x73\x6f\x6d\x65\x20\x6e\x75\x6c" \ + "\x6c\x73\x2e\x0a\x00\x00\x01\x02\x00\x00\x01\x02\x00\x00\x01\x02" \ + "\x0a\x57\x65\x27\x72\x65\x20\x67\x6f\x69\x6e\x67\x20\x74\x6f\x20" \ + "\x63\x68\x61\x6e\x67\x65\x20\x70\x6f\x72\x74\x69\x6f\x6e\x73\x20" \ + "\x6f\x66\x20\x69\x74\x2e\x0a\x00\x00\x01\x02\x00\x00\x01\x02\x00" \ + "\x00\x01\x02\x0a\x53\x6f\x20\x74\x68\x61\x74\x20\x77\x65\x20\x67" \ + "\x69\x74\x20\x61\x20\x62\x69\x6e\x61\x72\x79\x20\x64\x65\x6c\x74" \ + "\x61\x20\x69\x6e\x73\x74\x65\x61\x64\x20\x6f\x66\x20\x74\x68\x65" \ + "\x20\x64\x65\x66\x6c\x61\x74\x65\x64\x20\x63\x6f\x6e\x74\x65\x6e" \ + "\x74\x73\x2e\x0a\x00\x00\x01\x02\x00\x00\x01\x02\x00\x00\x01\x02" \ + "\x0a" +#define FILE_BINARY_DELTA_ORIGINAL_LEN 209 + +#define FILE_BINARY_DELTA_MODIFIED \ + "\x00\x00\x01\x02\x00\x00\x01\x02\x00\x00\x01\x02\x0a\x5a\x5a\x5a" \ + "\x5a\x20\x69\x73\x20\x61\x20\x62\x69\x6e\x61\x72\x79\x20\x66\x69" \ + "\x6c\x65\x2c\x20\x62\x79\x20\x76\x69\x72\x74\x75\x65\x20\x6f\x66" \ + "\x20\x68\x61\x76\x69\x6e\x67\x20\x73\x6f\x6d\x65\x20\x6e\x75\x6c" \ + "\x6c\x73\x2e\x0a\x00\x00\x01\x02\x00\x00\x01\x02\x00\x00\x01\x02" \ + "\x0a\x57\x65\x27\x72\x65\x20\x67\x6f\x69\x6e\x67\x20\x74\x6f\x20" \ + "\x63\x68\x61\x6e\x67\x65\x20\x70\x6f\x72\x74\x69\x6f\x6e\x73\x20" \ + "\x6f\x66\x20\x49\x54\x2e\x0a\x00\x00\x01\x02\x00\x00\x01\x02\x00" \ + "\x00\x01\x02\x0a\x53\x4f\x20\x74\x68\x61\x74\x20\x77\x65\x20\x67" \ + "\x69\x74\x20\x61\x20\x62\x69\x6e\x61\x72\x79\x20\x64\x65\x6c\x74" \ + "\x61\x20\x69\x6e\x73\x74\x65\x61\x64\x20\x6f\x66\x20\x74\x68\x65" \ + "\x20\x64\x65\x66\x6c\x61\x74\x65\x64\x20\x63\x6f\x6e\x74\x65\x6e" \ + "\x74\x73\x2e\x0a\x00\x00\x01\x02\x00\x00\x01\x02\x00\x00\x01\x02" \ + "\x0a" +#define FILE_BINARY_DELTA_MODIFIED_LEN 209 + +#define PATCH_BINARY_DELTA \ + "diff --git a/binary.bin b/binary.bin\n" \ + "index 27184d9883b12c4c9c54b4a31137603586169f51..7c94f9e60bf366033d98e0d551ae37d30faef74a 100644\n" \ + "GIT binary patch\n" \ + "delta 48\n" \ + "kc$~Y)c#%<%fq{_;hPk4EV4`4>uxE%K7m7r%|HL+L0In7XGynhq\n" \ + "\n" \ + "delta 48\n" \ + "mc$~Y)c#%<%fq{_;hPgsAGK(h)CJASj=y9P)1m{m|^9BI99|yz$\n\n" + +#define PATCH_BINARY_ADD \ + "diff --git a/binary.bin b/binary.bin\n" \ + "new file mode 100644\n" \ + "index 0000000000000000000000000000000000000000..7c94f9e60bf366033d98e0d551ae37d30faef74a\n" \ + "GIT binary patch\n" \ + "literal 209\n" \ + "zc${60u?oUK5JXSQe8qG&;(u6KCC_\n" \ + "zAhe=XX7rNzh<3&##YcwqNHmEKsP<&&m~%Zf;eX@Khr$?aExDmfqyyt+#l^I)3+LMg\n" \ + "kxnAIj9Pfn_|Gh`fP7tlm6j#y{FJYg_IifRlR^R@A08f862mk;8\n" \ + "\n" \ + "literal 0\n" \ + "Hc$@C_\n" \ + "zAhe=XX7rNzh<3&##YcwqNHmEKsP<&&m~%Zf;eX@Khr$?aExDmfqyyt+#l^I)3+LMg\n" \ + "kxnAIj9Pfn_|Gh`fP7tlm6j#y{FJYg_IifRlR^R@A08f862mk;8\n\n" + +/* contains an old side that does not match the expected source */ +#define PATCH_BINARY_NOT_REVERSIBLE \ + "diff --git a/binary.bin b/binary.bin\n" \ + "index 27184d9883b12c4c9c54b4a31137603586169f51..7c94f9e60bf366033d98e0d551ae37d30faef74a 100644\n" \ + "GIT binary patch\n" \ + "literal 5\n" \ + "Mc${NkU}WL~000&M4gdfE\n" \ + "\n" \ + "delta 48\n" \ + "mc$~Y)c#%<%fq{_;hPgsAGK(h)CJASj=y9P)1m{m|^9BI99|yz$\n\n" diff --git a/tests/apply/fromdiff.c b/tests/apply/fromdiff.c index af0541de8cb..ae37d0719e1 100644 --- a/tests/apply/fromdiff.c +++ b/tests/apply/fromdiff.c @@ -8,10 +8,13 @@ #include "apply_common.h" static git_repository *repo = NULL; +static git_diff_options binary_opts = GIT_DIFF_OPTIONS_INIT; void test_apply_fromdiff__initialize(void) { repo = cl_git_sandbox_init("renames"); + + binary_opts.flags |= GIT_DIFF_SHOW_BINARY; } void test_apply_fromdiff__cleanup(void) @@ -19,10 +22,10 @@ void test_apply_fromdiff__cleanup(void) cl_git_sandbox_cleanup(); } -static int apply_buf( - const char *old, +static int apply_gitbuf( + const git_buf *old, const char *oldname, - const char *new, + const git_buf *new, const char *newname, const char *patch_expected, const git_diff_options *diff_opts) @@ -35,22 +38,27 @@ static int apply_buf( int error; cl_git_pass(git_patch_from_buffers(&patch, - old, old ? strlen(old) : 0, oldname, - new, new ? strlen(new) : 0, newname, + old ? old->ptr : NULL, old ? old->size : 0, + oldname, + new ? new->ptr : NULL, new ? new->size : 0, + newname, diff_opts)); - cl_git_pass(git_patch_to_buf(&patchbuf, patch)); - cl_assert_equal_s(patch_expected, patchbuf.ptr); + if (patch_expected) { + cl_git_pass(git_patch_to_buf(&patchbuf, patch)); + cl_assert_equal_s(patch_expected, patchbuf.ptr); + } - error = git_apply__patch(&result, &filename, &mode, old, old ? strlen(old) : 0, patch); + error = git_apply__patch(&result, &filename, &mode, old ? old->ptr : NULL, old ? old->size : 0, patch); if (error == 0 && new == NULL) { cl_assert_equal_i(0, result.size); cl_assert_equal_p(NULL, filename); cl_assert_equal_i(0, mode); - } else { - cl_assert_equal_s(new, result.ptr); - cl_assert_equal_s("file.txt", filename); + } + else if (error == 0) { + cl_assert_equal_s(new->ptr, result.ptr); + cl_assert_equal_s(newname ? newname : oldname, filename); cl_assert_equal_i(0100644, mode); } @@ -62,6 +70,32 @@ static int apply_buf( return error; } +static int apply_buf( + const char *old, + const char *oldname, + const char *new, + const char *newname, + const char *patch_expected, + const git_diff_options *diff_opts) +{ + git_buf o = GIT_BUF_INIT, n = GIT_BUF_INIT, + *optr = NULL, *nptr = NULL; + + if (old) { + o.ptr = (char *)old; + o.size = strlen(old); + optr = &o; + } + + if (new) { + n.ptr = (char *)new; + n.size = strlen(new); + nptr = &n; + } + + return apply_gitbuf(optr, oldname, nptr, newname, patch_expected, diff_opts); +} + void test_apply_fromdiff__change_middle(void) { cl_git_pass(apply_buf( @@ -182,3 +216,74 @@ void test_apply_fromdiff__no_change(void) FILE_ORIGINAL, "file.txt", "", NULL)); } + +void test_apply_fromdiff__binary_add(void) +{ + git_buf newfile = GIT_BUF_INIT; + + newfile.ptr = FILE_BINARY_DELTA_MODIFIED; + newfile.size = FILE_BINARY_DELTA_MODIFIED_LEN; + + cl_git_pass(apply_gitbuf( + NULL, NULL, + &newfile, "binary.bin", + NULL, &binary_opts)); +} + +void test_apply_fromdiff__binary_no_change(void) +{ + git_buf original = GIT_BUF_INIT; + + original.ptr = FILE_BINARY_DELTA_ORIGINAL; + original.size = FILE_BINARY_DELTA_ORIGINAL_LEN; + + cl_git_pass(apply_gitbuf( + &original, "binary.bin", + &original, "binary.bin", + "", &binary_opts)); +} + +void test_apply_fromdiff__binary_change_delta(void) +{ + git_buf original = GIT_BUF_INIT, modified = GIT_BUF_INIT; + + original.ptr = FILE_BINARY_DELTA_ORIGINAL; + original.size = FILE_BINARY_DELTA_ORIGINAL_LEN; + + modified.ptr = FILE_BINARY_DELTA_MODIFIED; + modified.size = FILE_BINARY_DELTA_MODIFIED_LEN; + + cl_git_pass(apply_gitbuf( + &original, "binary.bin", + &modified, "binary.bin", + NULL, &binary_opts)); +} + +void test_apply_fromdiff__binary_change_literal(void) +{ + git_buf original = GIT_BUF_INIT, modified = GIT_BUF_INIT; + + original.ptr = FILE_BINARY_LITERAL_ORIGINAL; + original.size = FILE_BINARY_LITERAL_ORIGINAL_LEN; + + modified.ptr = FILE_BINARY_LITERAL_MODIFIED; + modified.size = FILE_BINARY_LITERAL_MODIFIED_LEN; + + cl_git_pass(apply_gitbuf( + &original, "binary.bin", + &modified, "binary.bin", + NULL, &binary_opts)); +} + +void test_apply_fromdiff__binary_delete(void) +{ + git_buf original = GIT_BUF_INIT; + + original.ptr = FILE_BINARY_DELTA_MODIFIED; + original.size = FILE_BINARY_DELTA_MODIFIED_LEN; + + cl_git_pass(apply_gitbuf( + &original, "binary.bin", + NULL, NULL, + NULL, &binary_opts)); +} diff --git a/tests/apply/fromfile.c b/tests/apply/fromfile.c index cbf50d82d21..75b318523e9 100644 --- a/tests/apply/fromfile.c +++ b/tests/apply/fromfile.c @@ -299,3 +299,30 @@ void test_apply_fromfile__fail_not_a_patch(void) cl_git_fail(git_patch_from_patchfile(&patch, PATCH_NOT_A_PATCH, strlen(PATCH_NOT_A_PATCH))); } + +/* +void test_apply_fromdiff__binary_change_must_be_reversible(void) +{ + git_buf original = GIT_BUF_INIT, modified = GIT_BUF_INIT, + result = GIT_BUF_INIT; + char *filename; + unsigned int mode; + + original.ptr = FILE_BINARY_DELTA_ORIGINAL; + original.size = FILE_BINARY_DELTA_ORIGINAL_LEN; + + modified.ptr = FILE_BINARY_DELTA_MODIFIED; + modified.size = FILE_BINARY_DELTA_MODIFIED_LEN; + + cl_git_fail(git_apply__patch(&result, &filename, &mode, old ? old->ptr : NULL, old ? old->size : 0, patch); + + cl_git_fail(apply_gitbuf( + &original, "binary.bin", + &modified, "binary.bin", + PATCH_BINARY_NOT_REVERSIBLE, &binary_opts)); + cl_assert_equal_s("binary patch did not apply cleanly", giterr_last()->message); + + git_buf_free(&result); + git__free(filename); +} +*/ From 5b78dbdbf30d863760936ee6755dfd3db951c1e3 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Thu, 9 Jul 2015 13:04:10 -0500 Subject: [PATCH 08/54] git_buf: decode base85 inputs --- src/buffer.c | 131 +++++++++++++++++++++++++++++++++++++------- src/buffer.h | 2 + tests/core/buffer.c | 36 ++++++++++++ 3 files changed, 149 insertions(+), 20 deletions(-) diff --git a/src/buffer.c b/src/buffer.c index 5fafe69cb44..c2a54a5bde2 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -273,42 +273,51 @@ int git_buf_encode_base64(git_buf *buf, const char *data, size_t len) return 0; } -/* The inverse of base64_encode, offset by '+' == 43. */ +/* The inverse of base64_encode */ static const int8_t base64_decode[] = { - 62, - -1, -1, -1, - 63, - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, - -1, -1, -1, 0, -1, -1, -1, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, - 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, - -1, -1, -1, -1, -1, -1, - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 0, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }; -#define BASE64_DECODE_VALUE(c) (((c) < 43 || (c) > 122) ? -1 : base64_decode[c - 43]) - int git_buf_decode_base64(git_buf *buf, const char *base64, size_t len) { size_t i; int8_t a, b, c, d; size_t orig_size = buf->size, new_size; + if (len % 4) { + giterr_set(GITERR_INVALID, "invalid base64 input"); + return -1; + } + assert(len % 4 == 0); GITERR_CHECK_ALLOC_ADD(&new_size, (len / 4 * 3), buf->size); GITERR_CHECK_ALLOC_ADD(&new_size, new_size, 1); ENSURE_SIZE(buf, new_size); for (i = 0; i < len; i += 4) { - if ((a = BASE64_DECODE_VALUE(base64[i])) < 0 || - (b = BASE64_DECODE_VALUE(base64[i+1])) < 0 || - (c = BASE64_DECODE_VALUE(base64[i+2])) < 0 || - (d = BASE64_DECODE_VALUE(base64[i+3])) < 0) { + if ((a = base64_decode[(unsigned char)base64[i]]) < 0 || + (b = base64_decode[(unsigned char)base64[i+1]]) < 0 || + (c = base64_decode[(unsigned char)base64[i+2]]) < 0 || + (d = base64_decode[(unsigned char)base64[i+3]]) < 0) { buf->size = orig_size; buf->ptr[buf->size] = '\0'; - giterr_set(GITERR_INVALID, "Invalid base64 input"); + giterr_set(GITERR_INVALID, "invalid base64 input"); return -1; } @@ -321,7 +330,7 @@ int git_buf_decode_base64(git_buf *buf, const char *base64, size_t len) return 0; } -static const char b85str[] = +static const char base85_encode[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~"; int git_buf_encode_base85(git_buf *buf, const char *data, size_t len) @@ -351,7 +360,7 @@ int git_buf_encode_base85(git_buf *buf, const char *data, size_t len) int val = acc % 85; acc /= 85; - b85[i] = b85str[val]; + b85[i] = base85_encode[val]; } for (i = 0; i < 5; i++) @@ -363,6 +372,88 @@ int git_buf_encode_base85(git_buf *buf, const char *data, size_t len) return 0; } +/* The inverse of base85_encode */ +static const int8_t base85_decode[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 63, -1, 64, 65, 66, 67, -1, 68, 69, 70, 71, -1, 72, -1, -1, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, -1, 73, 74, 75, 76, 77, + 78, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, -1, -1, -1, 79, 80, + 81, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 82, 83, 84, 85, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 +}; + +int git_buf_decode_base85( + git_buf *buf, + const char *base85, + size_t base85_len, + size_t output_len) +{ + size_t orig_size = buf->size, new_size; + + if (base85_len % 5 || + output_len > base85_len * 4 / 5) { + giterr_set(GITERR_INVALID, "invalid base85 input"); + return -1; + } + + GITERR_CHECK_ALLOC_ADD(&new_size, output_len, buf->size); + GITERR_CHECK_ALLOC_ADD(&new_size, new_size, 1); + ENSURE_SIZE(buf, new_size); + + while (output_len) { + unsigned acc = 0; + int de, cnt = 4; + unsigned char ch; + do { + ch = *base85++; + de = base85_decode[ch]; + if (--de < 0) + goto on_error; + + acc = acc * 85 + de; + } while (--cnt); + ch = *base85++; + de = base85_decode[ch]; + if (--de < 0) + goto on_error; + + /* Detect overflow. */ + if (0xffffffff / 85 < acc || + 0xffffffff - de < (acc *= 85)) + goto on_error; + + acc += de; + + cnt = (output_len < 4) ? output_len : 4; + output_len -= cnt; + do { + acc = (acc << 8) | (acc >> 24); + buf->ptr[buf->size++] = acc; + } while (--cnt); + } + + buf->ptr[buf->size] = 0; + + return 0; + +on_error: + buf->size = orig_size; + buf->ptr[buf->size] = '\0'; + + giterr_set(GITERR_INVALID, "invalid base85 input"); + return -1; +} + int git_buf_vprintf(git_buf *buf, const char *format, va_list ap) { size_t expected_size, new_size; diff --git a/src/buffer.h b/src/buffer.h index d446e0487f6..2be299b14d1 100644 --- a/src/buffer.h +++ b/src/buffer.h @@ -185,6 +185,8 @@ int git_buf_decode_base64(git_buf *buf, const char *base64, size_t len); /* Write data as "base85" encoded in buffer */ int git_buf_encode_base85(git_buf *buf, const char *data, size_t len); +/* Decode the given "base85" and write the result to the buffer */ +int git_buf_decode_base85(git_buf *buf, const char *base64, size_t len, size_t output_len); /* * Insert, remove or replace a portion of the buffer. diff --git a/tests/core/buffer.c b/tests/core/buffer.c index 9872af7f400..1cf23426b3b 100644 --- a/tests/core/buffer.c +++ b/tests/core/buffer.c @@ -813,6 +813,42 @@ void test_core_buffer__encode_base85(void) git_buf_free(&buf); } +void test_core_buffer__decode_base85(void) +{ + git_buf buf = GIT_BUF_INIT; + + cl_git_pass(git_buf_decode_base85(&buf, "bZBXF", 5, 4)); + cl_assert_equal_sz(4, buf.size); + cl_assert_equal_s("this", buf.ptr); + git_buf_clear(&buf); + + cl_git_pass(git_buf_decode_base85(&buf, "ba!tca&BaE", 10, 8)); + cl_assert_equal_sz(8, buf.size); + cl_assert_equal_s("two rnds", buf.ptr); + git_buf_clear(&buf); + + cl_git_pass(git_buf_decode_base85(&buf, "bZBXFAZc?TVqtS-AUHK3Wo~0{WMyOk", 30, 23)); + cl_assert_equal_sz(23, buf.size); + cl_assert_equal_s("this is base 85 encoded", buf.ptr); + git_buf_clear(&buf); + + git_buf_free(&buf); +} + +void test_core_buffer__decode_base85_fails_gracefully(void) +{ + git_buf buf = GIT_BUF_INIT; + + git_buf_puts(&buf, "foobar"); + + cl_git_fail(git_buf_decode_base85(&buf, "invalid charsZZ", 15, 42)); + cl_git_fail(git_buf_decode_base85(&buf, "invalidchars__ ", 15, 42)); + cl_git_fail(git_buf_decode_base85(&buf, "overflowZZ~~~~~", 15, 42)); + cl_git_fail(git_buf_decode_base85(&buf, "truncated", 9, 42)); + cl_assert_equal_sz(6, buf.size); + cl_assert_equal_s("foobar", buf.ptr); +} + void test_core_buffer__classify_with_utf8(void) { char *data0 = "Simple text\n"; From b8dc2fdb92c350b786fe4cb27e9b841b794c1e86 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Thu, 9 Jul 2015 18:36:53 -0500 Subject: [PATCH 09/54] zstream: fail when asked to inflate garbage When we are provided some input buffer (with a length) to inflate, and it contains more data than simply the deflated data, fail. zlib will helpfully tell us when it is done reading (via Z_STREAM_END), so if there is data leftover in the input buffer, fail lest we continually try to inflate it. --- src/zstream.c | 5 +++++ tests/core/zstream.c | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/zstream.c b/src/zstream.c index 6533449e83a..d9ad4ca894f 100644 --- a/src/zstream.c +++ b/src/zstream.c @@ -86,6 +86,11 @@ int git_zstream_get_output(void *out, size_t *out_len, git_zstream *zstream) int zflush = Z_FINISH; size_t out_remain = *out_len; + if (zstream->in_len && zstream->zerr == Z_STREAM_END) { + giterr_set(GITERR_ZLIB, "zlib input had trailing garbage"); + return -1; + } + while (out_remain > 0 && zstream->zerr != Z_STREAM_END) { size_t out_queued, in_queued, out_used, in_used; diff --git a/tests/core/zstream.c b/tests/core/zstream.c index b13429b0a8b..961904ec378 100644 --- a/tests/core/zstream.c +++ b/tests/core/zstream.c @@ -58,6 +58,25 @@ void test_core_zstream__basic(void) assert_zlib_equal(data, strlen(data) + 1, out, outlen); } +void test_core_zstream__fails_on_trailing_garbage(void) +{ + git_buf deflated = GIT_BUF_INIT, inflated = GIT_BUF_INIT; + size_t i = 0; + + /* compress a simple string */ + git_zstream_deflatebuf(&deflated, "foobar!!", 8); + + /* append some garbage */ + for (i = 0; i < 10; i++) { + git_buf_putc(&deflated, i); + } + + cl_git_fail(git_zstream_inflatebuf(&inflated, deflated.ptr, deflated.size)); + + git_buf_free(&deflated); + git_buf_free(&inflated); +} + void test_core_zstream__buffer(void) { git_buf out = GIT_BUF_INIT; From 5d17d72621acad4aa216cf6a05c7f46b8206f284 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Thu, 9 Jul 2015 19:22:28 -0500 Subject: [PATCH 10/54] patch parsing: parse binary patch files --- src/patch.c | 154 ++++++++++++++++++++++++++++++++--- tests/apply/fromfile.c | 178 ++++++++++++++++++++++++++++------------- 2 files changed, 268 insertions(+), 64 deletions(-) diff --git a/src/patch.c b/src/patch.c index 9999fa24dc5..f6eceac2d1e 100644 --- a/src/patch.c +++ b/src/patch.c @@ -65,6 +65,15 @@ static int parse_advance_ws(patch_parse_ctx *ctx) return ret; } +static int parse_advance_nl(patch_parse_ctx *ctx) +{ + if (ctx->line_len != 1 || ctx->line[0] != '\n') + return -1; + + parse_advance_line(ctx); + return 0; +} + static int header_path_len(patch_parse_ctx *ctx) { bool inquote = 0; @@ -354,6 +363,7 @@ typedef struct { static const header_git_op header_git_ops[] = { { "@@ -", NULL }, + { "GIT binary patch", NULL }, { "--- ", parse_header_git_oldpath }, { "+++ ", parse_header_git_newpath }, { "index ", parse_header_git_index }, @@ -426,7 +436,7 @@ static int parse_header_git( return error; } -static int parse_number(int *out, patch_parse_ctx *ctx) +static int parse_number(git_off_t *out, patch_parse_ctx *ctx) { const char *end; int64_t num; @@ -440,12 +450,23 @@ static int parse_number(int *out, patch_parse_ctx *ctx) if (num < 0) return -1; - *out = (int)num; + *out = num; parse_advance_chars(ctx, (end - ctx->line)); return 0; } +static int parse_int(int *out, patch_parse_ctx *ctx) +{ + git_off_t num; + + if (parse_number(&num, ctx) < 0 || !git__is_int(num)) + return -1; + + *out = (int)num; + return 0; +} + static int parse_hunk_header( diff_patch_hunk *hunk, patch_parse_ctx *ctx) @@ -456,22 +477,22 @@ static int parse_hunk_header( hunk->hunk.new_lines = 1; if (parse_advance_expected(ctx, "@@ -", 4) < 0 || - parse_number(&hunk->hunk.old_start, ctx) < 0) + parse_int(&hunk->hunk.old_start, ctx) < 0) goto fail; if (ctx->line_len > 0 && ctx->line[0] == ',') { if (parse_advance_expected(ctx, ",", 1) < 0 || - parse_number(&hunk->hunk.old_lines, ctx) < 0) + parse_int(&hunk->hunk.old_lines, ctx) < 0) goto fail; } if (parse_advance_expected(ctx, " +", 2) < 0 || - parse_number(&hunk->hunk.new_start, ctx) < 0) + parse_int(&hunk->hunk.new_start, ctx) < 0) goto fail; if (ctx->line_len > 0 && ctx->line[0] == ',') { if (parse_advance_expected(ctx, ",", 1) < 0 || - parse_number(&hunk->hunk.new_lines, ctx) < 0) + parse_int(&hunk->hunk.new_lines, ctx) < 0) goto fail; } @@ -672,7 +693,110 @@ static int parse_patch_header( return error; } -static int parse_patch_body( +static int parse_patch_binary_side( + git_diff_binary_file *binary, + patch_parse_ctx *ctx) +{ + git_diff_binary_t type = GIT_DIFF_BINARY_NONE; + git_buf base85 = GIT_BUF_INIT, decoded = GIT_BUF_INIT; + git_off_t len; + int error = 0; + + if (ctx->line_len >= 8 && memcmp(ctx->line, "literal ", 8) == 0) { + type = GIT_DIFF_BINARY_LITERAL; + parse_advance_chars(ctx, 8); + } else if (ctx->line_len >= 6 && memcmp(ctx->line, "delta ", 6) == 0) { + type = GIT_DIFF_BINARY_DELTA; + parse_advance_chars(ctx, 6); + } else { + error = parse_err("unknown binary delta type at line %d", ctx->line_num); + goto done; + } + + if (parse_number(&len, ctx) < 0 || parse_advance_nl(ctx) < 0 || len < 0) { + error = parse_err("invalid binary size at line %d", ctx->line_num); + goto done; + } + + while (ctx->line_len) { + char c = ctx->line[0]; + size_t encoded_len, decoded_len = 0, decoded_orig = decoded.size; + + if (c == '\n') + break; + else if (c >= 'A' && c <= 'Z') + decoded_len = c - 'A' + 1; + else if (c >= 'a' && c <= 'z') + decoded_len = c - 'a' + 26 + 1; + + if (!decoded_len) { + error = parse_err("invalid binary length at line %d", ctx->line_num); + goto done; + } + + parse_advance_chars(ctx, 1); + + encoded_len = ((decoded_len / 4) + !!(decoded_len % 4)) * 5; + + if (encoded_len > ctx->line_len - 1) { + error = parse_err("truncated binary data at line %d", ctx->line_num); + goto done; + } + + if ((error = git_buf_decode_base85( + &decoded, ctx->line, encoded_len, decoded_len)) < 0) + goto done; + + if (decoded.size - decoded_orig != decoded_len) { + error = parse_err("truncated binary data at line %d", ctx->line_num); + goto done; + } + + parse_advance_chars(ctx, encoded_len); + + if (parse_advance_nl(ctx) < 0) { + error = parse_err("trailing data at line %d", ctx->line_num); + goto done; + } + } + + binary->type = type; + binary->inflatedlen = (size_t)len; + binary->datalen = decoded.size; + binary->data = git_buf_detach(&decoded); + +done: + git_buf_free(&base85); + git_buf_free(&decoded); + return error; +} + +static int parse_patch_binary( + git_patch *patch, + patch_parse_ctx *ctx) +{ + int error; + + if (parse_advance_expected(ctx, "GIT binary patch", 16) < 0 || + parse_advance_nl(ctx) < 0) + return parse_err("corrupt git binary header at line %d", ctx->line_num); + + /* parse old->new binary diff */ + if ((error = parse_patch_binary_side(&patch->binary.new_file, ctx)) < 0) + return error; + + if (parse_advance_nl(ctx) < 0) + return parse_err("corrupt git binary separator at line %d", ctx->line_num); + + /* parse new->old binary diff */ + if ((error = parse_patch_binary_side(&patch->binary.old_file, ctx)) < 0) + return error; + + patch->delta->flags |= GIT_DIFF_FLAG_BINARY; + return 0; +} + +static int parse_patch_hunks( git_patch *patch, patch_parse_ctx *ctx) { @@ -698,6 +822,17 @@ static int parse_patch_body( return error; } +static int parse_patch_body(git_patch *patch, patch_parse_ctx *ctx) +{ + if (ctx->line_len >= 16 && memcmp(ctx->line, "GIT binary patch", 16) == 0) + return parse_patch_binary(patch, ctx); + + else if (ctx->line_len >= 4 && memcmp(ctx->line, "@@ -", 4) == 0) + return parse_patch_hunks(patch, ctx); + + return 0; +} + static int check_patch(git_patch *patch) { if (!patch->ofile.file->path && patch->delta->status != GIT_DELTA_ADDED) @@ -712,8 +847,9 @@ static int check_patch(git_patch *patch) } if (patch->delta->status == GIT_DELTA_MODIFIED && - patch->nfile.file->mode == patch->ofile.file->mode && - git_array_size(patch->hunks) == 0) + !(patch->delta->flags & GIT_DIFF_FLAG_BINARY) && + patch->nfile.file->mode == patch->ofile.file->mode && + git_array_size(patch->hunks) == 0) return parse_err("patch with no hunks"); return 0; diff --git a/tests/apply/fromfile.c b/tests/apply/fromfile.c index 75b318523e9..4b1bacddb0e 100644 --- a/tests/apply/fromfile.c +++ b/tests/apply/fromfile.c @@ -21,7 +21,9 @@ void test_apply_fromfile__cleanup(void) static int apply_patchfile( const char *old, + size_t old_len, const char *new, + size_t new_len, const char *patchfile, const char *filename_expected, unsigned int mode_expected) @@ -35,13 +37,11 @@ static int apply_patchfile( cl_git_pass(git_patch_from_patchfile(&patch, patchfile, strlen(patchfile))); - error = git_apply__patch(&result, &filename, &mode, old, old ? strlen(old) : 0, patch); + error = git_apply__patch(&result, &filename, &mode, old, old_len, patch); if (error == 0) { - if (new == NULL) - cl_assert_equal_i(0, result.size); - else - cl_assert_equal_s(new, result.ptr); + cl_assert_equal_i(new_len, result.size); + cl_assert(memcmp(new, result.ptr, new_len) == 0); cl_assert_equal_s(filename_expected, filename); cl_assert_equal_i(mode_expected, mode); @@ -57,7 +57,9 @@ static int apply_patchfile( static int validate_and_apply_patchfile( const char *old, + size_t old_len, const char *new, + size_t new_len, const char *patchfile, const git_diff_options *diff_opts, const char *filename_expected, @@ -68,14 +70,14 @@ static int validate_and_apply_patchfile( int error; cl_git_pass(git_patch_from_buffers(&patch_fromdiff, - old, old ? strlen(old) : 0, "file.txt", - new, new ? strlen(new) : 0, "file.txt", + old, old_len, "file.txt", + new, new_len, "file.txt", diff_opts)); cl_git_pass(git_patch_to_buf(&validated, patch_fromdiff)); cl_assert_equal_s(patchfile, validated.ptr); - error = apply_patchfile(old, new, patchfile, filename_expected, mode_expected); + error = apply_patchfile(old, old_len, new, new_len, patchfile, filename_expected, mode_expected); git_buf_free(&validated); git_patch_free(patch_fromdiff); @@ -85,8 +87,10 @@ static int validate_and_apply_patchfile( void test_apply_fromfile__change_middle(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, - FILE_CHANGE_MIDDLE, PATCH_ORIGINAL_TO_CHANGE_MIDDLE, NULL, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), + PATCH_ORIGINAL_TO_CHANGE_MIDDLE, NULL, "b/file.txt", 0100644)); } @@ -95,28 +99,36 @@ void test_apply_fromfile__change_middle_nocontext(void) git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; diff_opts.context_lines = 0; - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, - FILE_CHANGE_MIDDLE, PATCH_ORIGINAL_TO_CHANGE_MIDDLE_NOCONTEXT, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), + PATCH_ORIGINAL_TO_CHANGE_MIDDLE_NOCONTEXT, &diff_opts, "b/file.txt", 0100644)); } void test_apply_fromfile__change_firstline(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, - FILE_CHANGE_FIRSTLINE, PATCH_ORIGINAL_TO_CHANGE_FIRSTLINE, NULL, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_FIRSTLINE, strlen(FILE_CHANGE_FIRSTLINE), + PATCH_ORIGINAL_TO_CHANGE_FIRSTLINE, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__lastline(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, - FILE_CHANGE_LASTLINE, PATCH_ORIGINAL_TO_CHANGE_LASTLINE, NULL, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_LASTLINE, strlen(FILE_CHANGE_LASTLINE), + PATCH_ORIGINAL_TO_CHANGE_LASTLINE, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__prepend(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, FILE_PREPEND, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_PREPEND, strlen(FILE_PREPEND), PATCH_ORIGINAL_TO_PREPEND, NULL, "b/file.txt", 0100644)); } @@ -125,14 +137,18 @@ void test_apply_fromfile__prepend_nocontext(void) git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; diff_opts.context_lines = 0; - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, FILE_PREPEND, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_PREPEND, strlen(FILE_PREPEND), PATCH_ORIGINAL_TO_PREPEND_NOCONTEXT, &diff_opts, "b/file.txt", 0100644)); } void test_apply_fromfile__append(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, FILE_APPEND, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_APPEND, strlen(FILE_APPEND), PATCH_ORIGINAL_TO_APPEND, NULL, "b/file.txt", 0100644)); } @@ -141,82 +157,108 @@ void test_apply_fromfile__append_nocontext(void) git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; diff_opts.context_lines = 0; - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, FILE_APPEND, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_APPEND, strlen(FILE_APPEND), PATCH_ORIGINAL_TO_APPEND_NOCONTEXT, &diff_opts, "b/file.txt", 0100644)); } void test_apply_fromfile__prepend_and_append(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, - FILE_PREPEND_AND_APPEND, PATCH_ORIGINAL_TO_PREPEND_AND_APPEND, NULL, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_PREPEND_AND_APPEND, strlen(FILE_PREPEND_AND_APPEND), + PATCH_ORIGINAL_TO_PREPEND_AND_APPEND, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__to_empty_file(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, "", + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + "", 0, PATCH_ORIGINAL_TO_EMPTY_FILE, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__from_empty_file(void) { - cl_git_pass(validate_and_apply_patchfile("", FILE_ORIGINAL, + cl_git_pass(validate_and_apply_patchfile( + "", 0, + FILE_ORIGINAL, strlen(FILE_ORIGINAL), PATCH_EMPTY_FILE_TO_ORIGINAL, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__add(void) { - cl_git_pass(validate_and_apply_patchfile(NULL, FILE_ORIGINAL, + cl_git_pass(validate_and_apply_patchfile( + NULL, 0, + FILE_ORIGINAL, strlen(FILE_ORIGINAL), PATCH_ADD_ORIGINAL, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__delete(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, NULL, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + NULL, 0, PATCH_DELETE_ORIGINAL, NULL, NULL, 0)); } void test_apply_fromfile__rename_exact(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_ORIGINAL, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_ORIGINAL, strlen(FILE_ORIGINAL), PATCH_RENAME_EXACT, "b/newfile.txt", 0100644)); } void test_apply_fromfile__rename_similar(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), PATCH_RENAME_SIMILAR, "b/newfile.txt", 0100644)); } void test_apply_fromfile__rename_similar_quotedname(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), PATCH_RENAME_SIMILAR_QUOTEDNAME, "b/foo\"bar.txt", 0100644)); } void test_apply_fromfile__modechange(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_ORIGINAL, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_ORIGINAL, strlen(FILE_ORIGINAL), PATCH_MODECHANGE_UNCHANGED, "b/file.txt", 0100755)); } void test_apply_fromfile__modechange_with_modification(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), PATCH_MODECHANGE_MODIFIED, "b/file.txt", 0100755)); } void test_apply_fromfile__noisy(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), PATCH_NOISY, "b/file.txt", 0100644)); } void test_apply_fromfile__noisy_nocontext(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), PATCH_NOISY_NOCONTEXT, "b/file.txt", 0100644)); } @@ -250,15 +292,19 @@ void test_apply_fromfile__fail_corrupt_githeader(void) void test_apply_fromfile__empty_context(void) { - cl_git_pass(apply_patchfile(FILE_EMPTY_CONTEXT_ORIGINAL, - FILE_EMPTY_CONTEXT_MODIFIED, PATCH_EMPTY_CONTEXT, + cl_git_pass(apply_patchfile( + FILE_EMPTY_CONTEXT_ORIGINAL, strlen(FILE_EMPTY_CONTEXT_ORIGINAL), + FILE_EMPTY_CONTEXT_MODIFIED, strlen(FILE_EMPTY_CONTEXT_MODIFIED), + PATCH_EMPTY_CONTEXT, "b/file.txt", 0100644)); } void test_apply_fromfile__append_no_nl(void) { cl_git_pass(validate_and_apply_patchfile( - FILE_ORIGINAL, FILE_APPEND_NO_NL, PATCH_APPEND_NO_NL, NULL, "b/file.txt", 0100644)); + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_APPEND_NO_NL, strlen(FILE_APPEND_NO_NL), + PATCH_APPEND_NO_NL, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__fail_missing_new_file(void) @@ -300,29 +346,51 @@ void test_apply_fromfile__fail_not_a_patch(void) strlen(PATCH_NOT_A_PATCH))); } -/* -void test_apply_fromdiff__binary_change_must_be_reversible(void) +void test_apply_fromfile__binary_add(void) { - git_buf original = GIT_BUF_INIT, modified = GIT_BUF_INIT, - result = GIT_BUF_INIT; - char *filename; - unsigned int mode; + cl_git_pass(apply_patchfile( + NULL, 0, + FILE_BINARY_DELTA_MODIFIED, FILE_BINARY_DELTA_MODIFIED_LEN, + PATCH_BINARY_ADD, "b/binary.bin", 0100644)); +} - original.ptr = FILE_BINARY_DELTA_ORIGINAL; - original.size = FILE_BINARY_DELTA_ORIGINAL_LEN; +void test_apply_fromfile__binary_change_delta(void) +{ + cl_git_pass(apply_patchfile( + FILE_BINARY_DELTA_ORIGINAL, FILE_BINARY_DELTA_ORIGINAL_LEN, + FILE_BINARY_DELTA_MODIFIED, FILE_BINARY_DELTA_MODIFIED_LEN, + PATCH_BINARY_DELTA, "b/binary.bin", 0100644)); +} - modified.ptr = FILE_BINARY_DELTA_MODIFIED; - modified.size = FILE_BINARY_DELTA_MODIFIED_LEN; +void test_apply_fromfile__binary_change_literal(void) +{ + cl_git_pass(apply_patchfile( + FILE_BINARY_LITERAL_ORIGINAL, FILE_BINARY_LITERAL_ORIGINAL_LEN, + FILE_BINARY_LITERAL_MODIFIED, FILE_BINARY_LITERAL_MODIFIED_LEN, + PATCH_BINARY_LITERAL, "b/binary.bin", 0100644)); +} - cl_git_fail(git_apply__patch(&result, &filename, &mode, old ? old->ptr : NULL, old ? old->size : 0, patch); +void test_apply_fromfile__binary_delete(void) +{ + cl_git_pass(apply_patchfile( + FILE_BINARY_DELTA_MODIFIED, FILE_BINARY_DELTA_MODIFIED_LEN, + NULL, 0, + PATCH_BINARY_DELETE, NULL, 0)); +} - cl_git_fail(apply_gitbuf( - &original, "binary.bin", - &modified, "binary.bin", - PATCH_BINARY_NOT_REVERSIBLE, &binary_opts)); - cl_assert_equal_s("binary patch did not apply cleanly", giterr_last()->message); +void test_apply_fromfile__binary_change_does_not_apply(void) +{ + /* try to apply patch backwards, ensure it does not apply */ + cl_git_fail(apply_patchfile( + FILE_BINARY_DELTA_MODIFIED, FILE_BINARY_DELTA_MODIFIED_LEN, + FILE_BINARY_DELTA_ORIGINAL, FILE_BINARY_DELTA_ORIGINAL_LEN, + PATCH_BINARY_DELTA, "b/binary.bin", 0100644)); +} - git_buf_free(&result); - git__free(filename); +void test_apply_fromfile__binary_change_must_be_reversible(void) +{ + cl_git_fail(apply_patchfile( + FILE_BINARY_DELTA_MODIFIED, FILE_BINARY_DELTA_MODIFIED_LEN, + NULL, 0, + PATCH_BINARY_NOT_REVERSIBLE, NULL, 0)); } -*/ From 8d2eef27ffae3fe9cfbaa0c0c16655598b5b1d6b Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Fri, 10 Jul 2015 09:09:27 -0500 Subject: [PATCH 11/54] patch parsing: ensure empty patches are illegal --- tests/apply/fromfile.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/apply/fromfile.c b/tests/apply/fromfile.c index 4b1bacddb0e..1e21659199a 100644 --- a/tests/apply/fromfile.c +++ b/tests/apply/fromfile.c @@ -394,3 +394,11 @@ void test_apply_fromfile__binary_change_must_be_reversible(void) NULL, 0, PATCH_BINARY_NOT_REVERSIBLE, NULL, 0)); } + +void test_apply_fromfile__empty_file_not_allowed(void) +{ + git_patch *patch; + + cl_git_fail(git_patch_from_patchfile(&patch, "", 0)); + cl_git_fail(git_patch_from_patchfile(&patch, NULL, 0)); +} From 804d5fe9f59f4d8548da9f650bc43050951f26d7 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Fri, 11 Sep 2015 08:37:12 -0400 Subject: [PATCH 12/54] patch: abstract patches into diff'ed and parsed Patches can now come from a variety of sources - either internally generated (from diffing two commits) or as the results of parsing some external data. --- src/apply.c | 14 +- src/diff_driver.c | 1 - src/diff_patch.h | 83 --- src/diff_print.c | 149 ++--- src/diff_stats.c | 5 +- src/diff_xdiff.c | 18 +- src/diff_xdiff.h | 6 +- src/patch.c | 943 ++++------------------------- src/patch.h | 57 ++ src/{diff_patch.c => patch_diff.c} | 509 +++++----------- src/patch_diff.h | 63 ++ src/patch_parse.c | 920 ++++++++++++++++++++++++++++ tests/apply/fromfile.c | 1 + 13 files changed, 1408 insertions(+), 1361 deletions(-) delete mode 100644 src/diff_patch.h create mode 100644 src/patch.h rename src/{diff_patch.c => patch_diff.c} (65%) create mode 100644 src/patch_diff.h create mode 100644 src/patch_parse.c diff --git a/src/apply.c b/src/apply.c index c667c6308de..875f3d0421a 100644 --- a/src/apply.c +++ b/src/apply.c @@ -10,7 +10,7 @@ #include "git2/patch.h" #include "git2/filter.h" #include "array.h" -#include "diff_patch.h" +#include "patch.h" #include "fileops.h" #include "apply.h" #include "delta.h" @@ -163,7 +163,7 @@ static int update_hunk( static int apply_hunk( patch_image *image, git_patch *patch, - diff_patch_hunk *hunk) + git_patch_hunk *hunk) { patch_image preimage, postimage; size_t line_num, i; @@ -218,7 +218,7 @@ static int apply_hunks( size_t source_len, git_patch *patch) { - diff_patch_hunk *hunk; + git_patch_hunk *hunk; git_diff_line *line; patch_image image; size_t i; @@ -340,9 +340,11 @@ int git_apply__patch( *mode_out = 0; if (patch->delta->status != GIT_DELTA_DELETED) { - filename = git__strdup(patch->nfile.file->path); - mode = patch->nfile.file->mode ? - patch->nfile.file->mode : GIT_FILEMODE_BLOB; + const git_diff_file *newfile = patch->newfile(patch); + + filename = git__strdup(newfile->path); + mode = newfile->mode ? + newfile->mode : GIT_FILEMODE_BLOB; } if (patch->delta->flags & GIT_DIFF_FLAG_BINARY) diff --git a/src/diff_driver.c b/src/diff_driver.c index bc3518991c8..3874c838e0b 100644 --- a/src/diff_driver.c +++ b/src/diff_driver.c @@ -9,7 +9,6 @@ #include "git2/attr.h" #include "diff.h" -#include "diff_patch.h" #include "diff_driver.h" #include "strmap.h" #include "map.h" diff --git a/src/diff_patch.h b/src/diff_patch.h deleted file mode 100644 index 7b4dacddec5..00000000000 --- a/src/diff_patch.h +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) the libgit2 contributors. All rights reserved. - * - * This file is part of libgit2, distributed under the GNU GPL v2 with - * a Linking Exception. For full terms see the included COPYING file. - */ -#ifndef INCLUDE_diff_patch_h__ -#define INCLUDE_diff_patch_h__ - -#include "common.h" -#include "diff.h" -#include "diff_file.h" -#include "array.h" -#include "git2/patch.h" - - /* cached information about a hunk in a diff */ -typedef struct diff_patch_hunk { - git_diff_hunk hunk; - size_t line_start; - size_t line_count; -} diff_patch_hunk; - -enum { - GIT_DIFF_PATCH_ALLOCATED = (1 << 0), - GIT_DIFF_PATCH_INITIALIZED = (1 << 1), - GIT_DIFF_PATCH_LOADED = (1 << 2), - /* the two sides are different */ - GIT_DIFF_PATCH_DIFFABLE = (1 << 3), - /* the difference between the two sides has been computed */ - GIT_DIFF_PATCH_DIFFED = (1 << 4), - GIT_DIFF_PATCH_FLATTENED = (1 << 5), -}; - -struct git_patch { - git_refcount rc; - git_diff *diff; /* for refcount purposes, maybe NULL for blob diffs */ - git_diff_options diff_opts; - git_diff_delta *delta; - size_t delta_index; - git_diff_file_content ofile; - git_diff_file_content nfile; - uint32_t flags; - git_diff_binary binary; - git_array_t(diff_patch_hunk) hunks; - git_array_t(git_diff_line) lines; - size_t content_size, context_size, header_size; - git_pool flattened; -}; - -extern git_diff *git_patch__diff(git_patch *); - -extern git_diff_driver *git_patch__driver(git_patch *); - -extern void git_patch__old_data(char **, size_t *, git_patch *); -extern void git_patch__new_data(char **, size_t *, git_patch *); - -extern int git_patch__invoke_callbacks( - git_patch *patch, - git_diff_file_cb file_cb, - git_diff_binary_cb binary_cb, - git_diff_hunk_cb hunk_cb, - git_diff_line_cb line_cb, - void *payload); - -typedef struct git_diff_output git_diff_output; -struct git_diff_output { - /* these callbacks are issued with the diff data */ - git_diff_file_cb file_cb; - git_diff_binary_cb binary_cb; - git_diff_hunk_cb hunk_cb; - git_diff_line_cb data_cb; - void *payload; - - /* this records the actual error in cases where it may be obscured */ - int error; - - /* this callback is used to do the diff and drive the other callbacks. - * see diff_xdiff.h for how to use this in practice for now. - */ - int (*diff_cb)(git_diff_output *output, git_patch *patch); -}; - -#endif diff --git a/src/diff_print.c b/src/diff_print.c index d1834846254..09bf77aef8e 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -6,7 +6,8 @@ */ #include "common.h" #include "diff.h" -#include "diff_patch.h" +#include "diff_file.h" +#include "patch_diff.h" #include "fileops.h" #include "zstream.h" #include "blob.h" @@ -14,19 +15,19 @@ #include "git2/sys/diff.h" typedef struct { - git_diff *diff; git_diff_format_t format; git_diff_line_cb print_cb; void *payload; + git_buf *buf; + git_diff_line line; + + const char *old_prefix; + const char *new_prefix; uint32_t flags; int oid_strlen; - git_diff_line line; - unsigned int - content_loaded : 1, - content_allocated : 1; - git_diff_file_content *ofile; - git_diff_file_content *nfile; + + int (*strcomp)(const char *, const char *); } diff_print_info; static int diff_print_info_init__common( @@ -74,11 +75,13 @@ static int diff_print_info_init_fromdiff( memset(pi, 0, sizeof(diff_print_info)); - pi->diff = diff; - if (diff) { pi->flags = diff->opts.flags; pi->oid_strlen = diff->opts.id_abbrev; + pi->old_prefix = diff->opts.old_prefix; + pi->new_prefix = diff->opts.new_prefix; + + pi->strcomp = diff->strcomp; } return diff_print_info_init__common(pi, out, repo, format, cb, payload); @@ -92,24 +95,16 @@ static int diff_print_info_init_frompatch( git_diff_line_cb cb, void *payload) { - git_repository *repo; - assert(patch); - repo = patch->diff ? patch->diff->repo : NULL; - memset(pi, 0, sizeof(diff_print_info)); - pi->diff = patch->diff; - pi->flags = patch->diff_opts.flags; pi->oid_strlen = patch->diff_opts.id_abbrev; + pi->old_prefix = patch->diff_opts.old_prefix; + pi->new_prefix = patch->diff_opts.new_prefix; - pi->content_loaded = 1; - pi->ofile = &patch->ofile; - pi->nfile = &patch->nfile; - - return diff_print_info_init__common(pi, out, repo, format, cb, payload); + return diff_print_info_init__common(pi, out, patch->repo, format, cb, payload); } static char diff_pick_suffix(int mode) @@ -173,8 +168,8 @@ static int diff_print_one_name_status( diff_print_info *pi = data; git_buf *out = pi->buf; char old_suffix, new_suffix, code = git_diff_status_char(delta->status); - int (*strcomp)(const char *, const char *) = - pi->diff ? pi->diff->strcomp : git__strcmp; + int(*strcomp)(const char *, const char *) = pi->strcomp ? + pi->strcomp : git__strcmp; GIT_UNUSED(progress); @@ -367,39 +362,6 @@ static int format_binary( return 0; } -static int diff_print_load_content( - diff_print_info *pi, - git_diff_delta *delta) -{ - git_diff_file_content *ofile, *nfile; - int error; - - assert(pi->diff); - - ofile = git__calloc(1, sizeof(git_diff_file_content)); - nfile = git__calloc(1, sizeof(git_diff_file_content)); - - GITERR_CHECK_ALLOC(ofile); - GITERR_CHECK_ALLOC(nfile); - - if ((error = git_diff_file_content__init_from_diff( - ofile, pi->diff, delta, true)) < 0 || - (error = git_diff_file_content__init_from_diff( - nfile, pi->diff, delta, true)) < 0) { - - git__free(ofile); - git__free(nfile); - return error; - } - - pi->content_loaded = 1; - pi->content_allocated = 1; - pi->ofile = ofile; - pi->nfile = nfile; - - return 0; -} - static int diff_print_patch_file_binary( diff_print_info *pi, git_diff_delta *delta, const char *old_pfx, const char *new_pfx, @@ -411,10 +373,6 @@ static int diff_print_patch_file_binary( if ((pi->flags & GIT_DIFF_SHOW_BINARY) == 0) goto noshow; - if (!pi->content_loaded && - (error = diff_print_load_content(pi, delta)) < 0) - return error; - if (binary->new_file.datalen == 0 && binary->old_file.datalen == 0) return 0; @@ -450,9 +408,9 @@ static int diff_print_patch_file( int error; diff_print_info *pi = data; const char *oldpfx = - pi->diff ? pi->diff->opts.old_prefix : DIFF_OLD_PREFIX_DEFAULT; + pi->old_prefix ? pi->old_prefix : DIFF_OLD_PREFIX_DEFAULT; const char *newpfx = - pi->diff ? pi->diff->opts.new_prefix : DIFF_NEW_PREFIX_DEFAULT; + pi->new_prefix ? pi->new_prefix : DIFF_NEW_PREFIX_DEFAULT; bool binary = (delta->flags & GIT_DIFF_FLAG_BINARY) || (pi->flags & GIT_DIFF_FORCE_BINARY); @@ -488,9 +446,9 @@ static int diff_print_patch_binary( { diff_print_info *pi = data; const char *old_pfx = - pi->diff ? pi->diff->opts.old_prefix : DIFF_OLD_PREFIX_DEFAULT; + pi->old_prefix ? pi->old_prefix : DIFF_OLD_PREFIX_DEFAULT; const char *new_pfx = - pi->diff ? pi->diff->opts.new_prefix : DIFF_NEW_PREFIX_DEFAULT; + pi->new_prefix ? pi->new_prefix : DIFF_NEW_PREFIX_DEFAULT; int error; git_buf_clear(pi->buf); @@ -585,43 +543,11 @@ int git_diff_print( giterr_set_after_callback_function(error, "git_diff_print"); } - git__free(pi.nfile); - git__free(pi.ofile); - git_buf_free(&buf); return error; } -/* print a git_patch to an output callback */ -int git_patch_print( - git_patch *patch, - git_diff_line_cb print_cb, - void *payload) -{ - int error; - git_buf temp = GIT_BUF_INIT; - diff_print_info pi; - - assert(patch && print_cb); - - if (!(error = diff_print_info_init_frompatch( - &pi, &temp, patch, - GIT_DIFF_FORMAT_PATCH, print_cb, payload))) - { - error = git_patch__invoke_callbacks( - patch, diff_print_patch_file, diff_print_patch_binary, - diff_print_patch_hunk, diff_print_patch_line, &pi); - - if (error) /* make sure error message is set */ - giterr_set_after_callback_function(error, "git_patch_print"); - } - - git_buf_free(&temp); - - return error; -} - int git_diff_print_callback__to_buf( const git_diff_delta *delta, const git_diff_hunk *hunk, @@ -662,6 +588,37 @@ int git_diff_print_callback__to_file_handle( return 0; } +/* print a git_patch to an output callback */ +int git_patch_print( + git_patch *patch, + git_diff_line_cb print_cb, + void *payload) +{ + int error; + git_buf temp = GIT_BUF_INIT; + diff_print_info pi; + + assert(patch && print_cb); + + if (!(error = diff_print_info_init_frompatch( + &pi, &temp, patch, + GIT_DIFF_FORMAT_PATCH, print_cb, payload))) + { + error = git_patch__invoke_callbacks( + patch, + diff_print_patch_file, diff_print_patch_binary, + diff_print_patch_hunk, diff_print_patch_line, + &pi); + + if (error) /* make sure error message is set */ + giterr_set_after_callback_function(error, "git_patch_print"); + } + + git_buf_free(&temp); + + return error; +} + /* print a git_patch to a git_buf */ int git_patch_to_buf(git_buf *out, git_patch *patch) { diff --git a/src/diff_stats.c b/src/diff_stats.c index 42ccbfb8712..f2eb6968035 100644 --- a/src/diff_stats.c +++ b/src/diff_stats.c @@ -7,7 +7,7 @@ #include "common.h" #include "vector.h" #include "diff.h" -#include "diff_patch.h" +#include "patch_diff.h" #define DIFF_RENAME_FILE_SEPARATOR " => " #define STATS_FULL_MIN_SCALE 7 @@ -190,8 +190,9 @@ int git_diff_get_stats( break; /* keep a count of renames because it will affect formatting */ - delta = git_patch_get_delta(patch); + delta = patch->delta; + /* TODO ugh */ namelen = strlen(delta->new_file.path); if (strcmp(delta->old_file.path, delta->new_file.path) != 0) { namelen += strlen(delta->old_file.path); diff --git a/src/diff_xdiff.c b/src/diff_xdiff.c index 1057df3aaca..8017c954118 100644 --- a/src/diff_xdiff.c +++ b/src/diff_xdiff.c @@ -8,8 +8,8 @@ #include "common.h" #include "diff.h" #include "diff_driver.h" -#include "diff_patch.h" #include "diff_xdiff.h" +#include "patch_diff.h" static int git_xdiff_scan_int(const char **str, int *value) { @@ -56,7 +56,7 @@ static int git_xdiff_parse_hunk(git_diff_hunk *hunk, const char *header) typedef struct { git_xdiff_output *xo; - git_patch *patch; + git_patch_diff *patch; git_diff_hunk hunk; int old_lineno, new_lineno; mmfile_t xd_old_data, xd_new_data; @@ -110,9 +110,9 @@ static int diff_update_lines( static int git_xdiff_cb(void *priv, mmbuffer_t *bufs, int len) { git_xdiff_info *info = priv; - git_patch *patch = info->patch; - const git_diff_delta *delta = git_patch_get_delta(patch); - git_diff_output *output = &info->xo->output; + git_patch_diff *patch = info->patch; + const git_diff_delta *delta = patch->base.delta; + git_patch_diff_output *output = &info->xo->output; git_diff_line line; if (len == 1) { @@ -181,7 +181,7 @@ static int git_xdiff_cb(void *priv, mmbuffer_t *bufs, int len) return output->error; } -static int git_xdiff(git_diff_output *output, git_patch *patch) +static int git_xdiff(git_patch_diff_output *output, git_patch_diff *patch) { git_xdiff_output *xo = (git_xdiff_output *)output; git_xdiff_info info; @@ -194,7 +194,7 @@ static int git_xdiff(git_diff_output *output, git_patch *patch) xo->callback.priv = &info; git_diff_find_context_init( - &xo->config.find_func, &findctxt, git_patch__driver(patch)); + &xo->config.find_func, &findctxt, git_patch_diff_driver(patch)); xo->config.find_func_priv = &findctxt; if (xo->config.find_func != NULL) @@ -206,8 +206,8 @@ static int git_xdiff(git_diff_output *output, git_patch *patch) * updates are needed to xo->params.flags */ - git_patch__old_data(&info.xd_old_data.ptr, &info.xd_old_data.size, patch); - git_patch__new_data(&info.xd_new_data.ptr, &info.xd_new_data.size, patch); + git_patch_diff_old_data(&info.xd_old_data.ptr, &info.xd_old_data.size, patch); + git_patch_diff_new_data(&info.xd_new_data.ptr, &info.xd_new_data.size, patch); if (info.xd_old_data.size > GIT_XDIFF_MAX_SIZE || info.xd_new_data.size > GIT_XDIFF_MAX_SIZE) { diff --git a/src/diff_xdiff.h b/src/diff_xdiff.h index 98e11b2cbf6..e1c58a22923 100644 --- a/src/diff_xdiff.h +++ b/src/diff_xdiff.h @@ -8,20 +8,20 @@ #define INCLUDE_diff_xdiff_h__ #include "diff.h" -#include "diff_patch.h" #include "xdiff/xdiff.h" +#include "patch_diff.h" /* xdiff cannot cope with large files. these files should not be passed to * xdiff. callers should treat these large files as binary. */ #define GIT_XDIFF_MAX_SIZE (1024LL * 1024 * 1023) -/* A git_xdiff_output is a git_diff_output with extra fields necessary +/* A git_xdiff_output is a git_patch_diff_output with extra fields necessary * to use libxdiff. Calling git_xdiff_init() will set the diff_cb field * of the output to use xdiff to generate the diffs. */ typedef struct { - git_diff_output output; + git_patch_diff_output output; xdemitconf_t config; xpparam_t params; diff --git a/src/patch.c b/src/patch.c index f6eceac2d1e..f05cfb21add 100644 --- a/src/patch.c +++ b/src/patch.c @@ -1,894 +1,213 @@ #include "git2/patch.h" -#include "diff_patch.h" +#include "diff.h" +#include "patch.h" -#define parse_err(...) \ - ( giterr_set(GITERR_PATCH, __VA_ARGS__), -1 ) -typedef struct { - const char *content; - size_t content_len; - - const char *line; - size_t line_len; - size_t line_num; - - size_t remain; - - char *header_new_path; - char *header_old_path; -} patch_parse_ctx; - - -static void parse_advance_line(patch_parse_ctx *ctx) -{ - ctx->line += ctx->line_len; - ctx->remain -= ctx->line_len; - ctx->line_len = git__linenlen(ctx->line, ctx->remain); - ctx->line_num++; -} - -static void parse_advance_chars(patch_parse_ctx *ctx, size_t char_cnt) -{ - ctx->line += char_cnt; - ctx->remain -= char_cnt; - ctx->line_len -= char_cnt; -} - -static int parse_advance_expected( - patch_parse_ctx *ctx, - const char *expected, - size_t expected_len) +int git_patch__invoke_callbacks( + git_patch *patch, + git_diff_file_cb file_cb, + git_diff_binary_cb binary_cb, + git_diff_hunk_cb hunk_cb, + git_diff_line_cb line_cb, + void *payload) { - if (ctx->line_len < expected_len) - return -1; + int error = 0; + uint32_t i, j; - if (memcmp(ctx->line, expected, expected_len) != 0) - return -1; + if (file_cb) + error = file_cb(patch->delta, 0, payload); - parse_advance_chars(ctx, expected_len); - return 0; -} + if ((patch->delta->flags & GIT_DIFF_FLAG_BINARY) != 0) { + if (binary_cb) + error = binary_cb(patch->delta, &patch->binary, payload); -static int parse_advance_ws(patch_parse_ctx *ctx) -{ - int ret = -1; - - while (ctx->line_len > 0 && - ctx->line[0] != '\n' && - git__isspace(ctx->line[0])) { - ctx->line++; - ctx->line_len--; - ctx->remain--; - ret = 0; + return error; } - return ret; -} + if (!hunk_cb && !line_cb) + return error; -static int parse_advance_nl(patch_parse_ctx *ctx) -{ - if (ctx->line_len != 1 || ctx->line[0] != '\n') - return -1; + for (i = 0; !error && i < git_array_size(patch->hunks); ++i) { + git_patch_hunk *h = git_array_get(patch->hunks, i); - parse_advance_line(ctx); - return 0; -} + if (hunk_cb) + error = hunk_cb(patch->delta, &h->hunk, payload); -static int header_path_len(patch_parse_ctx *ctx) -{ - bool inquote = 0; - bool quoted = (ctx->line_len > 0 && ctx->line[0] == '"'); - size_t len; + if (!line_cb) + continue; - for (len = quoted; len < ctx->line_len; len++) { - if (!quoted && git__isspace(ctx->line[len])) - break; - else if (quoted && !inquote && ctx->line[len] == '"') { - len++; - break; - } + for (j = 0; !error && j < h->line_count; ++j) { + git_diff_line *l = + git_array_get(patch->lines, h->line_start + j); - inquote = (!inquote && ctx->line[len] == '\\'); + error = line_cb(patch->delta, &h->hunk, l, payload); + } } - return len; -} - -static int parse_header_path_buf(git_buf *path, patch_parse_ctx *ctx) -{ - int path_len, error = 0; - - path_len = header_path_len(ctx); - - if ((error = git_buf_put(path, ctx->line, path_len)) < 0) - goto done; - - parse_advance_chars(ctx, path_len); - - git_buf_rtrim(path); - - if (path->size > 0 && path->ptr[0] == '"') - error = git_buf_unquote(path); - - if (error < 0) - goto done; - - git_path_squash_slashes(path); - -done: - return error; -} - -static int parse_header_path(char **out, patch_parse_ctx *ctx) -{ - git_buf path = GIT_BUF_INIT; - int error = parse_header_path_buf(&path, ctx); - - *out = git_buf_detach(&path); - return error; } -static int parse_header_git_oldpath(git_patch *patch, patch_parse_ctx *ctx) -{ - return parse_header_path((char **)&patch->ofile.file->path, ctx); -} - -static int parse_header_git_newpath(git_patch *patch, patch_parse_ctx *ctx) -{ - return parse_header_path((char **)&patch->nfile.file->path, ctx); -} - -static int parse_header_mode(uint16_t *mode, patch_parse_ctx *ctx) +size_t git_patch_size( + git_patch *patch, + int include_context, + int include_hunk_headers, + int include_file_headers) { - const char *end; - int32_t m; - int ret; + size_t out; - if (ctx->line_len < 1 || !git__isdigit(ctx->line[0])) - return parse_err("invalid file mode at line %d", ctx->line_num); + assert(patch); - if ((ret = git__strntol32(&m, ctx->line, ctx->line_len, &end, 8)) < 0) - return ret; + out = patch->content_size; - if (m > UINT16_MAX) - return -1; + if (!include_context) + out -= patch->context_size; - *mode = (uint16_t)m; + if (include_hunk_headers) + out += patch->header_size; - parse_advance_chars(ctx, (end - ctx->line)); + if (include_file_headers) { + git_buf file_header = GIT_BUF_INIT; - return ret; -} + if (git_diff_delta__format_file_header( + &file_header, patch->delta, NULL, NULL, 0) < 0) + giterr_clear(); + else + out += git_buf_len(&file_header); -static int parse_header_oid( - git_oid *oid, - size_t *oid_len, - patch_parse_ctx *ctx) -{ - size_t len; - - for (len = 0; len < ctx->line_len && len < GIT_OID_HEXSZ; len++) { - if (!git__isxdigit(ctx->line[len])) - break; + git_buf_free(&file_header); } - if (len < GIT_OID_MINPREFIXLEN || - git_oid_fromstrn(oid, ctx->line, len) < 0) - return parse_err("invalid hex formatted object id at line %d", - ctx->line_num); - - parse_advance_chars(ctx, len); - - *oid_len = len; - - return 0; + return out; } -static int parse_header_git_index(git_patch *patch, patch_parse_ctx *ctx) +int git_patch_line_stats( + size_t *total_ctxt, + size_t *total_adds, + size_t *total_dels, + const git_patch *patch) { - /* - * TODO: we read the prefix provided in the diff into the delta's id - * field, but do not mark is at an abbreviated id. - */ - size_t oid_len, nid_len; - - if (parse_header_oid(&patch->delta->old_file.id, &oid_len, ctx) < 0 || - parse_advance_expected(ctx, "..", 2) < 0 || - parse_header_oid(&patch->delta->new_file.id, &nid_len, ctx) < 0) - return -1; - - if (ctx->line_len > 0 && ctx->line[0] == ' ') { - uint16_t mode; - - parse_advance_chars(ctx, 1); + size_t totals[3], idx; - if (parse_header_mode(&mode, ctx) < 0) - return -1; + memset(totals, 0, sizeof(totals)); - if (!patch->delta->new_file.mode) - patch->delta->new_file.mode = mode; + for (idx = 0; idx < git_array_size(patch->lines); ++idx) { + git_diff_line *line = git_array_get(patch->lines, idx); + if (!line) + continue; - if (!patch->delta->old_file.mode) - patch->delta->old_file.mode = mode; + switch (line->origin) { + case GIT_DIFF_LINE_CONTEXT: totals[0]++; break; + case GIT_DIFF_LINE_ADDITION: totals[1]++; break; + case GIT_DIFF_LINE_DELETION: totals[2]++; break; + default: + /* diff --stat and --numstat don't count EOFNL marks because + * they will always be paired with a ADDITION or DELETION line. + */ + break; + } } - return 0; -} - -static int parse_header_git_oldmode(git_patch *patch, patch_parse_ctx *ctx) -{ - return parse_header_mode(&patch->ofile.file->mode, ctx); -} - -static int parse_header_git_newmode(git_patch *patch, patch_parse_ctx *ctx) -{ - return parse_header_mode(&patch->nfile.file->mode, ctx); -} - -static int parse_header_git_deletedfilemode( - git_patch *patch, - patch_parse_ctx *ctx) -{ - git__free((char *)patch->ofile.file->path); - - patch->ofile.file->path = NULL; - patch->delta->status = GIT_DELTA_DELETED; - - return parse_header_mode(&patch->ofile.file->mode, ctx); -} - -static int parse_header_git_newfilemode( - git_patch *patch, - patch_parse_ctx *ctx) -{ - git__free((char *)patch->nfile.file->path); - - patch->nfile.file->path = NULL; - patch->delta->status = GIT_DELTA_ADDED; - - return parse_header_mode(&patch->nfile.file->mode, ctx); -} - -static int parse_header_rename( - char **out, - char **header_path, - patch_parse_ctx *ctx) -{ - git_buf path = GIT_BUF_INIT; - size_t header_path_len, prefix_len; - - if (*header_path == NULL) - return parse_err("rename without proper git diff header at line %d", - ctx->line_num); - - header_path_len = strlen(*header_path); - - if (parse_header_path_buf(&path, ctx) < 0) - return -1; - - if (header_path_len < git_buf_len(&path)) - return parse_err("rename path is invalid at line %d", ctx->line_num); - - /* This sanity check exists because git core uses the data in the - * "rename from" / "rename to" lines, but it's formatted differently - * than the other paths and lacks the normal prefix. This irregularity - * causes us to ignore these paths (we always store the prefixed paths) - * but instead validate that they match the suffix of the paths we parsed - * since we would behave differently from git core if they ever differed. - * Instead, we raise an error, rather than parsing differently. - */ - prefix_len = header_path_len - path.size; - - if (strncmp(*header_path + prefix_len, path.ptr, path.size) != 0 || - (prefix_len > 0 && (*header_path)[prefix_len - 1] != '/')) - return parse_err("rename path does not match header at line %d", - ctx->line_num); - - *out = *header_path; - *header_path = NULL; - - git_buf_free(&path); + if (total_ctxt) + *total_ctxt = totals[0]; + if (total_adds) + *total_adds = totals[1]; + if (total_dels) + *total_dels = totals[2]; return 0; } -static int parse_header_renamefrom(git_patch *patch, patch_parse_ctx *ctx) +const git_diff_delta *git_patch_get_delta(const git_patch *patch) { - patch->delta->status |= GIT_DELTA_RENAMED; - - return parse_header_rename( - (char **)&patch->ofile.file->path, - &ctx->header_old_path, - ctx); + assert(patch); + return patch->delta; } -static int parse_header_renameto(git_patch *patch, patch_parse_ctx *ctx) +size_t git_patch_num_hunks(const git_patch *patch) { - patch->delta->status |= GIT_DELTA_RENAMED; - - return parse_header_rename( - (char **)&patch->nfile.file->path, - &ctx->header_new_path, - ctx); + assert(patch); + return git_array_size(patch->hunks); } -static int parse_header_percent(uint16_t *out, patch_parse_ctx *ctx) +static int patch_error_outofrange(const char *thing) { - int32_t val; - const char *end; - - if (ctx->line_len < 1 || !git__isdigit(ctx->line[0]) || - git__strntol32(&val, ctx->line, ctx->line_len, &end, 10) < 0) - return -1; - - parse_advance_chars(ctx, (end - ctx->line)); - - if (parse_advance_expected(ctx, "%", 1) < 0) - return -1; - - if (val > 100) - return -1; - - *out = val; - return 0; + giterr_set(GITERR_INVALID, "patch %s index out of range", thing); + return GIT_ENOTFOUND; } -static int parse_header_similarity(git_patch *patch, patch_parse_ctx *ctx) -{ - if (parse_header_percent(&patch->delta->similarity, ctx) < 0) - return parse_err("invalid similarity percentage at line %d", - ctx->line_num); - - return 0; -} - -static int parse_header_dissimilarity(git_patch *patch, patch_parse_ctx *ctx) -{ - uint16_t dissimilarity; - - if (parse_header_percent(&dissimilarity, ctx) < 0) - return parse_err("invalid similarity percentage at line %d", - ctx->line_num); - - patch->delta->similarity = 100 - dissimilarity; - - return 0; -} - -typedef struct { - const char *str; - int (*fn)(git_patch *, patch_parse_ctx *); -} header_git_op; - -static const header_git_op header_git_ops[] = { - { "@@ -", NULL }, - { "GIT binary patch", NULL }, - { "--- ", parse_header_git_oldpath }, - { "+++ ", parse_header_git_newpath }, - { "index ", parse_header_git_index }, - { "old mode ", parse_header_git_oldmode }, - { "new mode ", parse_header_git_newmode }, - { "deleted file mode ", parse_header_git_deletedfilemode }, - { "new file mode ", parse_header_git_newfilemode }, - { "rename from ", parse_header_renamefrom }, - { "rename to ", parse_header_renameto }, - { "rename old ", parse_header_renamefrom }, - { "rename new ", parse_header_renameto }, - { "similarity index ", parse_header_similarity }, - { "dissimilarity index ", parse_header_dissimilarity }, -}; - -static int parse_header_git( +int git_patch_get_hunk( + const git_diff_hunk **out, + size_t *lines_in_hunk, git_patch *patch, - patch_parse_ctx *ctx) + size_t hunk_idx) { - size_t i; - int error = 0; - - /* Parse the diff --git line */ - if (parse_advance_expected(ctx, "diff --git ", 11) < 0) - return parse_err("corrupt git diff header at line %d", ctx->line_num); - - if (parse_header_path(&ctx->header_old_path, ctx) < 0) - return parse_err("corrupt old path in git diff header at line %d", - ctx->line_num); - - if (parse_advance_ws(ctx) < 0 || - parse_header_path(&ctx->header_new_path, ctx) < 0) - return parse_err("corrupt new path in git diff header at line %d", - ctx->line_num); - - /* Parse remaining header lines */ - for (parse_advance_line(ctx); ctx->remain > 0; parse_advance_line(ctx)) { - if (ctx->line_len == 0 || ctx->line[ctx->line_len - 1] != '\n') - break; - - for (i = 0; i < ARRAY_SIZE(header_git_ops); i++) { - const header_git_op *op = &header_git_ops[i]; - size_t op_len = strlen(op->str); - - if (memcmp(ctx->line, op->str, min(op_len, ctx->line_len)) != 0) - continue; + git_patch_hunk *hunk; + assert(patch); - /* Do not advance if this is the patch separator */ - if (op->fn == NULL) - goto done; + hunk = git_array_get(patch->hunks, hunk_idx); - parse_advance_chars(ctx, op_len); - - if ((error = op->fn(patch, ctx)) < 0) - goto done; - - parse_advance_ws(ctx); - parse_advance_expected(ctx, "\n", 1); - - if (ctx->line_len > 0) { - error = parse_err("trailing data at line %d", ctx->line_num); - goto done; - } - - break; - } + if (!hunk) { + if (out) *out = NULL; + if (lines_in_hunk) *lines_in_hunk = 0; + return patch_error_outofrange("hunk"); } -done: - return error; -} - -static int parse_number(git_off_t *out, patch_parse_ctx *ctx) -{ - const char *end; - int64_t num; - - if (!git__isdigit(ctx->line[0])) - return -1; - - if (git__strntol64(&num, ctx->line, ctx->line_len, &end, 10) < 0) - return -1; - - if (num < 0) - return -1; - - *out = num; - parse_advance_chars(ctx, (end - ctx->line)); - - return 0; -} - -static int parse_int(int *out, patch_parse_ctx *ctx) -{ - git_off_t num; - - if (parse_number(&num, ctx) < 0 || !git__is_int(num)) - return -1; - - *out = (int)num; + if (out) *out = &hunk->hunk; + if (lines_in_hunk) *lines_in_hunk = hunk->line_count; return 0; } -static int parse_hunk_header( - diff_patch_hunk *hunk, - patch_parse_ctx *ctx) +int git_patch_num_lines_in_hunk(const git_patch *patch, size_t hunk_idx) { - const char *header_start = ctx->line; - - hunk->hunk.old_lines = 1; - hunk->hunk.new_lines = 1; - - if (parse_advance_expected(ctx, "@@ -", 4) < 0 || - parse_int(&hunk->hunk.old_start, ctx) < 0) - goto fail; - - if (ctx->line_len > 0 && ctx->line[0] == ',') { - if (parse_advance_expected(ctx, ",", 1) < 0 || - parse_int(&hunk->hunk.old_lines, ctx) < 0) - goto fail; - } - - if (parse_advance_expected(ctx, " +", 2) < 0 || - parse_int(&hunk->hunk.new_start, ctx) < 0) - goto fail; - - if (ctx->line_len > 0 && ctx->line[0] == ',') { - if (parse_advance_expected(ctx, ",", 1) < 0 || - parse_int(&hunk->hunk.new_lines, ctx) < 0) - goto fail; - } - - if (parse_advance_expected(ctx, " @@", 3) < 0) - goto fail; - - parse_advance_line(ctx); - - if (!hunk->hunk.old_lines && !hunk->hunk.new_lines) - goto fail; - - hunk->hunk.header_len = ctx->line - header_start; - if (hunk->hunk.header_len > (GIT_DIFF_HUNK_HEADER_SIZE - 1)) - return parse_err("oversized patch hunk header at line %d", - ctx->line_num); - - memcpy(hunk->hunk.header, header_start, hunk->hunk.header_len); - hunk->hunk.header[hunk->hunk.header_len] = '\0'; - - return 0; + git_patch_hunk *hunk; + assert(patch); -fail: - giterr_set(GITERR_PATCH, "invalid patch hunk header at line %d", - ctx->line_num); - return -1; + if (!(hunk = git_array_get(patch->hunks, hunk_idx))) + return patch_error_outofrange("hunk"); + return (int)hunk->line_count; } -static int parse_hunk_body( +int git_patch_get_line_in_hunk( + const git_diff_line **out, git_patch *patch, - diff_patch_hunk *hunk, - patch_parse_ctx *ctx) + size_t hunk_idx, + size_t line_of_hunk) { + git_patch_hunk *hunk; git_diff_line *line; - int error = 0; - - int oldlines = hunk->hunk.old_lines; - int newlines = hunk->hunk.new_lines; - - for (; - ctx->remain > 4 && (oldlines || newlines) && - memcmp(ctx->line, "@@ -", 4) != 0; - parse_advance_line(ctx)) { - - int origin; - int prefix = 1; - - if (ctx->line_len == 0 || ctx->line[ctx->line_len - 1] != '\n') { - error = parse_err("invalid patch instruction at line %d", - ctx->line_num); - goto done; - } - - switch (ctx->line[0]) { - case '\n': - prefix = 0; - - case ' ': - origin = GIT_DIFF_LINE_CONTEXT; - oldlines--; - newlines--; - break; - - case '-': - origin = GIT_DIFF_LINE_DELETION; - oldlines--; - break; - - case '+': - origin = GIT_DIFF_LINE_ADDITION; - newlines--; - break; - - default: - error = parse_err("invalid patch hunk at line %d", ctx->line_num); - goto done; - } - - line = git_array_alloc(patch->lines); - GITERR_CHECK_ALLOC(line); - - memset(line, 0x0, sizeof(git_diff_line)); - - line->content = ctx->line + prefix; - line->content_len = ctx->line_len - prefix; - line->content_offset = ctx->content_len - ctx->remain; - line->origin = origin; - - hunk->line_count++; - } - - if (oldlines || newlines) { - error = parse_err( - "invalid patch hunk, expected %d old lines and %d new lines", - hunk->hunk.old_lines, hunk->hunk.new_lines); - goto done; - } - - /* Handle "\ No newline at end of file". Only expect the leading - * backslash, though, because the rest of the string could be - * localized. Because `diff` optimizes for the case where you - * want to apply the patch by hand. - */ - if (ctx->line_len >= 2 && memcmp(ctx->line, "\\ ", 2) == 0 && - git_array_size(patch->lines) > 0) { - - line = git_array_get(patch->lines, git_array_size(patch->lines)-1); - - if (line->content_len < 1) { - error = parse_err("cannot trim trailing newline of empty line"); - goto done; - } - - line->content_len--; - - parse_advance_line(ctx); - } - -done: - return error; -} - -static int parse_header_traditional(git_patch *patch, patch_parse_ctx *ctx) -{ - GIT_UNUSED(patch); - GIT_UNUSED(ctx); - - return 1; -} - -static int parse_patch_header( - git_patch *patch, - patch_parse_ctx *ctx) -{ - int error = 0; - - for (ctx->line = ctx->content; ctx->remain > 0; parse_advance_line(ctx)) { - /* This line is too short to be a patch header. */ - if (ctx->line_len < 6) - continue; - - /* This might be a hunk header without a patch header, provide a - * sensible error message. */ - if (memcmp(ctx->line, "@@ -", 4) == 0) { - size_t line_num = ctx->line_num; - diff_patch_hunk hunk; - - /* If this cannot be parsed as a hunk header, it's just leading - * noise, continue. - */ - if (parse_hunk_header(&hunk, ctx) < 0) { - giterr_clear(); - continue; - } - - error = parse_err("invalid hunk header outside patch at line %d", - line_num); - goto done; - } - - /* This buffer is too short to contain a patch. */ - if (ctx->remain < ctx->line_len + 6) - break; - - /* A proper git patch */ - if (ctx->line_len >= 11 && memcmp(ctx->line, "diff --git ", 11) == 0) { - if ((error = parse_header_git(patch, ctx)) < 0) - goto done; - - /* For modechange only patches, it does not include filenames; - * instead we need to use the paths in the diff --git header. - */ - if (!patch->ofile.file->path && !patch->nfile.file->path) { - if (!ctx->header_old_path || !ctx->header_new_path) { - error = parse_err("git diff header lacks old / new paths"); - goto done; - } - - patch->ofile.file->path = ctx->header_old_path; - ctx->header_old_path = NULL; - - patch->nfile.file->path = ctx->header_new_path; - ctx->header_new_path = NULL; - } - - goto done; - } - if ((error = parse_header_traditional(patch, ctx)) <= 0) - goto done; + assert(patch); - error = 0; - continue; + if (!(hunk = git_array_get(patch->hunks, hunk_idx))) { + if (out) *out = NULL; + return patch_error_outofrange("hunk"); } - error = parse_err("no header in patch file"); - -done: - return error; -} - -static int parse_patch_binary_side( - git_diff_binary_file *binary, - patch_parse_ctx *ctx) -{ - git_diff_binary_t type = GIT_DIFF_BINARY_NONE; - git_buf base85 = GIT_BUF_INIT, decoded = GIT_BUF_INIT; - git_off_t len; - int error = 0; - - if (ctx->line_len >= 8 && memcmp(ctx->line, "literal ", 8) == 0) { - type = GIT_DIFF_BINARY_LITERAL; - parse_advance_chars(ctx, 8); - } else if (ctx->line_len >= 6 && memcmp(ctx->line, "delta ", 6) == 0) { - type = GIT_DIFF_BINARY_DELTA; - parse_advance_chars(ctx, 6); - } else { - error = parse_err("unknown binary delta type at line %d", ctx->line_num); - goto done; - } - - if (parse_number(&len, ctx) < 0 || parse_advance_nl(ctx) < 0 || len < 0) { - error = parse_err("invalid binary size at line %d", ctx->line_num); - goto done; - } - - while (ctx->line_len) { - char c = ctx->line[0]; - size_t encoded_len, decoded_len = 0, decoded_orig = decoded.size; - - if (c == '\n') - break; - else if (c >= 'A' && c <= 'Z') - decoded_len = c - 'A' + 1; - else if (c >= 'a' && c <= 'z') - decoded_len = c - 'a' + 26 + 1; - - if (!decoded_len) { - error = parse_err("invalid binary length at line %d", ctx->line_num); - goto done; - } - - parse_advance_chars(ctx, 1); - - encoded_len = ((decoded_len / 4) + !!(decoded_len % 4)) * 5; - - if (encoded_len > ctx->line_len - 1) { - error = parse_err("truncated binary data at line %d", ctx->line_num); - goto done; - } - - if ((error = git_buf_decode_base85( - &decoded, ctx->line, encoded_len, decoded_len)) < 0) - goto done; - - if (decoded.size - decoded_orig != decoded_len) { - error = parse_err("truncated binary data at line %d", ctx->line_num); - goto done; - } - - parse_advance_chars(ctx, encoded_len); - - if (parse_advance_nl(ctx) < 0) { - error = parse_err("trailing data at line %d", ctx->line_num); - goto done; - } + if (line_of_hunk >= hunk->line_count || + !(line = git_array_get( + patch->lines, hunk->line_start + line_of_hunk))) { + if (out) *out = NULL; + return patch_error_outofrange("line"); } - binary->type = type; - binary->inflatedlen = (size_t)len; - binary->datalen = decoded.size; - binary->data = git_buf_detach(&decoded); - -done: - git_buf_free(&base85); - git_buf_free(&decoded); - return error; -} - -static int parse_patch_binary( - git_patch *patch, - patch_parse_ctx *ctx) -{ - int error; - - if (parse_advance_expected(ctx, "GIT binary patch", 16) < 0 || - parse_advance_nl(ctx) < 0) - return parse_err("corrupt git binary header at line %d", ctx->line_num); - - /* parse old->new binary diff */ - if ((error = parse_patch_binary_side(&patch->binary.new_file, ctx)) < 0) - return error; - - if (parse_advance_nl(ctx) < 0) - return parse_err("corrupt git binary separator at line %d", ctx->line_num); - - /* parse new->old binary diff */ - if ((error = parse_patch_binary_side(&patch->binary.old_file, ctx)) < 0) - return error; - - patch->delta->flags |= GIT_DIFF_FLAG_BINARY; + if (out) *out = line; return 0; } -static int parse_patch_hunks( - git_patch *patch, - patch_parse_ctx *ctx) -{ - diff_patch_hunk *hunk; - int error = 0; - - for (; ctx->line_len > 4 && memcmp(ctx->line, "@@ -", 4) == 0; ) { - - hunk = git_array_alloc(patch->hunks); - GITERR_CHECK_ALLOC(hunk); - - memset(hunk, 0, sizeof(diff_patch_hunk)); - - hunk->line_start = git_array_size(patch->lines); - hunk->line_count = 0; - - if ((error = parse_hunk_header(hunk, ctx)) < 0 || - (error = parse_hunk_body(patch, hunk, ctx)) < 0) - goto done; - } - -done: - return error; -} - -static int parse_patch_body(git_patch *patch, patch_parse_ctx *ctx) +static void git_patch__free(git_patch *patch) { - if (ctx->line_len >= 16 && memcmp(ctx->line, "GIT binary patch", 16) == 0) - return parse_patch_binary(patch, ctx); + git_array_clear(patch->lines); + git_array_clear(patch->hunks); - else if (ctx->line_len >= 4 && memcmp(ctx->line, "@@ -", 4) == 0) - return parse_patch_hunks(patch, ctx); + git__free((char *)patch->binary.old_file.data); + git__free((char *)patch->binary.new_file.data); - return 0; + if (patch->free_fn) + patch->free_fn(patch); } -static int check_patch(git_patch *patch) +void git_patch_free(git_patch *patch) { - if (!patch->ofile.file->path && patch->delta->status != GIT_DELTA_ADDED) - return parse_err("missing old file path"); - - if (!patch->nfile.file->path && patch->delta->status != GIT_DELTA_DELETED) - return parse_err("missing new file path"); - - if (patch->ofile.file->path && patch->nfile.file->path) { - if (!patch->nfile.file->mode) - patch->nfile.file->mode = patch->ofile.file->mode; - } - - if (patch->delta->status == GIT_DELTA_MODIFIED && - !(patch->delta->flags & GIT_DIFF_FLAG_BINARY) && - patch->nfile.file->mode == patch->ofile.file->mode && - git_array_size(patch->hunks) == 0) - return parse_err("patch with no hunks"); - - return 0; -} - -int git_patch_from_patchfile( - git_patch **out, - const char *content, - size_t content_len) -{ - patch_parse_ctx ctx = {0}; - git_patch *patch; - int error = 0; - - *out = NULL; - - patch = git__calloc(1, sizeof(git_patch)); - GITERR_CHECK_ALLOC(patch); - - patch->delta = git__calloc(1, sizeof(git_diff_delta)); - patch->ofile.file = git__calloc(1, sizeof(git_diff_file)); - patch->nfile.file = git__calloc(1, sizeof(git_diff_file)); - - patch->delta->status = GIT_DELTA_MODIFIED; - - ctx.content = content; - ctx.content_len = content_len; - ctx.remain = content_len; - - if ((error = parse_patch_header(patch, &ctx)) < 0 || - (error = parse_patch_body(patch, &ctx)) < 0 || - (error = check_patch(patch)) < 0) - goto done; - - *out = patch; - -done: - git__free(ctx.header_old_path); - git__free(ctx.header_new_path); - - return error; + if (patch) + GIT_REFCOUNT_DEC(patch, git_patch__free); } diff --git a/src/patch.h b/src/patch.h new file mode 100644 index 00000000000..ecab570d5f4 --- /dev/null +++ b/src/patch.h @@ -0,0 +1,57 @@ +/* +* 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_patch_h__ +#define INCLUDE_patch_h__ + +#include "git2/patch.h" +#include "array.h" + +/* cached information about a hunk in a patch */ +typedef struct git_patch_hunk { + git_diff_hunk hunk; + size_t line_start; + size_t line_count; +} git_patch_hunk; + +struct git_patch { + git_refcount rc; + + git_repository *repo; /* may be null */ + + git_diff_options diff_opts; + + git_diff_delta *delta; + git_diff_binary binary; + git_array_t(git_patch_hunk) hunks; + git_array_t(git_diff_line) lines; + + size_t header_size; + size_t content_size; + size_t context_size; + + const git_diff_file *(*newfile)(git_patch *patch); + const git_diff_file *(*oldfile)(git_patch *patch); + void (*free_fn)(git_patch *patch); +}; + +extern int git_patch__invoke_callbacks( + git_patch *patch, + git_diff_file_cb file_cb, + git_diff_binary_cb binary_cb, + git_diff_hunk_cb hunk_cb, + git_diff_line_cb line_cb, + void *payload); + +extern int git_patch_line_stats( + size_t *total_ctxt, + size_t *total_adds, + size_t *total_dels, + const git_patch *patch); + +extern void git_patch_free(git_patch *patch); + +#endif diff --git a/src/diff_patch.c b/src/patch_diff.c similarity index 65% rename from src/diff_patch.c rename to src/patch_diff.c index 20a84388f29..0e06cd6ebff 100644 --- a/src/diff_patch.c +++ b/src/patch_diff.c @@ -9,47 +9,82 @@ #include "diff.h" #include "diff_file.h" #include "diff_driver.h" -#include "diff_patch.h" +#include "patch_diff.h" #include "diff_xdiff.h" #include "delta.h" #include "zstream.h" #include "fileops.h" static void diff_output_init( - git_diff_output*, const git_diff_options*, git_diff_file_cb, + git_patch_diff_output *, const git_diff_options *, git_diff_file_cb, git_diff_binary_cb, git_diff_hunk_cb, git_diff_line_cb, void*); -static void diff_output_to_patch(git_diff_output *, git_patch *); +static void diff_output_to_patch(git_patch_diff_output *, git_patch_diff *); -static void diff_patch_update_binary(git_patch *patch) +static const git_diff_file *patch_diff_newfile(git_patch *p) { - if ((patch->delta->flags & DIFF_FLAGS_KNOWN_BINARY) != 0) + git_patch_diff *patch = (git_patch_diff *)p; + return patch->nfile.file; +} + +static const git_diff_file *patch_diff_oldfile(git_patch *p) +{ + git_patch_diff *patch = (git_patch_diff *)p; + return patch->ofile.file; +} + +static void patch_diff_free(git_patch *p) +{ + git_patch_diff *patch = (git_patch_diff *)p; + + git_diff_file_content__clear(&patch->ofile); + git_diff_file_content__clear(&patch->nfile); + + git_diff_free(patch->diff); /* decrements refcount */ + patch->diff = NULL; + + git_pool_clear(&patch->flattened); + + git__free((char *)patch->base.diff_opts.old_prefix); + git__free((char *)patch->base.diff_opts.new_prefix); + + if (patch->flags & GIT_PATCH_DIFF_ALLOCATED) + git__free(patch); +} + +static void patch_diff_update_binary(git_patch_diff *patch) +{ + if ((patch->base.delta->flags & DIFF_FLAGS_KNOWN_BINARY) != 0) return; if ((patch->ofile.file->flags & GIT_DIFF_FLAG_BINARY) != 0 || (patch->nfile.file->flags & GIT_DIFF_FLAG_BINARY) != 0) - patch->delta->flags |= GIT_DIFF_FLAG_BINARY; + patch->base.delta->flags |= GIT_DIFF_FLAG_BINARY; else if (patch->ofile.file->size > GIT_XDIFF_MAX_SIZE || - patch->nfile.file->size > GIT_XDIFF_MAX_SIZE) - patch->delta->flags |= GIT_DIFF_FLAG_BINARY; + patch->nfile.file->size > GIT_XDIFF_MAX_SIZE) + patch->base.delta->flags |= GIT_DIFF_FLAG_BINARY; else if ((patch->ofile.file->flags & DIFF_FLAGS_NOT_BINARY) != 0 && - (patch->nfile.file->flags & DIFF_FLAGS_NOT_BINARY) != 0) - patch->delta->flags |= GIT_DIFF_FLAG_NOT_BINARY; + (patch->nfile.file->flags & DIFF_FLAGS_NOT_BINARY) != 0) + patch->base.delta->flags |= GIT_DIFF_FLAG_NOT_BINARY; } -static void diff_patch_init_common(git_patch *patch) +static void patch_diff_init_common(git_patch_diff *patch) { - diff_patch_update_binary(patch); + patch->base.newfile = patch_diff_newfile; + patch->base.oldfile = patch_diff_oldfile; + patch->base.free_fn = patch_diff_free; - patch->flags |= GIT_DIFF_PATCH_INITIALIZED; + patch_diff_update_binary(patch); + + patch->flags |= GIT_PATCH_DIFF_INITIALIZED; if (patch->diff) git_diff_addref(patch->diff); } -static int diff_patch_normalize_options( +static int patch_diff_normalize_options( git_diff_options *out, const git_diff_options *opts) { @@ -75,38 +110,40 @@ static int diff_patch_normalize_options( return 0; } -static int diff_patch_init_from_diff( - git_patch *patch, git_diff *diff, size_t delta_index) +static int patch_diff_init( + git_patch_diff *patch, git_diff *diff, size_t delta_index) { int error = 0; memset(patch, 0, sizeof(*patch)); - patch->diff = diff; - patch->delta = git_vector_get(&diff->deltas, delta_index); + + patch->diff = diff; + patch->base.repo = diff->repo; + patch->base.delta = git_vector_get(&diff->deltas, delta_index); patch->delta_index = delta_index; - if ((error = diff_patch_normalize_options( - &patch->diff_opts, &diff->opts)) < 0 || + if ((error = patch_diff_normalize_options( + &patch->base.diff_opts, &diff->opts)) < 0 || (error = git_diff_file_content__init_from_diff( - &patch->ofile, diff, patch->delta, true)) < 0 || + &patch->ofile, diff, patch->base.delta, true)) < 0 || (error = git_diff_file_content__init_from_diff( - &patch->nfile, diff, patch->delta, false)) < 0) + &patch->nfile, diff, patch->base.delta, false)) < 0) return error; - diff_patch_init_common(patch); + patch_diff_init_common(patch); return 0; } -static int diff_patch_alloc_from_diff( - git_patch **out, git_diff *diff, size_t delta_index) +static int patch_diff_alloc_from_diff( + git_patch_diff **out, git_diff *diff, size_t delta_index) { int error; - git_patch *patch = git__calloc(1, sizeof(git_patch)); + git_patch_diff *patch = git__calloc(1, sizeof(git_patch_diff)); GITERR_CHECK_ALLOC(patch); - if (!(error = diff_patch_init_from_diff(patch, diff, delta_index))) { - patch->flags |= GIT_DIFF_PATCH_ALLOCATED; + if (!(error = patch_diff_init(patch, diff, delta_index))) { + patch->flags |= GIT_PATCH_DIFF_ALLOCATED; GIT_REFCOUNT_INC(patch); } else { git__free(patch); @@ -117,27 +154,27 @@ static int diff_patch_alloc_from_diff( return error; } -GIT_INLINE(bool) should_skip_binary(git_patch *patch, git_diff_file *file) +GIT_INLINE(bool) should_skip_binary(git_patch_diff *patch, git_diff_file *file) { - if ((patch->diff_opts.flags & GIT_DIFF_SHOW_BINARY) != 0) + if ((patch->base.diff_opts.flags & GIT_DIFF_SHOW_BINARY) != 0) return false; return (file->flags & GIT_DIFF_FLAG_BINARY) != 0; } -static bool diff_patch_diffable(git_patch *patch) +static bool patch_diff_diffable(git_patch_diff *patch) { size_t olen, nlen; - if (patch->delta->status == GIT_DELTA_UNMODIFIED) + if (patch->base.delta->status == GIT_DELTA_UNMODIFIED) return false; /* if we've determined this to be binary (and we are not showing binary * data) then we have skipped loading the map data. instead, query the * file data itself. */ - if ((patch->delta->flags & GIT_DIFF_FLAG_BINARY) != 0 && - (patch->diff_opts.flags & GIT_DIFF_SHOW_BINARY) == 0) { + if ((patch->base.delta->flags & GIT_DIFF_FLAG_BINARY) != 0 && + (patch->base.diff_opts.flags & GIT_DIFF_SHOW_BINARY) == 0) { olen = (size_t)patch->ofile.file->size; nlen = (size_t)patch->nfile.file->size; } else { @@ -154,12 +191,12 @@ static bool diff_patch_diffable(git_patch *patch) !git_oid_equal(&patch->ofile.file->id, &patch->nfile.file->id)); } -static int diff_patch_load(git_patch *patch, git_diff_output *output) +static int patch_diff_load(git_patch_diff *patch, git_patch_diff_output *output) { int error = 0; bool incomplete_data; - if ((patch->flags & GIT_DIFF_PATCH_LOADED) != 0) + if ((patch->flags & GIT_PATCH_DIFF_LOADED) != 0) return 0; /* if no hunk and data callbacks and user doesn't care if data looks @@ -180,13 +217,13 @@ static int diff_patch_load(git_patch *patch, git_diff_output *output) */ if (patch->ofile.src == GIT_ITERATOR_TYPE_WORKDIR) { if ((error = git_diff_file_content__load( - &patch->ofile, &patch->diff_opts)) < 0 || + &patch->ofile, &patch->base.diff_opts)) < 0 || should_skip_binary(patch, patch->ofile.file)) goto cleanup; } if (patch->nfile.src == GIT_ITERATOR_TYPE_WORKDIR) { if ((error = git_diff_file_content__load( - &patch->nfile, &patch->diff_opts)) < 0 || + &patch->nfile, &patch->base.diff_opts)) < 0 || should_skip_binary(patch, patch->nfile.file)) goto cleanup; } @@ -194,13 +231,13 @@ static int diff_patch_load(git_patch *patch, git_diff_output *output) /* once workdir has been tried, load other data as needed */ if (patch->ofile.src != GIT_ITERATOR_TYPE_WORKDIR) { if ((error = git_diff_file_content__load( - &patch->ofile, &patch->diff_opts)) < 0 || + &patch->ofile, &patch->base.diff_opts)) < 0 || should_skip_binary(patch, patch->ofile.file)) goto cleanup; } if (patch->nfile.src != GIT_ITERATOR_TYPE_WORKDIR) { if ((error = git_diff_file_content__load( - &patch->nfile, &patch->diff_opts)) < 0 || + &patch->nfile, &patch->base.diff_opts)) < 0 || should_skip_binary(patch, patch->nfile.file)) goto cleanup; } @@ -212,24 +249,24 @@ static int diff_patch_load(git_patch *patch, git_diff_output *output) patch->ofile.file->mode == patch->nfile.file->mode && patch->ofile.file->mode != GIT_FILEMODE_COMMIT && git_oid_equal(&patch->ofile.file->id, &patch->nfile.file->id) && - patch->delta->status == GIT_DELTA_MODIFIED) /* not RENAMED/COPIED! */ - patch->delta->status = GIT_DELTA_UNMODIFIED; + patch->base.delta->status == GIT_DELTA_MODIFIED) /* not RENAMED/COPIED! */ + patch->base.delta->status = GIT_DELTA_UNMODIFIED; cleanup: - diff_patch_update_binary(patch); + patch_diff_update_binary(patch); if (!error) { - if (diff_patch_diffable(patch)) - patch->flags |= GIT_DIFF_PATCH_DIFFABLE; + if (patch_diff_diffable(patch)) + patch->flags |= GIT_PATCH_DIFF_DIFFABLE; - patch->flags |= GIT_DIFF_PATCH_LOADED; + patch->flags |= GIT_PATCH_DIFF_LOADED; } return error; } -static int diff_patch_invoke_file_callback( - git_patch *patch, git_diff_output *output) +static int patch_diff_invoke_file_callback( + git_patch_diff *patch, git_patch_diff_output *output) { float progress = patch->diff ? ((float)patch->delta_index / patch->diff->deltas.length) : 1.0f; @@ -238,7 +275,7 @@ static int diff_patch_invoke_file_callback( return 0; return giterr_set_after_callback_function( - output->file_cb(patch->delta, progress, output->payload), + output->file_cb(patch->base.delta, progress, output->payload), "git_patch"); } @@ -309,7 +346,7 @@ static int create_binary( return error; } -static int diff_binary(git_diff_output *output, git_patch *patch) +static int diff_binary(git_patch_diff_output *output, git_patch_diff *patch) { git_diff_binary binary = {{0}}; const char *old_data = patch->ofile.map.data; @@ -334,7 +371,7 @@ static int diff_binary(git_diff_output *output, git_patch *patch) return error; error = giterr_set_after_callback_function( - output->binary_cb(patch->delta, &binary, output->payload), + output->binary_cb(patch->base.delta, &binary, output->payload), "git_patch"); git__free((char *) binary.old_file.data); @@ -343,25 +380,25 @@ static int diff_binary(git_diff_output *output, git_patch *patch) return error; } -static int diff_patch_generate(git_patch *patch, git_diff_output *output) +static int patch_diff_generate(git_patch_diff *patch, git_patch_diff_output *output) { int error = 0; - if ((patch->flags & GIT_DIFF_PATCH_DIFFED) != 0) + if ((patch->flags & GIT_PATCH_DIFF_DIFFED) != 0) return 0; /* if we are not looking at the binary or text data, don't do the diff */ if (!output->binary_cb && !output->hunk_cb && !output->data_cb) return 0; - if ((patch->flags & GIT_DIFF_PATCH_LOADED) == 0 && - (error = diff_patch_load(patch, output)) < 0) + if ((patch->flags & GIT_PATCH_DIFF_LOADED) == 0 && + (error = patch_diff_load(patch, output)) < 0) return error; - if ((patch->flags & GIT_DIFF_PATCH_DIFFABLE) == 0) + if ((patch->flags & GIT_PATCH_DIFF_DIFFABLE) == 0) return 0; - if ((patch->delta->flags & GIT_DIFF_FLAG_BINARY) != 0) { + if ((patch->base.delta->flags & GIT_DIFF_FLAG_BINARY) != 0) { if (output->binary_cb) error = diff_binary(output, patch); } @@ -370,33 +407,10 @@ static int diff_patch_generate(git_patch *patch, git_diff_output *output) error = output->diff_cb(output, patch); } - patch->flags |= GIT_DIFF_PATCH_DIFFED; + patch->flags |= GIT_PATCH_DIFF_DIFFED; return error; } -static void diff_patch_free(git_patch *patch) -{ - git_diff_file_content__clear(&patch->ofile); - git_diff_file_content__clear(&patch->nfile); - - git_array_clear(patch->lines); - git_array_clear(patch->hunks); - - git_diff_free(patch->diff); /* decrements refcount */ - patch->diff = NULL; - - git_pool_clear(&patch->flattened); - - git__free((char *)patch->diff_opts.old_prefix); - git__free((char *)patch->diff_opts.new_prefix); - - git__free((char *)patch->binary.old_file.data); - git__free((char *)patch->binary.new_file.data); - - if (patch->flags & GIT_DIFF_PATCH_ALLOCATED) - git__free(patch); -} - static int diff_required(git_diff *diff, const char *action) { if (diff) @@ -416,7 +430,7 @@ int git_diff_foreach( int error = 0; git_xdiff_output xo; size_t idx; - git_patch patch; + git_patch_diff patch; if ((error = diff_required(diff, "git_diff_foreach")) < 0) return error; @@ -427,24 +441,24 @@ int git_diff_foreach( &xo.output, &diff->opts, file_cb, binary_cb, hunk_cb, data_cb, payload); git_xdiff_init(&xo, &diff->opts); - git_vector_foreach(&diff->deltas, idx, patch.delta) { + git_vector_foreach(&diff->deltas, idx, patch.base.delta) { /* check flags against patch status */ - if (git_diff_delta__should_skip(&diff->opts, patch.delta)) + if (git_diff_delta__should_skip(&diff->opts, patch.base.delta)) continue; if (binary_cb || hunk_cb || data_cb) { - if ((error = diff_patch_init_from_diff(&patch, diff, idx)) != 0 || - (error = diff_patch_load(&patch, &xo.output)) != 0) + if ((error = patch_diff_init(&patch, diff, idx)) != 0 || + (error = patch_diff_load(&patch, &xo.output)) != 0) return error; } - if ((error = diff_patch_invoke_file_callback(&patch, &xo.output)) == 0) { + if ((error = patch_diff_invoke_file_callback(&patch, &xo.output)) == 0) { if (binary_cb || hunk_cb || data_cb) - error = diff_patch_generate(&patch, &xo.output); + error = patch_diff_generate(&patch, &xo.output); } - git_patch_free(&patch); + git_patch_free(&patch.base); if (error) break; @@ -454,15 +468,15 @@ int git_diff_foreach( } typedef struct { - git_patch patch; + git_patch_diff patch; git_diff_delta delta; char paths[GIT_FLEX_ARRAY]; -} diff_patch_with_delta; +} patch_diff_with_delta; -static int diff_single_generate(diff_patch_with_delta *pd, git_xdiff_output *xo) +static int diff_single_generate(patch_diff_with_delta *pd, git_xdiff_output *xo) { int error = 0; - git_patch *patch = &pd->patch; + git_patch_diff *patch = &pd->patch; bool has_old = ((patch->ofile.flags & GIT_DIFF_FLAG__NO_DATA) == 0); bool has_new = ((patch->nfile.flags & GIT_DIFF_FLAG__NO_DATA) == 0); @@ -473,24 +487,24 @@ static int diff_single_generate(diff_patch_with_delta *pd, git_xdiff_output *xo) if (git_oid_equal(&patch->nfile.file->id, &patch->ofile.file->id)) pd->delta.status = GIT_DELTA_UNMODIFIED; - patch->delta = &pd->delta; + patch->base.delta = &pd->delta; - diff_patch_init_common(patch); + patch_diff_init_common(patch); if (pd->delta.status == GIT_DELTA_UNMODIFIED && !(patch->ofile.opts_flags & GIT_DIFF_INCLUDE_UNMODIFIED)) return error; - error = diff_patch_invoke_file_callback(patch, (git_diff_output *)xo); + error = patch_diff_invoke_file_callback(patch, (git_patch_diff_output *)xo); if (!error) - error = diff_patch_generate(patch, (git_diff_output *)xo); + error = patch_diff_generate(patch, (git_patch_diff_output *)xo); return error; } -static int diff_patch_from_sources( - diff_patch_with_delta *pd, +static int patch_diff_from_sources( + patch_diff_with_delta *pd, git_xdiff_output *xo, git_diff_file_content_src *oldsrc, git_diff_file_content_src *newsrc, @@ -503,7 +517,7 @@ static int diff_patch_from_sources( git_diff_file *lfile = &pd->delta.old_file, *rfile = &pd->delta.new_file; git_diff_file_content *ldata = &pd->patch.ofile, *rdata = &pd->patch.nfile; - if ((error = diff_patch_normalize_options(&pd->patch.diff_opts, opts)) < 0) + if ((error = patch_diff_normalize_options(&pd->patch.base.diff_opts, opts)) < 0) return error; if (opts && (opts->flags & GIT_DIFF_REVERSE) != 0) { @@ -511,7 +525,7 @@ static int diff_patch_from_sources( tmp = ldata; ldata = rdata; rdata = tmp; } - pd->patch.delta = &pd->delta; + pd->patch.base.delta = &pd->delta; if (!oldsrc->as_path) { if (newsrc->as_path) @@ -534,12 +548,12 @@ static int diff_patch_from_sources( return diff_single_generate(pd, xo); } -static int diff_patch_with_delta_alloc( - diff_patch_with_delta **out, +static int patch_diff_with_delta_alloc( + patch_diff_with_delta **out, const char **old_path, const char **new_path) { - diff_patch_with_delta *pd; + patch_diff_with_delta *pd; size_t old_len = *old_path ? strlen(*old_path) : 0; size_t new_len = *new_path ? strlen(*new_path) : 0; size_t alloc_len; @@ -551,7 +565,7 @@ static int diff_patch_with_delta_alloc( *out = pd = git__calloc(1, alloc_len); GITERR_CHECK_ALLOC(pd); - pd->patch.flags = GIT_DIFF_PATCH_ALLOCATED; + pd->patch.flags = GIT_PATCH_DIFF_ALLOCATED; if (*old_path) { memcpy(&pd->paths[0], *old_path, old_len); @@ -579,7 +593,7 @@ static int diff_from_sources( void *payload) { int error = 0; - diff_patch_with_delta pd; + patch_diff_with_delta pd; git_xdiff_output xo; memset(&xo, 0, sizeof(xo)); @@ -589,9 +603,9 @@ static int diff_from_sources( memset(&pd, 0, sizeof(pd)); - error = diff_patch_from_sources(&pd, &xo, oldsrc, newsrc, opts); + error = patch_diff_from_sources(&pd, &xo, oldsrc, newsrc, opts); - git_patch_free(&pd.patch); + git_patch_free(&pd.patch.base); return error; } @@ -603,13 +617,13 @@ static int patch_from_sources( const git_diff_options *opts) { int error = 0; - diff_patch_with_delta *pd; + patch_diff_with_delta *pd; git_xdiff_output xo; assert(out); *out = NULL; - if ((error = diff_patch_with_delta_alloc( + if ((error = patch_diff_with_delta_alloc( &pd, &oldsrc->as_path, &newsrc->as_path)) < 0) return error; @@ -617,7 +631,7 @@ static int patch_from_sources( diff_output_to_patch(&xo.output, &pd->patch); git_xdiff_init(&xo, opts); - if (!(error = diff_patch_from_sources(pd, &xo, oldsrc, newsrc, opts))) + if (!(error = patch_diff_from_sources(pd, &xo, oldsrc, newsrc, opts))) *out = (git_patch *)pd; else git_patch_free((git_patch *)pd); @@ -742,7 +756,7 @@ int git_patch_from_diff( int error = 0; git_xdiff_output xo; git_diff_delta *delta = NULL; - git_patch *patch = NULL; + git_patch_diff *patch = NULL; if (patch_ptr) *patch_ptr = NULL; @@ -764,17 +778,17 @@ int git_patch_from_diff( (diff->opts.flags & GIT_DIFF_SKIP_BINARY_CHECK) != 0)) return 0; - if ((error = diff_patch_alloc_from_diff(&patch, diff, idx)) < 0) + if ((error = patch_diff_alloc_from_diff(&patch, diff, idx)) < 0) return error; memset(&xo, 0, sizeof(xo)); diff_output_to_patch(&xo.output, patch); git_xdiff_init(&xo, &diff->opts); - error = diff_patch_invoke_file_callback(patch, &xo.output); + error = patch_diff_invoke_file_callback(patch, &xo.output); if (!error) - error = diff_patch_generate(patch, &xo.output); + error = patch_diff_generate(patch, &xo.output); if (!error) { /* TODO: if cumulative diff size is < 0.5 total size, flatten patch */ @@ -782,237 +796,34 @@ int git_patch_from_diff( } if (error || !patch_ptr) - git_patch_free(patch); + git_patch_free(&patch->base); else - *patch_ptr = patch; + *patch_ptr = &patch->base; return error; } -void git_patch_free(git_patch *patch) -{ - if (patch) - GIT_REFCOUNT_DEC(patch, diff_patch_free); -} - -const git_diff_delta *git_patch_get_delta(const git_patch *patch) -{ - assert(patch); - return patch->delta; -} - -size_t git_patch_num_hunks(const git_patch *patch) -{ - assert(patch); - return git_array_size(patch->hunks); -} - -int git_patch_line_stats( - size_t *total_ctxt, - size_t *total_adds, - size_t *total_dels, - const git_patch *patch) -{ - size_t totals[3], idx; - - memset(totals, 0, sizeof(totals)); - - for (idx = 0; idx < git_array_size(patch->lines); ++idx) { - git_diff_line *line = git_array_get(patch->lines, idx); - if (!line) - continue; - - switch (line->origin) { - case GIT_DIFF_LINE_CONTEXT: totals[0]++; break; - case GIT_DIFF_LINE_ADDITION: totals[1]++; break; - case GIT_DIFF_LINE_DELETION: totals[2]++; break; - default: - /* diff --stat and --numstat don't count EOFNL marks because - * they will always be paired with a ADDITION or DELETION line. - */ - break; - } - } - - if (total_ctxt) - *total_ctxt = totals[0]; - if (total_adds) - *total_adds = totals[1]; - if (total_dels) - *total_dels = totals[2]; - - return 0; -} - -static int diff_error_outofrange(const char *thing) -{ - giterr_set(GITERR_INVALID, "Diff patch %s index out of range", thing); - return GIT_ENOTFOUND; -} - -int git_patch_get_hunk( - const git_diff_hunk **out, - size_t *lines_in_hunk, - git_patch *patch, - size_t hunk_idx) -{ - diff_patch_hunk *hunk; - assert(patch); - - hunk = git_array_get(patch->hunks, hunk_idx); - - if (!hunk) { - if (out) *out = NULL; - if (lines_in_hunk) *lines_in_hunk = 0; - return diff_error_outofrange("hunk"); - } - - if (out) *out = &hunk->hunk; - if (lines_in_hunk) *lines_in_hunk = hunk->line_count; - return 0; -} - -int git_patch_num_lines_in_hunk(const git_patch *patch, size_t hunk_idx) -{ - diff_patch_hunk *hunk; - assert(patch); - - if (!(hunk = git_array_get(patch->hunks, hunk_idx))) - return diff_error_outofrange("hunk"); - return (int)hunk->line_count; -} - -int git_patch_get_line_in_hunk( - const git_diff_line **out, - git_patch *patch, - size_t hunk_idx, - size_t line_of_hunk) -{ - diff_patch_hunk *hunk; - git_diff_line *line; - - assert(patch); - - if (!(hunk = git_array_get(patch->hunks, hunk_idx))) { - if (out) *out = NULL; - return diff_error_outofrange("hunk"); - } - - if (line_of_hunk >= hunk->line_count || - !(line = git_array_get( - patch->lines, hunk->line_start + line_of_hunk))) { - if (out) *out = NULL; - return diff_error_outofrange("line"); - } - - if (out) *out = line; - return 0; -} - -size_t git_patch_size( - git_patch *patch, - int include_context, - int include_hunk_headers, - int include_file_headers) -{ - size_t out; - - assert(patch); - - out = patch->content_size; - - if (!include_context) - out -= patch->context_size; - - if (include_hunk_headers) - out += patch->header_size; - - if (include_file_headers) { - git_buf file_header = GIT_BUF_INIT; - - if (git_diff_delta__format_file_header( - &file_header, patch->delta, NULL, NULL, 0) < 0) - giterr_clear(); - else - out += git_buf_len(&file_header); - - git_buf_free(&file_header); - } - - return out; -} - -git_diff *git_patch__diff(git_patch *patch) -{ - return patch->diff; -} - -git_diff_driver *git_patch__driver(git_patch *patch) +git_diff_driver *git_patch_diff_driver(git_patch_diff *patch) { /* ofile driver is representative for whole patch */ return patch->ofile.driver; } -void git_patch__old_data( - char **ptr, size_t *len, git_patch *patch) +void git_patch_diff_old_data( + char **ptr, size_t *len, git_patch_diff *patch) { *ptr = patch->ofile.map.data; *len = patch->ofile.map.len; } -void git_patch__new_data( - char **ptr, size_t *len, git_patch *patch) +void git_patch_diff_new_data( + char **ptr, size_t *len, git_patch_diff *patch) { *ptr = patch->nfile.map.data; *len = patch->nfile.map.len; } -int git_patch__invoke_callbacks( - git_patch *patch, - git_diff_file_cb file_cb, - git_diff_binary_cb binary_cb, - git_diff_hunk_cb hunk_cb, - git_diff_line_cb line_cb, - void *payload) -{ - int error = 0; - uint32_t i, j; - - if (file_cb) - error = file_cb(patch->delta, 0, payload); - - if ((patch->delta->flags & GIT_DIFF_FLAG_BINARY) != 0) { - if (binary_cb) - error = binary_cb(patch->delta, &patch->binary, payload); - - return error; - } - - if (!hunk_cb && !line_cb) - return error; - - for (i = 0; !error && i < git_array_size(patch->hunks); ++i) { - diff_patch_hunk *h = git_array_get(patch->hunks, i); - - if (hunk_cb) - error = hunk_cb(patch->delta, &h->hunk, payload); - - if (!line_cb) - continue; - - for (j = 0; !error && j < h->line_count; ++j) { - git_diff_line *l = - git_array_get(patch->lines, h->line_start + j); - - error = line_cb(patch->delta, &h->hunk, l, payload); - } - } - - return error; -} - - -static int diff_patch_file_cb( +static int patch_diff_file_cb( const git_diff_delta *delta, float progress, void *payload) @@ -1021,7 +832,7 @@ static int diff_patch_file_cb( return 0; } -static int diff_patch_binary_cb( +static int patch_diff_binary_cb( const git_diff_delta *delta, const git_diff_binary *binary, void *payload) @@ -1051,62 +862,62 @@ static int diff_patch_binary_cb( return 0; } -static int diff_patch_hunk_cb( +static int git_patch_hunk_cb( const git_diff_delta *delta, const git_diff_hunk *hunk_, void *payload) { - git_patch *patch = payload; - diff_patch_hunk *hunk; + git_patch_diff *patch = payload; + git_patch_hunk *hunk; GIT_UNUSED(delta); - hunk = git_array_alloc(patch->hunks); + hunk = git_array_alloc(patch->base.hunks); GITERR_CHECK_ALLOC(hunk); memcpy(&hunk->hunk, hunk_, sizeof(hunk->hunk)); - patch->header_size += hunk_->header_len; + patch->base.header_size += hunk_->header_len; - hunk->line_start = git_array_size(patch->lines); + hunk->line_start = git_array_size(patch->base.lines); hunk->line_count = 0; return 0; } -static int diff_patch_line_cb( +static int patch_diff_line_cb( const git_diff_delta *delta, const git_diff_hunk *hunk_, const git_diff_line *line_, void *payload) { - git_patch *patch = payload; - diff_patch_hunk *hunk; + git_patch_diff *patch = payload; + git_patch_hunk *hunk; git_diff_line *line; GIT_UNUSED(delta); GIT_UNUSED(hunk_); - hunk = git_array_last(patch->hunks); + hunk = git_array_last(patch->base.hunks); assert(hunk); /* programmer error if no hunk is available */ - line = git_array_alloc(patch->lines); + line = git_array_alloc(patch->base.lines); GITERR_CHECK_ALLOC(line); memcpy(line, line_, sizeof(*line)); /* do some bookkeeping so we can provide old/new line numbers */ - patch->content_size += line->content_len; + patch->base.content_size += line->content_len; if (line->origin == GIT_DIFF_LINE_ADDITION || line->origin == GIT_DIFF_LINE_DELETION) - patch->content_size += 1; + patch->base.content_size += 1; else if (line->origin == GIT_DIFF_LINE_CONTEXT) { - patch->content_size += 1; - patch->context_size += line->content_len + 1; + patch->base.content_size += 1; + patch->base.context_size += line->content_len + 1; } else if (line->origin == GIT_DIFF_LINE_CONTEXT_EOFNL) - patch->context_size += line->content_len; + patch->base.context_size += line->content_len; hunk->line_count++; @@ -1114,7 +925,7 @@ static int diff_patch_line_cb( } static void diff_output_init( - git_diff_output *out, + git_patch_diff_output *out, const git_diff_options *opts, git_diff_file_cb file_cb, git_diff_binary_cb binary_cb, @@ -1133,14 +944,14 @@ static void diff_output_init( out->payload = payload; } -static void diff_output_to_patch(git_diff_output *out, git_patch *patch) +static void diff_output_to_patch(git_patch_diff_output *out, git_patch_diff *patch) { diff_output_init( out, NULL, - diff_patch_file_cb, - diff_patch_binary_cb, - diff_patch_hunk_cb, - diff_patch_line_cb, + patch_diff_file_cb, + patch_diff_binary_cb, + git_patch_hunk_cb, + patch_diff_line_cb, patch); } diff --git a/src/patch_diff.h b/src/patch_diff.h new file mode 100644 index 00000000000..076acdf4363 --- /dev/null +++ b/src/patch_diff.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_diff_patch_h__ +#define INCLUDE_diff_patch_h__ + +#include "common.h" +#include "diff.h" +#include "diff_file.h" +#include "patch.h" + +enum { + GIT_PATCH_DIFF_ALLOCATED = (1 << 0), + GIT_PATCH_DIFF_INITIALIZED = (1 << 1), + GIT_PATCH_DIFF_LOADED = (1 << 2), + /* the two sides are different */ + GIT_PATCH_DIFF_DIFFABLE = (1 << 3), + /* the difference between the two sides has been computed */ + GIT_PATCH_DIFF_DIFFED = (1 << 4), + GIT_PATCH_DIFF_FLATTENED = (1 << 5), +}; + +struct git_patch_diff { + struct git_patch base; + + git_diff *diff; /* for refcount purposes, maybe NULL for blob diffs */ + size_t delta_index; + git_diff_file_content ofile; + git_diff_file_content nfile; + uint32_t flags; + git_pool flattened; +}; + +typedef struct git_patch_diff git_patch_diff; + +extern git_diff_driver *git_patch_diff_driver(git_patch_diff *); + +extern void git_patch_diff_old_data(char **, size_t *, git_patch_diff *); +extern void git_patch_diff_new_data(char **, size_t *, git_patch_diff *); + +typedef struct git_patch_diff_output git_patch_diff_output; + +struct git_patch_diff_output { + /* these callbacks are issued with the diff data */ + git_diff_file_cb file_cb; + git_diff_binary_cb binary_cb; + git_diff_hunk_cb hunk_cb; + git_diff_line_cb data_cb; + void *payload; + + /* this records the actual error in cases where it may be obscured */ + int error; + + /* this callback is used to do the diff and drive the other callbacks. + * see diff_xdiff.h for how to use this in practice for now. + */ + int (*diff_cb)(git_patch_diff_output *output, git_patch_diff *patch); +}; + +#endif diff --git a/src/patch_parse.c b/src/patch_parse.c new file mode 100644 index 00000000000..e5019fce907 --- /dev/null +++ b/src/patch_parse.c @@ -0,0 +1,920 @@ +#include "git2/patch.h" +#include "patch.h" +#include "path.h" + +#define parse_err(...) \ + ( giterr_set(GITERR_PATCH, __VA_ARGS__), -1 ) + +typedef struct { + git_patch base; + + git_diff_file old_file; + git_diff_file new_file; +} git_patch_parsed; + +typedef struct { + const char *content; + size_t content_len; + + const char *line; + size_t line_len; + size_t line_num; + + size_t remain; + + /* TODO: move this into the parse struct? its lifecycle is odd... */ + char *header_new_path; + char *header_old_path; +} patch_parse_ctx; + + +static void parse_advance_line(patch_parse_ctx *ctx) +{ + ctx->line += ctx->line_len; + ctx->remain -= ctx->line_len; + ctx->line_len = git__linenlen(ctx->line, ctx->remain); + ctx->line_num++; +} + +static void parse_advance_chars(patch_parse_ctx *ctx, size_t char_cnt) +{ + ctx->line += char_cnt; + ctx->remain -= char_cnt; + ctx->line_len -= char_cnt; +} + +static int parse_advance_expected( + patch_parse_ctx *ctx, + const char *expected, + size_t expected_len) +{ + if (ctx->line_len < expected_len) + return -1; + + if (memcmp(ctx->line, expected, expected_len) != 0) + return -1; + + parse_advance_chars(ctx, expected_len); + return 0; +} + +static int parse_advance_ws(patch_parse_ctx *ctx) +{ + int ret = -1; + + while (ctx->line_len > 0 && + ctx->line[0] != '\n' && + git__isspace(ctx->line[0])) { + ctx->line++; + ctx->line_len--; + ctx->remain--; + ret = 0; + } + + return ret; +} + +static int parse_advance_nl(patch_parse_ctx *ctx) +{ + if (ctx->line_len != 1 || ctx->line[0] != '\n') + return -1; + + parse_advance_line(ctx); + return 0; +} + +static int header_path_len(patch_parse_ctx *ctx) +{ + bool inquote = 0; + bool quoted = (ctx->line_len > 0 && ctx->line[0] == '"'); + size_t len; + + for (len = quoted; len < ctx->line_len; len++) { + if (!quoted && git__isspace(ctx->line[len])) + break; + else if (quoted && !inquote && ctx->line[len] == '"') { + len++; + break; + } + + inquote = (!inquote && ctx->line[len] == '\\'); + } + + return len; +} + +static int parse_header_path_buf(git_buf *path, patch_parse_ctx *ctx) +{ + int path_len, error = 0; + + path_len = header_path_len(ctx); + + if ((error = git_buf_put(path, ctx->line, path_len)) < 0) + goto done; + + parse_advance_chars(ctx, path_len); + + git_buf_rtrim(path); + + if (path->size > 0 && path->ptr[0] == '"') + error = git_buf_unquote(path); + + if (error < 0) + goto done; + + git_path_squash_slashes(path); + +done: + return error; +} + +static int parse_header_path(char **out, patch_parse_ctx *ctx) +{ + git_buf path = GIT_BUF_INIT; + int error = parse_header_path_buf(&path, ctx); + + *out = git_buf_detach(&path); + + return error; +} + +static int parse_header_git_oldpath( + git_patch_parsed *patch, patch_parse_ctx *ctx) +{ + return parse_header_path((char **)&patch->old_file.path, ctx); +} + +static int parse_header_git_newpath( + git_patch_parsed *patch, patch_parse_ctx *ctx) +{ + return parse_header_path((char **)&patch->new_file.path, ctx); +} + +static int parse_header_mode(uint16_t *mode, patch_parse_ctx *ctx) +{ + const char *end; + int32_t m; + int ret; + + if (ctx->line_len < 1 || !git__isdigit(ctx->line[0])) + return parse_err("invalid file mode at line %d", ctx->line_num); + + if ((ret = git__strntol32(&m, ctx->line, ctx->line_len, &end, 8)) < 0) + return ret; + + if (m > UINT16_MAX) + return -1; + + *mode = (uint16_t)m; + + parse_advance_chars(ctx, (end - ctx->line)); + + return ret; +} + +static int parse_header_oid( + git_oid *oid, + size_t *oid_len, + patch_parse_ctx *ctx) +{ + size_t len; + + for (len = 0; len < ctx->line_len && len < GIT_OID_HEXSZ; len++) { + if (!git__isxdigit(ctx->line[len])) + break; + } + + if (len < GIT_OID_MINPREFIXLEN || + git_oid_fromstrn(oid, ctx->line, len) < 0) + return parse_err("invalid hex formatted object id at line %d", + ctx->line_num); + + parse_advance_chars(ctx, len); + + *oid_len = len; + + return 0; +} + +static int parse_header_git_index( + git_patch_parsed *patch, patch_parse_ctx *ctx) +{ + /* + * TODO: we read the prefix provided in the diff into the delta's id + * field, but do not mark is at an abbreviated id. + */ + size_t oid_len, nid_len; + + if (parse_header_oid(&patch->base.delta->old_file.id, &oid_len, ctx) < 0 || + parse_advance_expected(ctx, "..", 2) < 0 || + parse_header_oid(&patch->base.delta->new_file.id, &nid_len, ctx) < 0) + return -1; + + if (ctx->line_len > 0 && ctx->line[0] == ' ') { + uint16_t mode; + + parse_advance_chars(ctx, 1); + + if (parse_header_mode(&mode, ctx) < 0) + return -1; + + if (!patch->base.delta->new_file.mode) + patch->base.delta->new_file.mode = mode; + + if (!patch->base.delta->old_file.mode) + patch->base.delta->old_file.mode = mode; + } + + return 0; +} + +static int parse_header_git_oldmode( + git_patch_parsed *patch, patch_parse_ctx *ctx) +{ + return parse_header_mode(&patch->old_file.mode, ctx); +} + +static int parse_header_git_newmode( + git_patch_parsed *patch, patch_parse_ctx *ctx) +{ + return parse_header_mode(&patch->new_file.mode, ctx); +} + +static int parse_header_git_deletedfilemode( + git_patch_parsed *patch, + patch_parse_ctx *ctx) +{ + git__free((char *)patch->old_file.path); + + patch->old_file.path = NULL; + patch->base.delta->status = GIT_DELTA_DELETED; + + return parse_header_mode(&patch->old_file.mode, ctx); +} + +static int parse_header_git_newfilemode( + git_patch_parsed *patch, + patch_parse_ctx *ctx) +{ + git__free((char *)patch->new_file.path); + + patch->new_file.path = NULL; + patch->base.delta->status = GIT_DELTA_ADDED; + + return parse_header_mode(&patch->new_file.mode, ctx); +} + +static int parse_header_rename( + char **out, + char **header_path, + patch_parse_ctx *ctx) +{ + git_buf path = GIT_BUF_INIT; + size_t header_path_len, prefix_len; + + if (*header_path == NULL) + return parse_err("rename without proper git diff header at line %d", + ctx->line_num); + + header_path_len = strlen(*header_path); + + if (parse_header_path_buf(&path, ctx) < 0) + return -1; + + if (header_path_len < git_buf_len(&path)) + return parse_err("rename path is invalid at line %d", ctx->line_num); + + /* This sanity check exists because git core uses the data in the + * "rename from" / "rename to" lines, but it's formatted differently + * than the other paths and lacks the normal prefix. This irregularity + * causes us to ignore these paths (we always store the prefixed paths) + * but instead validate that they match the suffix of the paths we parsed + * since we would behave differently from git core if they ever differed. + * Instead, we raise an error, rather than parsing differently. + */ + prefix_len = header_path_len - path.size; + + if (strncmp(*header_path + prefix_len, path.ptr, path.size) != 0 || + (prefix_len > 0 && (*header_path)[prefix_len - 1] != '/')) + return parse_err("rename path does not match header at line %d", + ctx->line_num); + + *out = *header_path; + *header_path = NULL; + + git_buf_free(&path); + + return 0; +} + +static int parse_header_renamefrom( + git_patch_parsed *patch, patch_parse_ctx *ctx) +{ + patch->base.delta->status |= GIT_DELTA_RENAMED; + + return parse_header_rename( + (char **)&patch->old_file.path, + &ctx->header_old_path, + ctx); +} + +static int parse_header_renameto( + git_patch_parsed *patch, patch_parse_ctx *ctx) +{ + patch->base.delta->status |= GIT_DELTA_RENAMED; + + return parse_header_rename( + (char **)&patch->new_file.path, + &ctx->header_new_path, + ctx); +} + +static int parse_header_percent(uint16_t *out, patch_parse_ctx *ctx) +{ + int32_t val; + const char *end; + + if (ctx->line_len < 1 || !git__isdigit(ctx->line[0]) || + git__strntol32(&val, ctx->line, ctx->line_len, &end, 10) < 0) + return -1; + + parse_advance_chars(ctx, (end - ctx->line)); + + if (parse_advance_expected(ctx, "%", 1) < 0) + return -1; + + if (val > 100) + return -1; + + *out = val; + return 0; +} + +static int parse_header_similarity( + git_patch_parsed *patch, patch_parse_ctx *ctx) +{ + if (parse_header_percent(&patch->base.delta->similarity, ctx) < 0) + return parse_err("invalid similarity percentage at line %d", + ctx->line_num); + + return 0; +} + +static int parse_header_dissimilarity( + git_patch_parsed *patch, patch_parse_ctx *ctx) +{ + uint16_t dissimilarity; + + if (parse_header_percent(&dissimilarity, ctx) < 0) + return parse_err("invalid similarity percentage at line %d", + ctx->line_num); + + patch->base.delta->similarity = 100 - dissimilarity; + + return 0; +} + +typedef struct { + const char *str; + int(*fn)(git_patch_parsed *, patch_parse_ctx *); +} header_git_op; + +static const header_git_op header_git_ops[] = { + { "@@ -", NULL }, + { "GIT binary patch", NULL }, + { "--- ", parse_header_git_oldpath }, + { "+++ ", parse_header_git_newpath }, + { "index ", parse_header_git_index }, + { "old mode ", parse_header_git_oldmode }, + { "new mode ", parse_header_git_newmode }, + { "deleted file mode ", parse_header_git_deletedfilemode }, + { "new file mode ", parse_header_git_newfilemode }, + { "rename from ", parse_header_renamefrom }, + { "rename to ", parse_header_renameto }, + { "rename old ", parse_header_renamefrom }, + { "rename new ", parse_header_renameto }, + { "similarity index ", parse_header_similarity }, + { "dissimilarity index ", parse_header_dissimilarity }, +}; + +static int parse_header_git( + git_patch_parsed *patch, + patch_parse_ctx *ctx) +{ + size_t i; + int error = 0; + + /* Parse the diff --git line */ + if (parse_advance_expected(ctx, "diff --git ", 11) < 0) + return parse_err("corrupt git diff header at line %d", ctx->line_num); + + if (parse_header_path(&ctx->header_old_path, ctx) < 0) + return parse_err("corrupt old path in git diff header at line %d", + ctx->line_num); + + if (parse_advance_ws(ctx) < 0 || + parse_header_path(&ctx->header_new_path, ctx) < 0) + return parse_err("corrupt new path in git diff header at line %d", + ctx->line_num); + + /* Parse remaining header lines */ + for (parse_advance_line(ctx); ctx->remain > 0; parse_advance_line(ctx)) { + if (ctx->line_len == 0 || ctx->line[ctx->line_len - 1] != '\n') + break; + + for (i = 0; i < ARRAY_SIZE(header_git_ops); i++) { + const header_git_op *op = &header_git_ops[i]; + size_t op_len = strlen(op->str); + + if (memcmp(ctx->line, op->str, min(op_len, ctx->line_len)) != 0) + continue; + + /* Do not advance if this is the patch separator */ + if (op->fn == NULL) + goto done; + + parse_advance_chars(ctx, op_len); + + if ((error = op->fn(patch, ctx)) < 0) + goto done; + + parse_advance_ws(ctx); + parse_advance_expected(ctx, "\n", 1); + + if (ctx->line_len > 0) { + error = parse_err("trailing data at line %d", ctx->line_num); + goto done; + } + + break; + } + } + +done: + return error; +} + +static int parse_number(git_off_t *out, patch_parse_ctx *ctx) +{ + const char *end; + int64_t num; + + if (!git__isdigit(ctx->line[0])) + return -1; + + if (git__strntol64(&num, ctx->line, ctx->line_len, &end, 10) < 0) + return -1; + + if (num < 0) + return -1; + + *out = num; + parse_advance_chars(ctx, (end - ctx->line)); + + return 0; +} + +static int parse_int(int *out, patch_parse_ctx *ctx) +{ + git_off_t num; + + if (parse_number(&num, ctx) < 0 || !git__is_int(num)) + return -1; + + *out = (int)num; + return 0; +} + +static int parse_hunk_header( + git_patch_hunk *hunk, + patch_parse_ctx *ctx) +{ + const char *header_start = ctx->line; + + hunk->hunk.old_lines = 1; + hunk->hunk.new_lines = 1; + + if (parse_advance_expected(ctx, "@@ -", 4) < 0 || + parse_int(&hunk->hunk.old_start, ctx) < 0) + goto fail; + + if (ctx->line_len > 0 && ctx->line[0] == ',') { + if (parse_advance_expected(ctx, ",", 1) < 0 || + parse_int(&hunk->hunk.old_lines, ctx) < 0) + goto fail; + } + + if (parse_advance_expected(ctx, " +", 2) < 0 || + parse_int(&hunk->hunk.new_start, ctx) < 0) + goto fail; + + if (ctx->line_len > 0 && ctx->line[0] == ',') { + if (parse_advance_expected(ctx, ",", 1) < 0 || + parse_int(&hunk->hunk.new_lines, ctx) < 0) + goto fail; + } + + if (parse_advance_expected(ctx, " @@", 3) < 0) + goto fail; + + parse_advance_line(ctx); + + if (!hunk->hunk.old_lines && !hunk->hunk.new_lines) + goto fail; + + hunk->hunk.header_len = ctx->line - header_start; + if (hunk->hunk.header_len > (GIT_DIFF_HUNK_HEADER_SIZE - 1)) + return parse_err("oversized patch hunk header at line %d", + ctx->line_num); + + memcpy(hunk->hunk.header, header_start, hunk->hunk.header_len); + hunk->hunk.header[hunk->hunk.header_len] = '\0'; + + return 0; + +fail: + giterr_set(GITERR_PATCH, "invalid patch hunk header at line %d", + ctx->line_num); + return -1; +} + +static int parse_hunk_body( + git_patch_parsed *patch, + git_patch_hunk *hunk, + patch_parse_ctx *ctx) +{ + git_diff_line *line; + int error = 0; + + int oldlines = hunk->hunk.old_lines; + int newlines = hunk->hunk.new_lines; + + for (; + ctx->remain > 4 && (oldlines || newlines) && + memcmp(ctx->line, "@@ -", 4) != 0; + parse_advance_line(ctx)) { + + int origin; + int prefix = 1; + + if (ctx->line_len == 0 || ctx->line[ctx->line_len - 1] != '\n') { + error = parse_err("invalid patch instruction at line %d", + ctx->line_num); + goto done; + } + + switch (ctx->line[0]) { + case '\n': + prefix = 0; + + case ' ': + origin = GIT_DIFF_LINE_CONTEXT; + oldlines--; + newlines--; + break; + + case '-': + origin = GIT_DIFF_LINE_DELETION; + oldlines--; + break; + + case '+': + origin = GIT_DIFF_LINE_ADDITION; + newlines--; + break; + + default: + error = parse_err("invalid patch hunk at line %d", ctx->line_num); + goto done; + } + + line = git_array_alloc(patch->base.lines); + GITERR_CHECK_ALLOC(line); + + memset(line, 0x0, sizeof(git_diff_line)); + + line->content = ctx->line + prefix; + line->content_len = ctx->line_len - prefix; + line->content_offset = ctx->content_len - ctx->remain; + line->origin = origin; + + hunk->line_count++; + } + + if (oldlines || newlines) { + error = parse_err( + "invalid patch hunk, expected %d old lines and %d new lines", + hunk->hunk.old_lines, hunk->hunk.new_lines); + goto done; + } + + /* Handle "\ No newline at end of file". Only expect the leading + * backslash, though, because the rest of the string could be + * localized. Because `diff` optimizes for the case where you + * want to apply the patch by hand. + */ + if (ctx->line_len >= 2 && memcmp(ctx->line, "\\ ", 2) == 0 && + git_array_size(patch->base.lines) > 0) { + + line = git_array_get(patch->base.lines, git_array_size(patch->base.lines) - 1); + + if (line->content_len < 1) { + error = parse_err("cannot trim trailing newline of empty line"); + goto done; + } + + line->content_len--; + + parse_advance_line(ctx); + } + +done: + return error; +} + +static int parsed_patch_header( + git_patch_parsed *patch, + patch_parse_ctx *ctx) +{ + int error = 0; + + for (ctx->line = ctx->content; ctx->remain > 0; parse_advance_line(ctx)) { + /* This line is too short to be a patch header. */ + if (ctx->line_len < 6) + continue; + + /* This might be a hunk header without a patch header, provide a + * sensible error message. */ + if (memcmp(ctx->line, "@@ -", 4) == 0) { + size_t line_num = ctx->line_num; + git_patch_hunk hunk; + + /* If this cannot be parsed as a hunk header, it's just leading + * noise, continue. + */ + if (parse_hunk_header(&hunk, ctx) < 0) { + giterr_clear(); + continue; + } + + error = parse_err("invalid hunk header outside patch at line %d", + line_num); + goto done; + } + + /* This buffer is too short to contain a patch. */ + if (ctx->remain < ctx->line_len + 6) + break; + + /* A proper git patch */ + if (ctx->line_len >= 11 && memcmp(ctx->line, "diff --git ", 11) == 0) { + if ((error = parse_header_git(patch, ctx)) < 0) + goto done; + + /* For modechange only patches, it does not include filenames; + * instead we need to use the paths in the diff --git header. + */ + if (!patch->old_file.path && !patch->new_file.path) { + if (!ctx->header_old_path || !ctx->header_new_path) { + error = parse_err("git diff header lacks old / new paths"); + goto done; + } + + patch->old_file.path = ctx->header_old_path; + ctx->header_old_path = NULL; + + patch->new_file.path = ctx->header_new_path; + ctx->header_new_path = NULL; + } + + goto done; + } + + error = 0; + continue; + } + + error = parse_err("no header in patch file"); + +done: + return error; +} + +static int parsed_patch_binary_side( + git_diff_binary_file *binary, + patch_parse_ctx *ctx) +{ + git_diff_binary_t type = GIT_DIFF_BINARY_NONE; + git_buf base85 = GIT_BUF_INIT, decoded = GIT_BUF_INIT; + git_off_t len; + int error = 0; + + if (ctx->line_len >= 8 && memcmp(ctx->line, "literal ", 8) == 0) { + type = GIT_DIFF_BINARY_LITERAL; + parse_advance_chars(ctx, 8); + } + else if (ctx->line_len >= 6 && memcmp(ctx->line, "delta ", 6) == 0) { + type = GIT_DIFF_BINARY_DELTA; + parse_advance_chars(ctx, 6); + } + else { + error = parse_err("unknown binary delta type at line %d", ctx->line_num); + goto done; + } + + if (parse_number(&len, ctx) < 0 || parse_advance_nl(ctx) < 0 || len < 0) { + error = parse_err("invalid binary size at line %d", ctx->line_num); + goto done; + } + + while (ctx->line_len) { + char c = ctx->line[0]; + size_t encoded_len, decoded_len = 0, decoded_orig = decoded.size; + + if (c == '\n') + break; + else if (c >= 'A' && c <= 'Z') + decoded_len = c - 'A' + 1; + else if (c >= 'a' && c <= 'z') + decoded_len = c - 'a' + (('z' - 'a') + 1) + 1; + + if (!decoded_len) { + error = parse_err("invalid binary length at line %d", ctx->line_num); + goto done; + } + + parse_advance_chars(ctx, 1); + + encoded_len = ((decoded_len / 4) + !!(decoded_len % 4)) * 5; + + if (encoded_len > ctx->line_len - 1) { + error = parse_err("truncated binary data at line %d", ctx->line_num); + goto done; + } + + if ((error = git_buf_decode_base85( + &decoded, ctx->line, encoded_len, decoded_len)) < 0) + goto done; + + if (decoded.size - decoded_orig != decoded_len) { + error = parse_err("truncated binary data at line %d", ctx->line_num); + goto done; + } + + parse_advance_chars(ctx, encoded_len); + + if (parse_advance_nl(ctx) < 0) { + error = parse_err("trailing data at line %d", ctx->line_num); + goto done; + } + } + + binary->type = type; + binary->inflatedlen = (size_t)len; + binary->datalen = decoded.size; + binary->data = git_buf_detach(&decoded); + +done: + git_buf_free(&base85); + git_buf_free(&decoded); + return error; +} + +static int parsed_patch_binary( + git_patch_parsed *patch, + patch_parse_ctx *ctx) +{ + int error; + + if (parse_advance_expected(ctx, "GIT binary patch", 16) < 0 || + parse_advance_nl(ctx) < 0) + return parse_err("corrupt git binary header at line %d", ctx->line_num); + + /* parse old->new binary diff */ + if ((error = parsed_patch_binary_side( + &patch->base.binary.new_file, ctx)) < 0) + return error; + + if (parse_advance_nl(ctx) < 0) + return parse_err("corrupt git binary separator at line %d", + ctx->line_num); + + /* parse new->old binary diff */ + if ((error = parsed_patch_binary_side( + &patch->base.binary.old_file, ctx)) < 0) + return error; + + patch->base.delta->flags |= GIT_DIFF_FLAG_BINARY; + return 0; +} + +static int parsed_patch_hunks( + git_patch_parsed *patch, + patch_parse_ctx *ctx) +{ + git_patch_hunk *hunk; + int error = 0; + + for (; ctx->line_len > 4 && memcmp(ctx->line, "@@ -", 4) == 0; ) { + + hunk = git_array_alloc(patch->base.hunks); + GITERR_CHECK_ALLOC(hunk); + + memset(hunk, 0, sizeof(git_patch_hunk)); + + hunk->line_start = git_array_size(patch->base.lines); + hunk->line_count = 0; + + if ((error = parse_hunk_header(hunk, ctx)) < 0 || + (error = parse_hunk_body(patch, hunk, ctx)) < 0) + goto done; + } + +done: + return error; +} + +static int parsed_patch_body( + git_patch_parsed *patch, patch_parse_ctx *ctx) +{ + if (ctx->line_len >= 16 && memcmp(ctx->line, "GIT binary patch", 16) == 0) + return parsed_patch_binary(patch, ctx); + + else if (ctx->line_len >= 4 && memcmp(ctx->line, "@@ -", 4) == 0) + return parsed_patch_hunks(patch, ctx); + + return 0; +} + +static int check_patch(git_patch_parsed *patch) +{ + if (!patch->old_file.path && patch->base.delta->status != GIT_DELTA_ADDED) + return parse_err("missing old file path"); + + if (!patch->new_file.path && patch->base.delta->status != GIT_DELTA_DELETED) + return parse_err("missing new file path"); + + if (patch->old_file.path && patch->new_file.path) { + if (!patch->new_file.mode) + patch->new_file.mode = patch->old_file.mode; + } + + if (patch->base.delta->status == GIT_DELTA_MODIFIED && + !(patch->base.delta->flags & GIT_DIFF_FLAG_BINARY) && + patch->new_file.mode == patch->old_file.mode && + git_array_size(patch->base.hunks) == 0) + return parse_err("patch with no hunks"); + + return 0; +} + +static const git_diff_file *parsed_patch_newfile(git_patch *p) +{ + git_patch_parsed *patch = (git_patch_parsed *)p; + return &patch->new_file; +} + +static const git_diff_file *parsed_patch_oldfile(git_patch *p) +{ + git_patch_parsed *patch = (git_patch_parsed *)p; + return &patch->old_file; +} + +int git_patch_from_patchfile( + git_patch **out, + const char *content, + size_t content_len) +{ + patch_parse_ctx ctx = { 0 }; + git_patch_parsed *patch; + int error = 0; + + *out = NULL; + + patch = git__calloc(1, sizeof(git_patch_parsed)); + GITERR_CHECK_ALLOC(patch); + + patch->base.newfile = parsed_patch_newfile; + patch->base.oldfile = parsed_patch_oldfile; + + patch->base.delta = git__calloc(1, sizeof(git_diff_delta)); + patch->base.delta->status = GIT_DELTA_MODIFIED; + + ctx.content = content; + ctx.content_len = content_len; + ctx.remain = content_len; + + if ((error = parsed_patch_header(patch, &ctx)) < 0 || + (error = parsed_patch_body(patch, &ctx)) < 0 || + (error = check_patch(patch)) < 0) + goto done; + + GIT_REFCOUNT_INC(patch); + *out = &patch->base; + +done: + git__free(ctx.header_old_path); + git__free(ctx.header_new_path); + + return error; +} diff --git a/tests/apply/fromfile.c b/tests/apply/fromfile.c index 1e21659199a..14b5e9a9e5e 100644 --- a/tests/apply/fromfile.c +++ b/tests/apply/fromfile.c @@ -106,6 +106,7 @@ void test_apply_fromfile__change_middle_nocontext(void) &diff_opts, "b/file.txt", 0100644)); } + void test_apply_fromfile__change_firstline(void) { cl_git_pass(validate_and_apply_patchfile( From b85bd8ce66f271e281434ba4328a342877e0a23b Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 16 Sep 2015 11:37:03 -0400 Subject: [PATCH 13/54] patch: use delta's old_file/new_file members No need to replicate the old_file/new_file members, or plumb them strangely up. --- src/apply.c | 2 +- src/patch.h | 2 -- src/patch_diff.c | 14 ---------- src/patch_parse.c | 65 +++++++++++++++++++---------------------------- 4 files changed, 27 insertions(+), 56 deletions(-) diff --git a/src/apply.c b/src/apply.c index 875f3d0421a..a453d3dbbb8 100644 --- a/src/apply.c +++ b/src/apply.c @@ -340,7 +340,7 @@ int git_apply__patch( *mode_out = 0; if (patch->delta->status != GIT_DELTA_DELETED) { - const git_diff_file *newfile = patch->newfile(patch); + const git_diff_file *newfile = &patch->delta->new_file; filename = git__strdup(newfile->path); mode = newfile->mode ? diff --git a/src/patch.h b/src/patch.h index ecab570d5f4..b818c5cbe72 100644 --- a/src/patch.h +++ b/src/patch.h @@ -33,8 +33,6 @@ struct git_patch { size_t content_size; size_t context_size; - const git_diff_file *(*newfile)(git_patch *patch); - const git_diff_file *(*oldfile)(git_patch *patch); void (*free_fn)(git_patch *patch); }; diff --git a/src/patch_diff.c b/src/patch_diff.c index 0e06cd6ebff..1a3aeda5e07 100644 --- a/src/patch_diff.c +++ b/src/patch_diff.c @@ -21,18 +21,6 @@ static void diff_output_init( static void diff_output_to_patch(git_patch_diff_output *, git_patch_diff *); -static const git_diff_file *patch_diff_newfile(git_patch *p) -{ - git_patch_diff *patch = (git_patch_diff *)p; - return patch->nfile.file; -} - -static const git_diff_file *patch_diff_oldfile(git_patch *p) -{ - git_patch_diff *patch = (git_patch_diff *)p; - return patch->ofile.file; -} - static void patch_diff_free(git_patch *p) { git_patch_diff *patch = (git_patch_diff *)p; @@ -72,8 +60,6 @@ static void patch_diff_update_binary(git_patch_diff *patch) static void patch_diff_init_common(git_patch_diff *patch) { - patch->base.newfile = patch_diff_newfile; - patch->base.oldfile = patch_diff_oldfile; patch->base.free_fn = patch_diff_free; patch_diff_update_binary(patch); diff --git a/src/patch_parse.c b/src/patch_parse.c index e5019fce907..2c16e649774 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -5,11 +5,9 @@ #define parse_err(...) \ ( giterr_set(GITERR_PATCH, __VA_ARGS__), -1 ) +/* TODO: remove this, just use git_patch */ typedef struct { git_patch base; - - git_diff_file old_file; - git_diff_file new_file; } git_patch_parsed; typedef struct { @@ -141,13 +139,13 @@ static int parse_header_path(char **out, patch_parse_ctx *ctx) static int parse_header_git_oldpath( git_patch_parsed *patch, patch_parse_ctx *ctx) { - return parse_header_path((char **)&patch->old_file.path, ctx); + return parse_header_path((char **)&patch->base.delta->old_file.path, ctx); } static int parse_header_git_newpath( git_patch_parsed *patch, patch_parse_ctx *ctx) { - return parse_header_path((char **)&patch->new_file.path, ctx); + return parse_header_path((char **)&patch->base.delta->new_file.path, ctx); } static int parse_header_mode(uint16_t *mode, patch_parse_ctx *ctx) @@ -231,37 +229,37 @@ static int parse_header_git_index( static int parse_header_git_oldmode( git_patch_parsed *patch, patch_parse_ctx *ctx) { - return parse_header_mode(&patch->old_file.mode, ctx); + return parse_header_mode(&patch->base.delta->old_file.mode, ctx); } static int parse_header_git_newmode( git_patch_parsed *patch, patch_parse_ctx *ctx) { - return parse_header_mode(&patch->new_file.mode, ctx); + return parse_header_mode(&patch->base.delta->new_file.mode, ctx); } static int parse_header_git_deletedfilemode( git_patch_parsed *patch, patch_parse_ctx *ctx) { - git__free((char *)patch->old_file.path); + git__free((char *)patch->base.delta->old_file.path); - patch->old_file.path = NULL; + patch->base.delta->old_file.path = NULL; patch->base.delta->status = GIT_DELTA_DELETED; - return parse_header_mode(&patch->old_file.mode, ctx); + return parse_header_mode(&patch->base.delta->old_file.mode, ctx); } static int parse_header_git_newfilemode( git_patch_parsed *patch, patch_parse_ctx *ctx) { - git__free((char *)patch->new_file.path); + git__free((char *)patch->base.delta->new_file.path); - patch->new_file.path = NULL; + patch->base.delta->new_file.path = NULL; patch->base.delta->status = GIT_DELTA_ADDED; - return parse_header_mode(&patch->new_file.mode, ctx); + return parse_header_mode(&patch->base.delta->new_file.mode, ctx); } static int parse_header_rename( @@ -313,7 +311,7 @@ static int parse_header_renamefrom( patch->base.delta->status |= GIT_DELTA_RENAMED; return parse_header_rename( - (char **)&patch->old_file.path, + (char **)&patch->base.delta->old_file.path, &ctx->header_old_path, ctx); } @@ -324,7 +322,7 @@ static int parse_header_renameto( patch->base.delta->status |= GIT_DELTA_RENAMED; return parse_header_rename( - (char **)&patch->new_file.path, + (char **)&patch->base.delta->new_file.path, &ctx->header_new_path, ctx); } @@ -674,16 +672,18 @@ static int parsed_patch_header( /* For modechange only patches, it does not include filenames; * instead we need to use the paths in the diff --git header. */ - if (!patch->old_file.path && !patch->new_file.path) { + if (!patch->base.delta->old_file.path && + !patch->base.delta->new_file.path) { + if (!ctx->header_old_path || !ctx->header_new_path) { error = parse_err("git diff header lacks old / new paths"); goto done; } - patch->old_file.path = ctx->header_old_path; + patch->base.delta->old_file.path = ctx->header_old_path; ctx->header_old_path = NULL; - patch->new_file.path = ctx->header_new_path; + patch->base.delta->new_file.path = ctx->header_new_path; ctx->header_new_path = NULL; } @@ -848,38 +848,28 @@ static int parsed_patch_body( static int check_patch(git_patch_parsed *patch) { - if (!patch->old_file.path && patch->base.delta->status != GIT_DELTA_ADDED) + if (!patch->base.delta->old_file.path && + patch->base.delta->status != GIT_DELTA_ADDED) return parse_err("missing old file path"); - if (!patch->new_file.path && patch->base.delta->status != GIT_DELTA_DELETED) + if (!patch->base.delta->new_file.path && + patch->base.delta->status != GIT_DELTA_DELETED) return parse_err("missing new file path"); - if (patch->old_file.path && patch->new_file.path) { - if (!patch->new_file.mode) - patch->new_file.mode = patch->old_file.mode; + if (patch->base.delta->old_file.path && patch->base.delta->new_file.path) { + if (!patch->base.delta->new_file.mode) + patch->base.delta->new_file.mode = patch->base.delta->old_file.mode; } if (patch->base.delta->status == GIT_DELTA_MODIFIED && !(patch->base.delta->flags & GIT_DIFF_FLAG_BINARY) && - patch->new_file.mode == patch->old_file.mode && + patch->base.delta->new_file.mode == patch->base.delta->old_file.mode && git_array_size(patch->base.hunks) == 0) return parse_err("patch with no hunks"); return 0; } -static const git_diff_file *parsed_patch_newfile(git_patch *p) -{ - git_patch_parsed *patch = (git_patch_parsed *)p; - return &patch->new_file; -} - -static const git_diff_file *parsed_patch_oldfile(git_patch *p) -{ - git_patch_parsed *patch = (git_patch_parsed *)p; - return &patch->old_file; -} - int git_patch_from_patchfile( git_patch **out, const char *content, @@ -894,9 +884,6 @@ int git_patch_from_patchfile( patch = git__calloc(1, sizeof(git_patch_parsed)); GITERR_CHECK_ALLOC(patch); - patch->base.newfile = parsed_patch_newfile; - patch->base.oldfile = parsed_patch_oldfile; - patch->base.delta = git__calloc(1, sizeof(git_diff_delta)); patch->base.delta->status = GIT_DELTA_MODIFIED; From 8bca8b9e0379910686937380e5ac459c51a4864f Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 16 Sep 2015 14:40:44 -0400 Subject: [PATCH 14/54] apply: move patch data to patch_common.h --- tests/apply/fromdiff.c | 2 +- tests/apply/fromfile.c | 2 +- tests/{apply/apply_common.h => patch/patch_common.h} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename tests/{apply/apply_common.h => patch/patch_common.h} (100%) diff --git a/tests/apply/fromdiff.c b/tests/apply/fromdiff.c index ae37d0719e1..3497739649c 100644 --- a/tests/apply/fromdiff.c +++ b/tests/apply/fromdiff.c @@ -5,7 +5,7 @@ #include "repository.h" #include "buf_text.h" -#include "apply_common.h" +#include "../patch/patch_common.h" static git_repository *repo = NULL; static git_diff_options binary_opts = GIT_DIFF_OPTIONS_INIT; diff --git a/tests/apply/fromfile.c b/tests/apply/fromfile.c index 14b5e9a9e5e..acb635962ab 100644 --- a/tests/apply/fromfile.c +++ b/tests/apply/fromfile.c @@ -5,7 +5,7 @@ #include "repository.h" #include "buf_text.h" -#include "apply_common.h" +#include "../patch/patch_common.h" static git_repository *repo = NULL; diff --git a/tests/apply/apply_common.h b/tests/patch/patch_common.h similarity index 100% rename from tests/apply/apply_common.h rename to tests/patch/patch_common.h From e7ec327d4b94d8237f6238fb3d282bd3434b2b56 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Tue, 22 Sep 2015 17:56:42 -0400 Subject: [PATCH 15/54] patch parse: unset path prefix --- src/patch_parse.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/patch_parse.c b/src/patch_parse.c index 2c16e649774..11e26936c67 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -884,6 +884,10 @@ int git_patch_from_patchfile( patch = git__calloc(1, sizeof(git_patch_parsed)); GITERR_CHECK_ALLOC(patch); + /* TODO: allow callers to specify prefix depth (eg, `-p2`) */ + patch->base.diff_opts.new_prefix = ""; + patch->base.diff_opts.old_prefix = ""; + patch->base.delta = git__calloc(1, sizeof(git_diff_delta)); patch->base.delta->status = GIT_DELTA_MODIFIED; From d68cb736776e0f2f9494b49e2da30a9c4b9fc2c7 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Tue, 22 Sep 2015 18:25:03 -0400 Subject: [PATCH 16/54] diff: include oid length in deltas Now that `git_diff_delta` data can be produced by reading patch file data, which may have an abbreviated oid, allow consumers to know that the id is abbreviated. --- include/git2/diff.h | 8 +++++++- src/diff.c | 4 ++++ src/diff_file.c | 2 ++ src/diff_print.c | 27 +++++++++++++++++++++++++++ src/patch_parse.c | 12 ++++-------- 5 files changed, 44 insertions(+), 9 deletions(-) diff --git a/include/git2/diff.h b/include/git2/diff.h index f3bb337b7c1..065a786e920 100644 --- a/include/git2/diff.h +++ b/include/git2/diff.h @@ -264,10 +264,15 @@ typedef enum { * link, a submodule commit id, or even a tree (although that only if you * are tracking type changes or ignored/untracked directories). * - * The `oid` is the `git_oid` of the item. If the entry represents an + * The `id` is the `git_oid` of the item. If the entry represents an * absent side of a diff (e.g. the `old_file` of a `GIT_DELTA_ADDED` delta), * then the oid will be zeroes. * + * The `id_abbrev` represents the known length of the `id` field, when + * converted to a hex string. It is generally `GIT_OID_HEXSZ`, unless this + * delta was created from reading a patch file, in which case it may be + * abbreviated to something reasonable, like 7 characters. + * * `path` is the NUL-terminated path to the entry relative to the working * directory of the repository. * @@ -280,6 +285,7 @@ typedef enum { */ typedef struct { git_oid id; + int id_abbrev; const char *path; git_off_t size; uint32_t flags; diff --git a/src/diff.c b/src/diff.c index 26c0b895b53..9c27511f6a1 100644 --- a/src/diff.c +++ b/src/diff.c @@ -151,11 +151,13 @@ static int diff_delta__from_one( delta->old_file.size = entry->file_size; delta->old_file.flags |= GIT_DIFF_FLAG_EXISTS; git_oid_cpy(&delta->old_file.id, &entry->id); + delta->old_file.id_abbrev = GIT_OID_HEXSZ; } else /* ADDED, IGNORED, UNTRACKED */ { delta->new_file.mode = entry->mode; delta->new_file.size = entry->file_size; delta->new_file.flags |= GIT_DIFF_FLAG_EXISTS; git_oid_cpy(&delta->new_file.id, &entry->id); + delta->new_file.id_abbrev = GIT_OID_HEXSZ; } delta->old_file.flags |= GIT_DIFF_FLAG_VALID_ID; @@ -208,12 +210,14 @@ static int diff_delta__from_two( delta->old_file.size = old_entry->file_size; delta->old_file.mode = old_mode; git_oid_cpy(&delta->old_file.id, old_id); + delta->old_file.id_abbrev = GIT_OID_HEXSZ; delta->old_file.flags |= GIT_DIFF_FLAG_VALID_ID | GIT_DIFF_FLAG_EXISTS; } if (!git_index_entry_is_conflict(new_entry)) { git_oid_cpy(&delta->new_file.id, new_id); + delta->new_file.id_abbrev = GIT_OID_HEXSZ; delta->new_file.size = new_entry->file_size; delta->new_file.mode = new_mode; delta->old_file.flags |= GIT_DIFF_FLAG_EXISTS; diff --git a/src/diff_file.c b/src/diff_file.c index ecc34cf5517..8b945a5b79b 100644 --- a/src/diff_file.c +++ b/src/diff_file.c @@ -149,12 +149,14 @@ int git_diff_file_content__init_from_src( if (src->blob) { fc->file->size = git_blob_rawsize(src->blob); git_oid_cpy(&fc->file->id, git_blob_id(src->blob)); + fc->file->id_abbrev = GIT_OID_HEXSZ; fc->map.len = (size_t)fc->file->size; fc->map.data = (char *)git_blob_rawcontent(src->blob); } else { fc->file->size = src->buflen; git_odb_hash(&fc->file->id, src->buf, src->buflen, GIT_OBJ_BLOB); + fc->file->id_abbrev = GIT_OID_HEXSZ; fc->map.len = src->buflen; fc->map.data = (char *)src->buf; diff --git a/src/diff_print.c b/src/diff_print.c index 09bf77aef8e..7c9af244940 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -208,6 +208,7 @@ static int diff_print_one_raw( { diff_print_info *pi = data; git_buf *out = pi->buf; + int id_abbrev; char code = git_diff_status_char(delta->status); char start_oid[GIT_OID_HEXSZ+1], end_oid[GIT_OID_HEXSZ+1]; @@ -218,6 +219,16 @@ static int diff_print_one_raw( git_buf_clear(out); + id_abbrev = delta->old_file.mode ? delta->old_file.id_abbrev : + delta->new_file.id_abbrev; + + if (pi->oid_strlen - 1 > id_abbrev) { + giterr_set(GITERR_PATCH, + "The patch input contains %d id characters (cannot print %d)", + id_abbrev, pi->oid_strlen); + return -1; + } + git_oid_tostr(start_oid, pi->oid_strlen, &delta->old_file.id); git_oid_tostr(end_oid, pi->oid_strlen, &delta->new_file.id); @@ -252,6 +263,22 @@ static int diff_print_oid_range( { char start_oid[GIT_OID_HEXSZ+1], end_oid[GIT_OID_HEXSZ+1]; + if (delta->old_file.mode && + oid_strlen - 1 > delta->old_file.id_abbrev) { + giterr_set(GITERR_PATCH, + "The patch input contains %d id characters (cannot print %d)", + delta->old_file.id_abbrev, oid_strlen); + return -1; + } + + if ((delta->new_file.mode && + oid_strlen - 1 > delta->new_file.id_abbrev)) { + giterr_set(GITERR_PATCH, + "The patch input contains %d id characters (cannot print %d)", + delta->new_file.id_abbrev, oid_strlen); + return -1; + } + git_oid_tostr(start_oid, oid_strlen, &delta->old_file.id); git_oid_tostr(end_oid, oid_strlen, &delta->new_file.id); diff --git a/src/patch_parse.c b/src/patch_parse.c index 11e26936c67..323f8dc959f 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -197,15 +197,11 @@ static int parse_header_oid( static int parse_header_git_index( git_patch_parsed *patch, patch_parse_ctx *ctx) { - /* - * TODO: we read the prefix provided in the diff into the delta's id - * field, but do not mark is at an abbreviated id. - */ - size_t oid_len, nid_len; - - if (parse_header_oid(&patch->base.delta->old_file.id, &oid_len, ctx) < 0 || + if (parse_header_oid(&patch->base.delta->old_file.id, + &patch->base.delta->old_file.id_abbrev, ctx) < 0 || parse_advance_expected(ctx, "..", 2) < 0 || - parse_header_oid(&patch->base.delta->new_file.id, &nid_len, ctx) < 0) + parse_header_oid(&patch->base.delta->new_file.id, + &patch->base.delta->new_file.id_abbrev, ctx) < 0) return -1; if (ctx->line_len > 0 && ctx->line[0] == ' ') { From bc6a31c9fbc3fc48d6a44bb752afd43fcb60ebef Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Tue, 22 Sep 2015 18:29:14 -0400 Subject: [PATCH 17/54] patch: when parsing, set nfiles correctly in delta --- src/patch_parse.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/patch_parse.c b/src/patch_parse.c index 323f8dc959f..d32d351e8ed 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -242,6 +242,7 @@ static int parse_header_git_deletedfilemode( patch->base.delta->old_file.path = NULL; patch->base.delta->status = GIT_DELTA_DELETED; + patch->base.delta->nfiles = 1; return parse_header_mode(&patch->base.delta->old_file.mode, ctx); } @@ -254,6 +255,7 @@ static int parse_header_git_newfilemode( patch->base.delta->new_file.path = NULL; patch->base.delta->status = GIT_DELTA_ADDED; + patch->base.delta->nfiles = 1; return parse_header_mode(&patch->base.delta->new_file.mode, ctx); } @@ -886,6 +888,7 @@ int git_patch_from_patchfile( patch->base.delta = git__calloc(1, sizeof(git_diff_delta)); patch->base.delta->status = GIT_DELTA_MODIFIED; + patch->base.delta->nfiles = 2; ctx.content = content; ctx.content_len = content_len; From 42b3442823c45e83ce8eb67607e1b90c3ea7f3af Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Tue, 22 Sep 2015 18:54:10 -0400 Subject: [PATCH 18/54] patch_parse: ensure we can parse a patch --- tests/patch/parse.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/patch/parse.c diff --git a/tests/patch/parse.c b/tests/patch/parse.c new file mode 100644 index 00000000000..3191c8c3fd8 --- /dev/null +++ b/tests/patch/parse.c @@ -0,0 +1,31 @@ +#include "clar_libgit2.h" + +#include "patch_common.h" + +void test_patch_parse__original_to_change_middle(void) +{ + git_patch *patch; + const git_diff_delta *delta; + char idstr[GIT_OID_HEXSZ+1] = {0}; + + cl_git_pass(git_patch_from_patchfile(&patch, PATCH_ORIGINAL_TO_CHANGE_MIDDLE, strlen(PATCH_ORIGINAL_TO_CHANGE_MIDDLE))); + + cl_assert((delta = git_patch_get_delta(patch)) != NULL); + cl_assert_equal_i(2, delta->nfiles); + + cl_assert_equal_s(delta->old_file.path, "a/file.txt"); + cl_assert(delta->old_file.mode == GIT_FILEMODE_BLOB); + cl_assert_equal_i(7, delta->old_file.id_abbrev); + git_oid_nfmt(idstr, delta->old_file.id_abbrev, &delta->old_file.id); + cl_assert_equal_s(idstr, "9432026"); + cl_assert_equal_i(0, delta->old_file.size); + + cl_assert_equal_s(delta->new_file.path, "b/file.txt"); + cl_assert(delta->new_file.mode == GIT_FILEMODE_BLOB); + cl_assert_equal_i(7, delta->new_file.id_abbrev); + git_oid_nfmt(idstr, delta->new_file.id_abbrev, &delta->new_file.id); + cl_assert_equal_s(idstr, "cd8fd12"); + cl_assert_equal_i(0, delta->new_file.size); + + git_patch_free(patch); +} From 2f3b922ff1695f79b942d62ee8982a7d16ea7dfd Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Tue, 22 Sep 2015 18:54:10 -0400 Subject: [PATCH 19/54] patch_parse: test roundtrip patch parsing -> print --- tests/patch/print.c | 166 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 tests/patch/print.c diff --git a/tests/patch/print.c b/tests/patch/print.c new file mode 100644 index 00000000000..9bfc5f0eb71 --- /dev/null +++ b/tests/patch/print.c @@ -0,0 +1,166 @@ +#include "clar_libgit2.h" + +#include "patch_common.h" + + +/* sanity check the round-trip of patch parsing: ensure that we can parse + * and then print a variety of patch files. + */ + +void patch_print_from_patchfile(const char *data, size_t len) +{ + git_patch *patch; + git_buf buf = GIT_BUF_INIT; + + cl_git_pass(git_patch_from_patchfile(&patch, data, len)); + cl_git_pass(git_patch_to_buf(&buf, patch)); + + cl_assert_equal_s(data, buf.ptr); + + git_patch_free(patch); + git_buf_free(&buf); +} + +void test_patch_print__change_middle(void) +{ + patch_print_from_patchfile(PATCH_ORIGINAL_TO_CHANGE_MIDDLE, + strlen(PATCH_ORIGINAL_TO_CHANGE_MIDDLE)); +} + +void test_patch_print__change_middle_nocontext(void) +{ + patch_print_from_patchfile(PATCH_ORIGINAL_TO_CHANGE_MIDDLE_NOCONTEXT, + strlen(PATCH_ORIGINAL_TO_CHANGE_MIDDLE_NOCONTEXT)); +} + +void test_patch_print__change_firstline(void) +{ + patch_print_from_patchfile(PATCH_ORIGINAL_TO_CHANGE_FIRSTLINE, + strlen(PATCH_ORIGINAL_TO_CHANGE_FIRSTLINE)); +} + +void test_patch_print__change_lastline(void) +{ + patch_print_from_patchfile(PATCH_ORIGINAL_TO_CHANGE_LASTLINE, + strlen(PATCH_ORIGINAL_TO_CHANGE_LASTLINE)); +} + +void test_patch_print__prepend(void) +{ + patch_print_from_patchfile(PATCH_ORIGINAL_TO_PREPEND, + strlen(PATCH_ORIGINAL_TO_PREPEND)); +} + +void test_patch_print__prepend_nocontext(void) +{ + patch_print_from_patchfile(PATCH_ORIGINAL_TO_PREPEND_NOCONTEXT, + strlen(PATCH_ORIGINAL_TO_PREPEND_NOCONTEXT)); +} + +void test_patch_print__append(void) +{ + patch_print_from_patchfile(PATCH_ORIGINAL_TO_APPEND, + strlen(PATCH_ORIGINAL_TO_APPEND)); +} + +void test_patch_print__append_nocontext(void) +{ + patch_print_from_patchfile(PATCH_ORIGINAL_TO_APPEND_NOCONTEXT, + strlen(PATCH_ORIGINAL_TO_APPEND_NOCONTEXT)); +} + +void test_patch_print__prepend_and_append(void) +{ + patch_print_from_patchfile(PATCH_ORIGINAL_TO_PREPEND_AND_APPEND, + strlen(PATCH_ORIGINAL_TO_PREPEND_AND_APPEND)); +} + +void test_patch_print__to_empty_file(void) +{ + patch_print_from_patchfile(PATCH_ORIGINAL_TO_EMPTY_FILE, + strlen(PATCH_ORIGINAL_TO_EMPTY_FILE)); +} + +void test_patch_print__from_empty_file(void) +{ + patch_print_from_patchfile(PATCH_EMPTY_FILE_TO_ORIGINAL, + strlen(PATCH_EMPTY_FILE_TO_ORIGINAL)); +} + +void test_patch_print__add(void) +{ + patch_print_from_patchfile(PATCH_ADD_ORIGINAL, + strlen(PATCH_ADD_ORIGINAL)); +} + +void test_patch_print__delete(void) +{ + patch_print_from_patchfile(PATCH_DELETE_ORIGINAL, + strlen(PATCH_DELETE_ORIGINAL)); +} + +void test_patch_print__rename_exact(void) +{ + patch_print_from_patchfile(PATCH_RENAME_EXACT, + strlen(PATCH_RENAME_EXACT)); +} + +void test_patch_print__rename_similar(void) +{ + patch_print_from_patchfile(PATCH_RENAME_SIMILAR, + strlen(PATCH_RENAME_SIMILAR)); +} + +void test_patch_print__rename_exact_quotedname(void) +{ + patch_print_from_patchfile(PATCH_RENAME_EXACT_QUOTEDNAME, + strlen(PATCH_RENAME_EXACT_QUOTEDNAME)); +} + +void test_patch_print__rename_similar_quotedname(void) +{ + patch_print_from_patchfile(PATCH_RENAME_SIMILAR_QUOTEDNAME, + strlen(PATCH_RENAME_SIMILAR_QUOTEDNAME)); +} + +void test_patch_print__modechange_unchanged(void) +{ + patch_print_from_patchfile(PATCH_MODECHANGE_UNCHANGED, + strlen(PATCH_MODECHANGE_UNCHANGED)); +} + +void test_patch_print__modechange_modified(void) +{ + patch_print_from_patchfile(PATCH_MODECHANGE_MODIFIED, + strlen(PATCH_MODECHANGE_MODIFIED)); +} + +void test_patch_print__binary_literal(void) +{ + patch_print_from_patchfile(PATCH_BINARY_LITERAL, + strlen(PATCH_BINARY_LITERAL)); +} + +void test_patch_print__binary_delta(void) +{ + patch_print_from_patchfile(PATCH_BINARY_DELTA, + strlen(PATCH_BINARY_DELTA)); +} + +void test_patch_print__binary_add(void) +{ + patch_print_from_patchfile(PATCH_BINARY_ADD, + strlen(PATCH_BINARY_ADD)); +} + +void test_patch_print__binary_delete(void) +{ + patch_print_from_patchfile(PATCH_BINARY_DELETE, + strlen(PATCH_BINARY_DELETE)); +} + +void test_patch_print__not_reversible(void) +{ + patch_print_from_patchfile(PATCH_BINARY_NOT_REVERSIBLE, + strlen(PATCH_BINARY_NOT_REVERSIBLE)); +} From 1462c95a5d6d365e2f4fe686d186aecee8374b0c Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 23 Sep 2015 09:54:25 -0400 Subject: [PATCH 20/54] patch_parse: set binary flag We may have parsed binary data, set the `SHOW_BINARY` flag which indicates that we have actually computed a binary diff. --- src/patch_parse.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/patch_parse.c b/src/patch_parse.c index d32d351e8ed..a450ecc0544 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -885,6 +885,7 @@ int git_patch_from_patchfile( /* TODO: allow callers to specify prefix depth (eg, `-p2`) */ patch->base.diff_opts.new_prefix = ""; patch->base.diff_opts.old_prefix = ""; + patch->base.diff_opts.flags |= GIT_DIFF_SHOW_BINARY; patch->base.delta = git__calloc(1, sizeof(git_diff_delta)); patch->base.delta->status = GIT_DELTA_MODIFIED; From 28f704433b949bfd7fe43a15aab0023b114fe706 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 23 Sep 2015 10:38:51 -0400 Subject: [PATCH 21/54] patch_parse: use names from `diff --git` header When a text file is added or deleted, use the file names from the `diff --git` header instead of the `---` or `+++` lines. This is for compatibility with git. --- src/diff_print.c | 1 - src/patch_parse.c | 61 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/diff_print.c b/src/diff_print.c index 7c9af244940..29ffcdd5114 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -282,7 +282,6 @@ static int diff_print_oid_range( git_oid_tostr(start_oid, oid_strlen, &delta->old_file.id); git_oid_tostr(end_oid, oid_strlen, &delta->new_file.id); - /* TODO: Match git diff more closely */ if (delta->old_file.mode == delta->new_file.mode) { git_buf_printf(out, "index %s..%s %o\n", start_oid, end_oid, delta->old_file.mode); diff --git a/src/patch_parse.c b/src/patch_parse.c index a450ecc0544..f375688af40 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -628,6 +628,49 @@ static int parse_hunk_body( return error; } +static int check_filenames( + git_patch_parsed *patch, + patch_parse_ctx *ctx) +{ + /* For modechange only patches, it does not include filenames; + * instead we need to use the paths in the diff --git header. + */ + if (!patch->base.delta->old_file.path && + !patch->base.delta->new_file.path) { + + if (!ctx->header_old_path || !ctx->header_new_path) + return parse_err("git diff header lacks old / new paths"); + + patch->base.delta->old_file.path = ctx->header_old_path; + ctx->header_old_path = NULL; + + patch->base.delta->new_file.path = ctx->header_new_path; + ctx->header_new_path = NULL; + } + + /* For additions that have a `diff --git` header, set the old path + * to the path from the header, not `/dev/null`. + */ + if (patch->base.delta->status == GIT_DELTA_ADDED && + ctx->header_old_path) { + git__free((char *)patch->base.delta->old_file.path); + patch->base.delta->old_file.path = ctx->header_old_path; + ctx->header_old_path = NULL; + } + + /* For deletes, set the new path to the path from the + * `diff --git` header, not `/dev/null`. + */ + if (patch->base.delta->status == GIT_DELTA_DELETED && + ctx->header_new_path) { + git__free((char *)patch->base.delta->new_file.path); + patch->base.delta->new_file.path = ctx->header_new_path; + ctx->header_new_path = NULL; + } + + return 0; +} + static int parsed_patch_header( git_patch_parsed *patch, patch_parse_ctx *ctx) @@ -667,23 +710,7 @@ static int parsed_patch_header( if ((error = parse_header_git(patch, ctx)) < 0) goto done; - /* For modechange only patches, it does not include filenames; - * instead we need to use the paths in the diff --git header. - */ - if (!patch->base.delta->old_file.path && - !patch->base.delta->new_file.path) { - - if (!ctx->header_old_path || !ctx->header_new_path) { - error = parse_err("git diff header lacks old / new paths"); - goto done; - } - - patch->base.delta->old_file.path = ctx->header_old_path; - ctx->header_old_path = NULL; - - patch->base.delta->new_file.path = ctx->header_new_path; - ctx->header_new_path = NULL; - } + error = check_filenames(patch, ctx); goto done; } From d536ceacf56e0ff1f02c8dc29d496bc1c357914f Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 23 Sep 2015 10:47:34 -0400 Subject: [PATCH 22/54] patch_parse: don't set new mode when deleted --- src/patch_parse.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/patch_parse.c b/src/patch_parse.c index f375688af40..df2492ee5e6 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -881,10 +881,10 @@ static int check_patch(git_patch_parsed *patch) patch->base.delta->status != GIT_DELTA_DELETED) return parse_err("missing new file path"); - if (patch->base.delta->old_file.path && patch->base.delta->new_file.path) { - if (!patch->base.delta->new_file.mode) - patch->base.delta->new_file.mode = patch->base.delta->old_file.mode; - } + if (patch->base.delta->old_file.path && + patch->base.delta->status != GIT_DELTA_DELETED && + !patch->base.delta->new_file.mode) + patch->base.delta->new_file.mode = patch->base.delta->old_file.mode; if (patch->base.delta->status == GIT_DELTA_MODIFIED && !(patch->base.delta->flags & GIT_DIFF_FLAG_BINARY) && From 19e46645af31b594514cdf88e0ff037e15f39b9b Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 23 Sep 2015 11:07:04 -0400 Subject: [PATCH 23/54] patch printing: include rename information --- src/diff_print.c | 23 +++++++++++++++++++++++ src/patch_parse.c | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/diff_print.c b/src/diff_print.c index 29ffcdd5114..59f751cc2bc 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -322,6 +322,26 @@ static int diff_delta_format_with_paths( return git_buf_printf(out, template, oldpfx, oldpath, newpfx, newpath); } +int diff_delta_format_rename_header( + git_buf *out, + const git_diff_delta *delta) +{ + if (delta->similarity > 100) { + giterr_set(GITERR_PATCH, "invalid similarity %d", delta->similarity); + return -1; + } + + git_buf_printf(out, + "similarity index %d%%\n" + "rename from %s\n" + "rename to %s\n", + delta->similarity, + delta->old_file.path, + delta->new_file.path); + + return git_buf_oom(out) ? -1 : 0; +} + int git_diff_delta__format_file_header( git_buf *out, const git_diff_delta *delta, @@ -341,6 +361,9 @@ int git_diff_delta__format_file_header( git_buf_printf(out, "diff --git %s%s %s%s\n", oldpfx, delta->old_file.path, newpfx, delta->new_file.path); + if (delta->status == GIT_DELTA_RENAMED) + GITERR_CHECK_ERROR(diff_delta_format_rename_header(out, delta)); + GITERR_CHECK_ERROR(diff_print_oid_range(out, delta, oid_strlen)); if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0) diff --git a/src/patch_parse.c b/src/patch_parse.c index df2492ee5e6..aa767f3b983 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -306,7 +306,7 @@ static int parse_header_rename( static int parse_header_renamefrom( git_patch_parsed *patch, patch_parse_ctx *ctx) { - patch->base.delta->status |= GIT_DELTA_RENAMED; + patch->base.delta->status = GIT_DELTA_RENAMED; return parse_header_rename( (char **)&patch->base.delta->old_file.path, @@ -317,7 +317,7 @@ static int parse_header_renamefrom( static int parse_header_renameto( git_patch_parsed *patch, patch_parse_ctx *ctx) { - patch->base.delta->status |= GIT_DELTA_RENAMED; + patch->base.delta->status = GIT_DELTA_RENAMED; return parse_header_rename( (char **)&patch->base.delta->new_file.path, From 82175084e17d31051e691ebdcb5990a12d0230e7 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 23 Sep 2015 13:40:12 -0400 Subject: [PATCH 24/54] Introduce git_patch_options, handle prefixes Handle prefixes (in terms of number of path components) for patch parsing. --- include/git2/patch.h | 15 ++- src/patch_parse.c | 275 ++++++++++++++++++++++++----------------- tests/apply/fromfile.c | 74 +++++------ tests/patch/parse.c | 8 +- tests/patch/print.c | 2 +- 5 files changed, 220 insertions(+), 154 deletions(-) diff --git a/include/git2/patch.h b/include/git2/patch.h index aa8729c9ce7..f2e2476d9e1 100644 --- a/include/git2/patch.h +++ b/include/git2/patch.h @@ -267,18 +267,31 @@ GIT_EXTERN(int) git_patch_to_buf( git_buf *out, git_patch *patch); +/** Options for parsing patch files. */ +typedef struct { + /** + * The length of the prefix (in path segments) for the filenames. + * This prefix will be removed when looking for files. The default is 1. + */ + uint32_t prefix_len; +} git_patch_options; + +#define GIT_PATCH_OPTIONS_INIT { 1 } + /** * Create a patch from the contents of a patch file. * * @param out The patch to be created * @param patchfile The contents of a patch file * @param patchfile_len The length of the patch file + * @param opts The git_patch_options * @return 0 on success, <0 on failure. */ GIT_EXTERN(int) git_patch_from_patchfile( git_patch **out, const char *patchfile, - size_t patchfile_len); + size_t patchfile_len, + git_patch_options *opts); GIT_END_DECL diff --git a/src/patch_parse.c b/src/patch_parse.c index aa767f3b983..8ba75373a04 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -5,9 +5,25 @@ #define parse_err(...) \ ( giterr_set(GITERR_PATCH, __VA_ARGS__), -1 ) -/* TODO: remove this, just use git_patch */ typedef struct { git_patch base; + + git_patch_options opts; + + /* the paths from the `diff --git` header, these will be used if this is not + * a rename (and rename paths are specified) or if no `+++`/`---` line specify + * the paths. + */ + char *header_old_path, *header_new_path; + + /* renamed paths are precise and are not prefixed */ + char *rename_old_path, *rename_new_path; + + /* the paths given in `---` and `+++` lines */ + char *old_path, *new_path; + + /* the prefixes from the old/new paths */ + char *old_prefix, *new_prefix; } git_patch_parsed; typedef struct { @@ -19,10 +35,6 @@ typedef struct { size_t line_num; size_t remain; - - /* TODO: move this into the parse struct? its lifecycle is odd... */ - char *header_new_path; - char *header_old_path; } patch_parse_ctx; @@ -139,13 +151,13 @@ static int parse_header_path(char **out, patch_parse_ctx *ctx) static int parse_header_git_oldpath( git_patch_parsed *patch, patch_parse_ctx *ctx) { - return parse_header_path((char **)&patch->base.delta->old_file.path, ctx); + return parse_header_path(&patch->old_path, ctx); } static int parse_header_git_newpath( git_patch_parsed *patch, patch_parse_ctx *ctx) { - return parse_header_path((char **)&patch->base.delta->new_file.path, ctx); + return parse_header_path(&patch->new_path, ctx); } static int parse_header_mode(uint16_t *mode, patch_parse_ctx *ctx) @@ -262,44 +274,17 @@ static int parse_header_git_newfilemode( static int parse_header_rename( char **out, - char **header_path, patch_parse_ctx *ctx) { git_buf path = GIT_BUF_INIT; - size_t header_path_len, prefix_len; - - if (*header_path == NULL) - return parse_err("rename without proper git diff header at line %d", - ctx->line_num); - - header_path_len = strlen(*header_path); if (parse_header_path_buf(&path, ctx) < 0) return -1; - if (header_path_len < git_buf_len(&path)) - return parse_err("rename path is invalid at line %d", ctx->line_num); - - /* This sanity check exists because git core uses the data in the - * "rename from" / "rename to" lines, but it's formatted differently - * than the other paths and lacks the normal prefix. This irregularity - * causes us to ignore these paths (we always store the prefixed paths) - * but instead validate that they match the suffix of the paths we parsed - * since we would behave differently from git core if they ever differed. - * Instead, we raise an error, rather than parsing differently. - */ - prefix_len = header_path_len - path.size; - - if (strncmp(*header_path + prefix_len, path.ptr, path.size) != 0 || - (prefix_len > 0 && (*header_path)[prefix_len - 1] != '/')) - return parse_err("rename path does not match header at line %d", - ctx->line_num); - - *out = *header_path; - *header_path = NULL; - - git_buf_free(&path); - + /* Note: the `rename from` and `rename to` lines include the literal + * filename. They do *not* include the prefix. (Who needs consistency?) + */ + *out = git_buf_detach(&path); return 0; } @@ -307,22 +292,14 @@ static int parse_header_renamefrom( git_patch_parsed *patch, patch_parse_ctx *ctx) { patch->base.delta->status = GIT_DELTA_RENAMED; - - return parse_header_rename( - (char **)&patch->base.delta->old_file.path, - &ctx->header_old_path, - ctx); + return parse_header_rename(&patch->rename_old_path, ctx); } static int parse_header_renameto( git_patch_parsed *patch, patch_parse_ctx *ctx) { patch->base.delta->status = GIT_DELTA_RENAMED; - - return parse_header_rename( - (char **)&patch->base.delta->new_file.path, - &ctx->header_new_path, - ctx); + return parse_header_rename(&patch->rename_new_path, ctx); } static int parse_header_percent(uint16_t *out, patch_parse_ctx *ctx) @@ -404,12 +381,12 @@ static int parse_header_git( if (parse_advance_expected(ctx, "diff --git ", 11) < 0) return parse_err("corrupt git diff header at line %d", ctx->line_num); - if (parse_header_path(&ctx->header_old_path, ctx) < 0) + if (parse_header_path(&patch->header_old_path, ctx) < 0) return parse_err("corrupt old path in git diff header at line %d", ctx->line_num); if (parse_advance_ws(ctx) < 0 || - parse_header_path(&ctx->header_new_path, ctx) < 0) + parse_header_path(&patch->header_new_path, ctx) < 0) return parse_err("corrupt new path in git diff header at line %d", ctx->line_num); @@ -628,49 +605,6 @@ static int parse_hunk_body( return error; } -static int check_filenames( - git_patch_parsed *patch, - patch_parse_ctx *ctx) -{ - /* For modechange only patches, it does not include filenames; - * instead we need to use the paths in the diff --git header. - */ - if (!patch->base.delta->old_file.path && - !patch->base.delta->new_file.path) { - - if (!ctx->header_old_path || !ctx->header_new_path) - return parse_err("git diff header lacks old / new paths"); - - patch->base.delta->old_file.path = ctx->header_old_path; - ctx->header_old_path = NULL; - - patch->base.delta->new_file.path = ctx->header_new_path; - ctx->header_new_path = NULL; - } - - /* For additions that have a `diff --git` header, set the old path - * to the path from the header, not `/dev/null`. - */ - if (patch->base.delta->status == GIT_DELTA_ADDED && - ctx->header_old_path) { - git__free((char *)patch->base.delta->old_file.path); - patch->base.delta->old_file.path = ctx->header_old_path; - ctx->header_old_path = NULL; - } - - /* For deletes, set the new path to the path from the - * `diff --git` header, not `/dev/null`. - */ - if (patch->base.delta->status == GIT_DELTA_DELETED && - ctx->header_new_path) { - git__free((char *)patch->base.delta->new_file.path); - patch->base.delta->new_file.path = ctx->header_new_path; - ctx->header_new_path = NULL; - } - - return 0; -} - static int parsed_patch_header( git_patch_parsed *patch, patch_parse_ctx *ctx) @@ -707,11 +641,7 @@ static int parsed_patch_header( /* A proper git patch */ if (ctx->line_len >= 11 && memcmp(ctx->line, "diff --git ", 11) == 0) { - if ((error = parse_header_git(patch, ctx)) < 0) - goto done; - - error = check_filenames(patch, ctx); - + error = parse_header_git(patch, ctx); goto done; } @@ -871,15 +801,114 @@ static int parsed_patch_body( return 0; } -static int check_patch(git_patch_parsed *patch) +int check_header_names( + const char *one, + const char *two, + const char *old_or_new, + bool two_null) +{ + if (!one || !two) + return 0; + + if (two_null && strcmp(two, "/dev/null") != 0) + return parse_err("expected %s path of '/dev/null'", old_or_new); + + else if (!two_null && strcmp(one, two) != 0) + return parse_err("mismatched %s path names", old_or_new); + + return 0; +} + +static int check_prefix( + char **out, + size_t *out_len, + git_patch_parsed *patch, + const char *path_start) +{ + const char *path = path_start; + uint32_t remain = patch->opts.prefix_len; + + *out = NULL; + *out_len = 0; + + if (patch->opts.prefix_len == 0) + goto done; + + /* leading slashes do not count as part of the prefix in git apply */ + while (*path == '/') + path++; + + while (*path && remain) { + if (*path == '/') + remain--; + + path++; + } + + if (remain || !*path) + return parse_err("header filename does not contain %d path components", + patch->opts.prefix_len); + +done: + *out_len = (path - path_start); + *out = git__strndup(path_start, *out_len); + + return (out == NULL) ? -1 : 0; +} + +static int check_filenames(git_patch_parsed *patch) { + const char *prefixed_new, *prefixed_old; + size_t old_prefixlen = 0, new_prefixlen = 0; + bool added = (patch->base.delta->status == GIT_DELTA_ADDED); + bool deleted = (patch->base.delta->status == GIT_DELTA_DELETED); + + if (patch->old_path && !patch->new_path) + return parse_err("missing new path"); + + if (!patch->old_path && patch->new_path) + return parse_err("missing old path"); + + /* Ensure (non-renamed) paths match */ + if (check_header_names( + patch->header_old_path, patch->old_path, "old", added) < 0 || + check_header_names( + patch->header_new_path, patch->new_path, "new", deleted) < 0) + return -1; + + prefixed_old = (!added && patch->old_path) ? patch->old_path : + patch->header_old_path; + prefixed_new = (!deleted && patch->new_path) ? patch->new_path : + patch->header_new_path; + + if (check_prefix( + &patch->old_prefix, &old_prefixlen, patch, prefixed_old) < 0 || + check_prefix( + &patch->new_prefix, &new_prefixlen, patch, prefixed_new) < 0) + return -1; + + /* Prefer the rename filenames as they are unambiguous and unprefixed */ + if (patch->rename_old_path) + patch->base.delta->old_file.path = patch->rename_old_path; + else + patch->base.delta->old_file.path = prefixed_old + old_prefixlen; + + if (patch->rename_new_path) + patch->base.delta->new_file.path = patch->rename_new_path; + else + patch->base.delta->new_file.path = prefixed_new + new_prefixlen; + if (!patch->base.delta->old_file.path && - patch->base.delta->status != GIT_DELTA_ADDED) - return parse_err("missing old file path"); + !patch->base.delta->new_file.path) + return parse_err("git diff header lacks old / new paths"); - if (!patch->base.delta->new_file.path && - patch->base.delta->status != GIT_DELTA_DELETED) - return parse_err("missing new file path"); + return 0; +} + +static int check_patch(git_patch_parsed *patch) +{ + if (check_filenames(patch) < 0) + return -1; if (patch->base.delta->old_file.path && patch->base.delta->status != GIT_DELTA_DELETED && @@ -895,13 +924,32 @@ static int check_patch(git_patch_parsed *patch) return 0; } +static void patch_parsed__free(git_patch *p) +{ + git_patch_parsed *patch = (git_patch_parsed *)p; + + if (!patch) + return; + + git__free(patch->old_prefix); + git__free(patch->new_prefix); + git__free(patch->header_old_path); + git__free(patch->header_new_path); + git__free(patch->rename_old_path); + git__free(patch->rename_new_path); + git__free(patch->old_path); + git__free(patch->new_path); +} + int git_patch_from_patchfile( git_patch **out, const char *content, - size_t content_len) + size_t content_len, + git_patch_options *opts) { patch_parse_ctx ctx = { 0 }; git_patch_parsed *patch; + git_patch_options default_opts = GIT_PATCH_OPTIONS_INIT; int error = 0; *out = NULL; @@ -909,10 +957,12 @@ int git_patch_from_patchfile( patch = git__calloc(1, sizeof(git_patch_parsed)); GITERR_CHECK_ALLOC(patch); - /* TODO: allow callers to specify prefix depth (eg, `-p2`) */ - patch->base.diff_opts.new_prefix = ""; - patch->base.diff_opts.old_prefix = ""; - patch->base.diff_opts.flags |= GIT_DIFF_SHOW_BINARY; + if (opts) + memcpy(&patch->opts, opts, sizeof(git_patch_options)); + else + memcpy(&patch->opts, &default_opts, sizeof(git_patch_options)); + + patch->base.free_fn = patch_parsed__free; patch->base.delta = git__calloc(1, sizeof(git_diff_delta)); patch->base.delta->status = GIT_DELTA_MODIFIED; @@ -927,12 +977,13 @@ int git_patch_from_patchfile( (error = check_patch(patch)) < 0) goto done; + patch->base.diff_opts.old_prefix = patch->old_prefix; + patch->base.diff_opts.new_prefix = patch->new_prefix; + patch->base.diff_opts.flags |= GIT_DIFF_SHOW_BINARY; + GIT_REFCOUNT_INC(patch); *out = &patch->base; done: - git__free(ctx.header_old_path); - git__free(ctx.header_new_path); - return error; } diff --git a/tests/apply/fromfile.c b/tests/apply/fromfile.c index acb635962ab..88a2f458ff3 100644 --- a/tests/apply/fromfile.c +++ b/tests/apply/fromfile.c @@ -35,7 +35,7 @@ static int apply_patchfile( unsigned int mode; int error; - cl_git_pass(git_patch_from_patchfile(&patch, patchfile, strlen(patchfile))); + cl_git_pass(git_patch_from_patchfile(&patch, patchfile, strlen(patchfile), NULL)); error = git_apply__patch(&result, &filename, &mode, old, old_len, patch); @@ -91,7 +91,7 @@ void test_apply_fromfile__change_middle(void) FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), PATCH_ORIGINAL_TO_CHANGE_MIDDLE, NULL, - "b/file.txt", 0100644)); + "file.txt", 0100644)); } void test_apply_fromfile__change_middle_nocontext(void) @@ -103,7 +103,7 @@ void test_apply_fromfile__change_middle_nocontext(void) FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), PATCH_ORIGINAL_TO_CHANGE_MIDDLE_NOCONTEXT, - &diff_opts, "b/file.txt", 0100644)); + &diff_opts, "file.txt", 0100644)); } @@ -113,7 +113,7 @@ void test_apply_fromfile__change_firstline(void) FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_CHANGE_FIRSTLINE, strlen(FILE_CHANGE_FIRSTLINE), PATCH_ORIGINAL_TO_CHANGE_FIRSTLINE, NULL, - "b/file.txt", 0100644)); + "file.txt", 0100644)); } void test_apply_fromfile__lastline(void) @@ -122,7 +122,7 @@ void test_apply_fromfile__lastline(void) FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_CHANGE_LASTLINE, strlen(FILE_CHANGE_LASTLINE), PATCH_ORIGINAL_TO_CHANGE_LASTLINE, NULL, - "b/file.txt", 0100644)); + "file.txt", 0100644)); } void test_apply_fromfile__prepend(void) @@ -130,7 +130,7 @@ void test_apply_fromfile__prepend(void) cl_git_pass(validate_and_apply_patchfile( FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_PREPEND, strlen(FILE_PREPEND), - PATCH_ORIGINAL_TO_PREPEND, NULL, "b/file.txt", 0100644)); + PATCH_ORIGINAL_TO_PREPEND, NULL, "file.txt", 0100644)); } void test_apply_fromfile__prepend_nocontext(void) @@ -142,7 +142,7 @@ void test_apply_fromfile__prepend_nocontext(void) FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_PREPEND, strlen(FILE_PREPEND), PATCH_ORIGINAL_TO_PREPEND_NOCONTEXT, &diff_opts, - "b/file.txt", 0100644)); + "file.txt", 0100644)); } void test_apply_fromfile__append(void) @@ -150,7 +150,7 @@ void test_apply_fromfile__append(void) cl_git_pass(validate_and_apply_patchfile( FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_APPEND, strlen(FILE_APPEND), - PATCH_ORIGINAL_TO_APPEND, NULL, "b/file.txt", 0100644)); + PATCH_ORIGINAL_TO_APPEND, NULL, "file.txt", 0100644)); } void test_apply_fromfile__append_nocontext(void) @@ -162,7 +162,7 @@ void test_apply_fromfile__append_nocontext(void) FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_APPEND, strlen(FILE_APPEND), PATCH_ORIGINAL_TO_APPEND_NOCONTEXT, &diff_opts, - "b/file.txt", 0100644)); + "file.txt", 0100644)); } void test_apply_fromfile__prepend_and_append(void) @@ -171,7 +171,7 @@ void test_apply_fromfile__prepend_and_append(void) FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_PREPEND_AND_APPEND, strlen(FILE_PREPEND_AND_APPEND), PATCH_ORIGINAL_TO_PREPEND_AND_APPEND, NULL, - "b/file.txt", 0100644)); + "file.txt", 0100644)); } void test_apply_fromfile__to_empty_file(void) @@ -179,7 +179,7 @@ void test_apply_fromfile__to_empty_file(void) cl_git_pass(validate_and_apply_patchfile( FILE_ORIGINAL, strlen(FILE_ORIGINAL), "", 0, - PATCH_ORIGINAL_TO_EMPTY_FILE, NULL, "b/file.txt", 0100644)); + PATCH_ORIGINAL_TO_EMPTY_FILE, NULL, "file.txt", 0100644)); } void test_apply_fromfile__from_empty_file(void) @@ -187,7 +187,7 @@ void test_apply_fromfile__from_empty_file(void) cl_git_pass(validate_and_apply_patchfile( "", 0, FILE_ORIGINAL, strlen(FILE_ORIGINAL), - PATCH_EMPTY_FILE_TO_ORIGINAL, NULL, "b/file.txt", 0100644)); + PATCH_EMPTY_FILE_TO_ORIGINAL, NULL, "file.txt", 0100644)); } void test_apply_fromfile__add(void) @@ -195,7 +195,7 @@ void test_apply_fromfile__add(void) cl_git_pass(validate_and_apply_patchfile( NULL, 0, FILE_ORIGINAL, strlen(FILE_ORIGINAL), - PATCH_ADD_ORIGINAL, NULL, "b/file.txt", 0100644)); + PATCH_ADD_ORIGINAL, NULL, "file.txt", 0100644)); } void test_apply_fromfile__delete(void) @@ -212,7 +212,7 @@ void test_apply_fromfile__rename_exact(void) cl_git_pass(apply_patchfile( FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_ORIGINAL, strlen(FILE_ORIGINAL), - PATCH_RENAME_EXACT, "b/newfile.txt", 0100644)); + PATCH_RENAME_EXACT, "newfile.txt", 0100644)); } void test_apply_fromfile__rename_similar(void) @@ -220,7 +220,7 @@ void test_apply_fromfile__rename_similar(void) cl_git_pass(apply_patchfile( FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), - PATCH_RENAME_SIMILAR, "b/newfile.txt", 0100644)); + PATCH_RENAME_SIMILAR, "newfile.txt", 0100644)); } void test_apply_fromfile__rename_similar_quotedname(void) @@ -228,7 +228,7 @@ void test_apply_fromfile__rename_similar_quotedname(void) cl_git_pass(apply_patchfile( FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), - PATCH_RENAME_SIMILAR_QUOTEDNAME, "b/foo\"bar.txt", 0100644)); + PATCH_RENAME_SIMILAR_QUOTEDNAME, "foo\"bar.txt", 0100644)); } void test_apply_fromfile__modechange(void) @@ -236,7 +236,7 @@ void test_apply_fromfile__modechange(void) cl_git_pass(apply_patchfile( FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_ORIGINAL, strlen(FILE_ORIGINAL), - PATCH_MODECHANGE_UNCHANGED, "b/file.txt", 0100755)); + PATCH_MODECHANGE_UNCHANGED, "file.txt", 0100755)); } void test_apply_fromfile__modechange_with_modification(void) @@ -244,7 +244,7 @@ void test_apply_fromfile__modechange_with_modification(void) cl_git_pass(apply_patchfile( FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), - PATCH_MODECHANGE_MODIFIED, "b/file.txt", 0100755)); + PATCH_MODECHANGE_MODIFIED, "file.txt", 0100755)); } void test_apply_fromfile__noisy(void) @@ -252,7 +252,7 @@ void test_apply_fromfile__noisy(void) cl_git_pass(apply_patchfile( FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), - PATCH_NOISY, "b/file.txt", 0100644)); + PATCH_NOISY, "file.txt", 0100644)); } void test_apply_fromfile__noisy_nocontext(void) @@ -260,35 +260,35 @@ void test_apply_fromfile__noisy_nocontext(void) cl_git_pass(apply_patchfile( FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), - PATCH_NOISY_NOCONTEXT, "b/file.txt", 0100644)); + PATCH_NOISY_NOCONTEXT, "file.txt", 0100644)); } void test_apply_fromfile__fail_truncated_1(void) { git_patch *patch; cl_git_fail(git_patch_from_patchfile(&patch, PATCH_TRUNCATED_1, - strlen(PATCH_TRUNCATED_1))); + strlen(PATCH_TRUNCATED_1), NULL)); } void test_apply_fromfile__fail_truncated_2(void) { git_patch *patch; cl_git_fail(git_patch_from_patchfile(&patch, PATCH_TRUNCATED_2, - strlen(PATCH_TRUNCATED_2))); + strlen(PATCH_TRUNCATED_2), NULL)); } void test_apply_fromfile__fail_truncated_3(void) { git_patch *patch; cl_git_fail(git_patch_from_patchfile(&patch, PATCH_TRUNCATED_3, - strlen(PATCH_TRUNCATED_3))); + strlen(PATCH_TRUNCATED_3), NULL)); } void test_apply_fromfile__fail_corrupt_githeader(void) { git_patch *patch; cl_git_fail(git_patch_from_patchfile(&patch, PATCH_CORRUPT_GIT_HEADER, - strlen(PATCH_CORRUPT_GIT_HEADER))); + strlen(PATCH_CORRUPT_GIT_HEADER), NULL)); } void test_apply_fromfile__empty_context(void) @@ -297,7 +297,7 @@ void test_apply_fromfile__empty_context(void) FILE_EMPTY_CONTEXT_ORIGINAL, strlen(FILE_EMPTY_CONTEXT_ORIGINAL), FILE_EMPTY_CONTEXT_MODIFIED, strlen(FILE_EMPTY_CONTEXT_MODIFIED), PATCH_EMPTY_CONTEXT, - "b/file.txt", 0100644)); + "file.txt", 0100644)); } void test_apply_fromfile__append_no_nl(void) @@ -305,7 +305,7 @@ void test_apply_fromfile__append_no_nl(void) cl_git_pass(validate_and_apply_patchfile( FILE_ORIGINAL, strlen(FILE_ORIGINAL), FILE_APPEND_NO_NL, strlen(FILE_APPEND_NO_NL), - PATCH_APPEND_NO_NL, NULL, "b/file.txt", 0100644)); + PATCH_APPEND_NO_NL, NULL, "file.txt", 0100644)); } void test_apply_fromfile__fail_missing_new_file(void) @@ -313,7 +313,7 @@ void test_apply_fromfile__fail_missing_new_file(void) git_patch *patch; cl_git_fail(git_patch_from_patchfile(&patch, PATCH_CORRUPT_MISSING_NEW_FILE, - strlen(PATCH_CORRUPT_MISSING_NEW_FILE))); + strlen(PATCH_CORRUPT_MISSING_NEW_FILE), NULL)); } void test_apply_fromfile__fail_missing_old_file(void) @@ -321,7 +321,7 @@ void test_apply_fromfile__fail_missing_old_file(void) git_patch *patch; cl_git_fail(git_patch_from_patchfile(&patch, PATCH_CORRUPT_MISSING_OLD_FILE, - strlen(PATCH_CORRUPT_MISSING_OLD_FILE))); + strlen(PATCH_CORRUPT_MISSING_OLD_FILE), NULL)); } void test_apply_fromfile__fail_no_changes(void) @@ -329,7 +329,7 @@ void test_apply_fromfile__fail_no_changes(void) git_patch *patch; cl_git_fail(git_patch_from_patchfile(&patch, PATCH_CORRUPT_NO_CHANGES, - strlen(PATCH_CORRUPT_NO_CHANGES))); + strlen(PATCH_CORRUPT_NO_CHANGES), NULL)); } void test_apply_fromfile__fail_missing_hunk_header(void) @@ -337,14 +337,14 @@ void test_apply_fromfile__fail_missing_hunk_header(void) git_patch *patch; cl_git_fail(git_patch_from_patchfile(&patch, PATCH_CORRUPT_MISSING_HUNK_HEADER, - strlen(PATCH_CORRUPT_MISSING_HUNK_HEADER))); + strlen(PATCH_CORRUPT_MISSING_HUNK_HEADER), NULL)); } void test_apply_fromfile__fail_not_a_patch(void) { git_patch *patch; cl_git_fail(git_patch_from_patchfile(&patch, PATCH_NOT_A_PATCH, - strlen(PATCH_NOT_A_PATCH))); + strlen(PATCH_NOT_A_PATCH), NULL)); } void test_apply_fromfile__binary_add(void) @@ -352,7 +352,7 @@ void test_apply_fromfile__binary_add(void) cl_git_pass(apply_patchfile( NULL, 0, FILE_BINARY_DELTA_MODIFIED, FILE_BINARY_DELTA_MODIFIED_LEN, - PATCH_BINARY_ADD, "b/binary.bin", 0100644)); + PATCH_BINARY_ADD, "binary.bin", 0100644)); } void test_apply_fromfile__binary_change_delta(void) @@ -360,7 +360,7 @@ void test_apply_fromfile__binary_change_delta(void) cl_git_pass(apply_patchfile( FILE_BINARY_DELTA_ORIGINAL, FILE_BINARY_DELTA_ORIGINAL_LEN, FILE_BINARY_DELTA_MODIFIED, FILE_BINARY_DELTA_MODIFIED_LEN, - PATCH_BINARY_DELTA, "b/binary.bin", 0100644)); + PATCH_BINARY_DELTA, "binary.bin", 0100644)); } void test_apply_fromfile__binary_change_literal(void) @@ -368,7 +368,7 @@ void test_apply_fromfile__binary_change_literal(void) cl_git_pass(apply_patchfile( FILE_BINARY_LITERAL_ORIGINAL, FILE_BINARY_LITERAL_ORIGINAL_LEN, FILE_BINARY_LITERAL_MODIFIED, FILE_BINARY_LITERAL_MODIFIED_LEN, - PATCH_BINARY_LITERAL, "b/binary.bin", 0100644)); + PATCH_BINARY_LITERAL, "binary.bin", 0100644)); } void test_apply_fromfile__binary_delete(void) @@ -385,7 +385,7 @@ void test_apply_fromfile__binary_change_does_not_apply(void) cl_git_fail(apply_patchfile( FILE_BINARY_DELTA_MODIFIED, FILE_BINARY_DELTA_MODIFIED_LEN, FILE_BINARY_DELTA_ORIGINAL, FILE_BINARY_DELTA_ORIGINAL_LEN, - PATCH_BINARY_DELTA, "b/binary.bin", 0100644)); + PATCH_BINARY_DELTA, "binary.bin", 0100644)); } void test_apply_fromfile__binary_change_must_be_reversible(void) @@ -400,6 +400,6 @@ void test_apply_fromfile__empty_file_not_allowed(void) { git_patch *patch; - cl_git_fail(git_patch_from_patchfile(&patch, "", 0)); - cl_git_fail(git_patch_from_patchfile(&patch, NULL, 0)); + cl_git_fail(git_patch_from_patchfile(&patch, "", 0, NULL)); + cl_git_fail(git_patch_from_patchfile(&patch, NULL, 0, NULL)); } diff --git a/tests/patch/parse.c b/tests/patch/parse.c index 3191c8c3fd8..28f61ffcdfa 100644 --- a/tests/patch/parse.c +++ b/tests/patch/parse.c @@ -8,19 +8,21 @@ void test_patch_parse__original_to_change_middle(void) const git_diff_delta *delta; char idstr[GIT_OID_HEXSZ+1] = {0}; - cl_git_pass(git_patch_from_patchfile(&patch, PATCH_ORIGINAL_TO_CHANGE_MIDDLE, strlen(PATCH_ORIGINAL_TO_CHANGE_MIDDLE))); + cl_git_pass(git_patch_from_patchfile( + &patch, PATCH_ORIGINAL_TO_CHANGE_MIDDLE, + strlen(PATCH_ORIGINAL_TO_CHANGE_MIDDLE), NULL)); cl_assert((delta = git_patch_get_delta(patch)) != NULL); cl_assert_equal_i(2, delta->nfiles); - cl_assert_equal_s(delta->old_file.path, "a/file.txt"); + cl_assert_equal_s(delta->old_file.path, "file.txt"); cl_assert(delta->old_file.mode == GIT_FILEMODE_BLOB); cl_assert_equal_i(7, delta->old_file.id_abbrev); git_oid_nfmt(idstr, delta->old_file.id_abbrev, &delta->old_file.id); cl_assert_equal_s(idstr, "9432026"); cl_assert_equal_i(0, delta->old_file.size); - cl_assert_equal_s(delta->new_file.path, "b/file.txt"); + cl_assert_equal_s(delta->new_file.path, "file.txt"); cl_assert(delta->new_file.mode == GIT_FILEMODE_BLOB); cl_assert_equal_i(7, delta->new_file.id_abbrev); git_oid_nfmt(idstr, delta->new_file.id_abbrev, &delta->new_file.id); diff --git a/tests/patch/print.c b/tests/patch/print.c index 9bfc5f0eb71..a07328fa814 100644 --- a/tests/patch/print.c +++ b/tests/patch/print.c @@ -12,7 +12,7 @@ void patch_print_from_patchfile(const char *data, size_t len) git_patch *patch; git_buf buf = GIT_BUF_INIT; - cl_git_pass(git_patch_from_patchfile(&patch, data, len)); + cl_git_pass(git_patch_from_patchfile(&patch, data, len, NULL)); cl_git_pass(git_patch_to_buf(&buf, patch)); cl_assert_equal_s(data, buf.ptr); From 72806f4cca7602460d19fbee8be98449304e92a2 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 23 Sep 2015 13:56:48 -0400 Subject: [PATCH 25/54] patch: don't print some headers on pure renames --- src/diff_print.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/diff_print.c b/src/diff_print.c index 59f751cc2bc..0253ca69639 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -349,6 +349,8 @@ int git_diff_delta__format_file_header( const char *newpfx, int oid_strlen) { + bool skip_index; + if (!oldpfx) oldpfx = DIFF_OLD_PREFIX_DEFAULT; if (!newpfx) @@ -364,11 +366,18 @@ int git_diff_delta__format_file_header( if (delta->status == GIT_DELTA_RENAMED) GITERR_CHECK_ERROR(diff_delta_format_rename_header(out, delta)); - GITERR_CHECK_ERROR(diff_print_oid_range(out, delta, oid_strlen)); + skip_index = (delta->status == GIT_DELTA_RENAMED && + delta->similarity == 100 && + delta->old_file.mode == 0 && + delta->new_file.mode == 0); + + if (!skip_index) { + GITERR_CHECK_ERROR(diff_print_oid_range(out, delta, oid_strlen)); - if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0) - diff_delta_format_with_paths( - out, delta, oldpfx, newpfx, "--- %s%s\n+++ %s%s\n"); + if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0) + diff_delta_format_with_paths( + out, delta, oldpfx, newpfx, "--- %s%s\n+++ %s%s\n"); + } return git_buf_oom(out) ? -1 : 0; } From d3d95d5ae2c0c06724d040713a04202073114041 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 23 Sep 2015 16:30:48 -0400 Subject: [PATCH 26/54] git_buf_quote: quote ugly characters --- src/buffer.c | 66 +++++++++++++++++++++++++++++++++++++++ src/buffer.h | 3 +- tests/buf/quote.c | 78 +++++++++++++++++++++++++++++++---------------- 3 files changed, 120 insertions(+), 27 deletions(-) diff --git a/src/buffer.c b/src/buffer.c index c2a54a5bde2..31341c4b5d0 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -858,6 +858,72 @@ int git_buf_splice( return 0; } +/* Quote per http://marc.info/?l=git&m=112927316408690&w=2 */ +int git_buf_quote(git_buf *buf) +{ + const char whitespace[] = { 'a', 'b', 't', 'n', 'v', 'f', 'r' }; + git_buf quoted = GIT_BUF_INIT; + size_t i = 0; + bool quote = false; + int error = 0; + + /* walk to the first char that needs quoting */ + if (buf->size && buf->ptr[0] == '!') + quote = true; + + for (i = 0; !quote && i < buf->size; i++) { + if (buf->ptr[i] == '"' || buf->ptr[i] == '\\' || + buf->ptr[i] < ' ' || buf->ptr[i] > '~') { + quote = true; + break; + } + } + + if (!quote) + goto done; + + git_buf_putc("ed, '"'); + git_buf_put("ed, buf->ptr, i); + + for (; i < buf->size; i++) { + /* whitespace - use the map above, which is ordered by ascii value */ + if (buf->ptr[i] >= '\a' && buf->ptr[i] <= '\r') { + git_buf_putc("ed, '\\'); + git_buf_putc("ed, whitespace[buf->ptr[i] - '\a']); + } + + /* double quote and backslash must be escaped */ + else if (buf->ptr[i] == '"' || buf->ptr[i] == '\\') { + git_buf_putc("ed, '\\'); + git_buf_putc("ed, buf->ptr[i]); + } + + /* escape anything unprintable as octal */ + else if (buf->ptr[i] != ' ' && + (buf->ptr[i] < '!' || buf->ptr[i] > '~')) { + git_buf_printf("ed, "\\%03o", buf->ptr[i]); + } + + /* yay, printable! */ + else { + git_buf_putc("ed, buf->ptr[i]); + } + } + + git_buf_putc("ed, '"'); + + if (git_buf_oom("ed)) { + error = -1; + goto done; + } + + git_buf_swap("ed, buf); + +done: + git_buf_free("ed); + return error; +} + /* Unquote per http://marc.info/?l=git&m=112927316408690&w=2 */ int git_buf_unquote(git_buf *buf) { diff --git a/src/buffer.h b/src/buffer.h index 2be299b14d1..cdfca6d99bb 100644 --- a/src/buffer.h +++ b/src/buffer.h @@ -173,9 +173,10 @@ void git_buf_rtrim(git_buf *buf); int git_buf_cmp(const git_buf *a, const git_buf *b); -/* Unquote a buffer as specified in +/* Quote and unquote a buffer as specified in * http://marc.info/?l=git&m=112927316408690&w=2 */ +int git_buf_quote(git_buf *buf); int git_buf_unquote(git_buf *buf); /* Write data as base64 encoded in buffer */ diff --git a/tests/buf/quote.c b/tests/buf/quote.c index ef26116632f..ed5021e25fe 100644 --- a/tests/buf/quote.c +++ b/tests/buf/quote.c @@ -1,7 +1,33 @@ #include "clar_libgit2.h" #include "buffer.h" -static void expect_pass(const char *expected, const char *quoted) +static void expect_quote_pass(const char *expected, const char *str) +{ + git_buf buf = GIT_BUF_INIT; + + cl_git_pass(git_buf_puts(&buf, str)); + cl_git_pass(git_buf_quote(&buf)); + + cl_assert_equal_s(expected, git_buf_cstr(&buf)); + cl_assert_equal_i(strlen(expected), git_buf_len(&buf)); + + git_buf_free(&buf); +} + +void test_buf_quote__quote_succeeds(void) +{ + expect_quote_pass("", ""); + expect_quote_pass("foo", "foo"); + expect_quote_pass("foo/bar/baz.c", "foo/bar/baz.c"); + expect_quote_pass("foo bar", "foo bar"); + expect_quote_pass("\"\\\"leading quote\"", "\"leading quote"); + expect_quote_pass("\"slash\\\\y\"", "slash\\y"); + expect_quote_pass("\"foo\\r\\nbar\"", "foo\r\nbar"); + expect_quote_pass("\"foo\\177bar\"", "foo\177bar"); + expect_quote_pass("\"foo\\001bar\"", "foo\001bar"); +} + +static void expect_unquote_pass(const char *expected, const char *quoted) { git_buf buf = GIT_BUF_INIT; @@ -14,7 +40,7 @@ static void expect_pass(const char *expected, const char *quoted) git_buf_free(&buf); } -static void expect_fail(const char *quoted) +static void expect_unquote_fail(const char *quoted) { git_buf buf = GIT_BUF_INIT; @@ -26,32 +52,32 @@ static void expect_fail(const char *quoted) void test_buf_quote__unquote_succeeds(void) { - expect_pass("", "\"\""); - expect_pass(" ", "\" \""); - expect_pass("foo", "\"foo\""); - expect_pass("foo bar", "\"foo bar\""); - expect_pass("foo\"bar", "\"foo\\\"bar\""); - expect_pass("foo\\bar", "\"foo\\\\bar\""); - expect_pass("foo\tbar", "\"foo\\tbar\""); - expect_pass("\vfoo\tbar\n", "\"\\vfoo\\tbar\\n\""); - expect_pass("foo\nbar", "\"foo\\012bar\""); - expect_pass("foo\r\nbar", "\"foo\\015\\012bar\""); - expect_pass("foo\r\nbar", "\"\\146\\157\\157\\015\\012\\142\\141\\162\""); - expect_pass("newline: \n", "\"newline: \\012\""); + expect_unquote_pass("", "\"\""); + expect_unquote_pass(" ", "\" \""); + expect_unquote_pass("foo", "\"foo\""); + expect_unquote_pass("foo bar", "\"foo bar\""); + expect_unquote_pass("foo\"bar", "\"foo\\\"bar\""); + expect_unquote_pass("foo\\bar", "\"foo\\\\bar\""); + expect_unquote_pass("foo\tbar", "\"foo\\tbar\""); + expect_unquote_pass("\vfoo\tbar\n", "\"\\vfoo\\tbar\\n\""); + expect_unquote_pass("foo\nbar", "\"foo\\012bar\""); + expect_unquote_pass("foo\r\nbar", "\"foo\\015\\012bar\""); + expect_unquote_pass("foo\r\nbar", "\"\\146\\157\\157\\015\\012\\142\\141\\162\""); + expect_unquote_pass("newline: \n", "\"newline: \\012\""); } void test_buf_quote__unquote_fails(void) { - expect_fail("no quotes at all"); - expect_fail("\"no trailing quote"); - expect_fail("no leading quote\""); - expect_fail("\"invalid \\z escape char\""); - expect_fail("\"\\q invalid escape char\""); - expect_fail("\"invalid escape char \\p\""); - expect_fail("\"invalid \\1 escape char \""); - expect_fail("\"invalid \\14 escape char \""); - expect_fail("\"invalid \\411 escape char\""); - expect_fail("\"truncated escape char \\\""); - expect_fail("\"truncated escape char \\0\""); - expect_fail("\"truncated escape char \\01\""); + expect_unquote_fail("no quotes at all"); + expect_unquote_fail("\"no trailing quote"); + expect_unquote_fail("no leading quote\""); + expect_unquote_fail("\"invalid \\z escape char\""); + expect_unquote_fail("\"\\q invalid escape char\""); + expect_unquote_fail("\"invalid escape char \\p\""); + expect_unquote_fail("\"invalid \\1 escape char \""); + expect_unquote_fail("\"invalid \\14 escape char \""); + expect_unquote_fail("\"invalid \\411 escape char\""); + expect_unquote_fail("\"truncated escape char \\\""); + expect_unquote_fail("\"truncated escape char \\0\""); + expect_unquote_fail("\"truncated escape char \\01\""); } From 4ac2d8acf4e76384ffaa84d4152333a71051f3c3 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 23 Sep 2015 16:33:02 -0400 Subject: [PATCH 27/54] patch: quote filenames when necessary --- src/diff_print.c | 129 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 96 insertions(+), 33 deletions(-) diff --git a/src/diff_print.c b/src/diff_print.c index 0253ca69639..93c887bf5d0 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -300,46 +300,67 @@ static int diff_print_oid_range( return git_buf_oom(out) ? -1 : 0; } +static int diff_delta_format_path( + git_buf *out, const char *prefix, const char *filename) +{ + if (git_buf_joinpath(out, prefix, filename) < 0) + return -1; + + return git_buf_quote(out); +} + + static int diff_delta_format_with_paths( git_buf *out, const git_diff_delta *delta, - const char *oldpfx, - const char *newpfx, - const char *template) + const char *template, + const char *oldpath, + const char *newpath) { - const char *oldpath = delta->old_file.path; - const char *newpath = delta->new_file.path; - - if (git_oid_iszero(&delta->old_file.id)) { - oldpfx = ""; + if (git_oid_iszero(&delta->old_file.id)) oldpath = "/dev/null"; - } - if (git_oid_iszero(&delta->new_file.id)) { - newpfx = ""; + + if (git_oid_iszero(&delta->new_file.id)) newpath = "/dev/null"; - } - return git_buf_printf(out, template, oldpfx, oldpath, newpfx, newpath); + return git_buf_printf(out, template, oldpath, newpath); } int diff_delta_format_rename_header( git_buf *out, const git_diff_delta *delta) { + git_buf old_path = GIT_BUF_INIT, new_path = GIT_BUF_INIT; + int error = 0; + if (delta->similarity > 100) { giterr_set(GITERR_PATCH, "invalid similarity %d", delta->similarity); - return -1; + error = -1; + goto done; } + if ((error = git_buf_puts(&old_path, delta->old_file.path)) < 0 || + (error = git_buf_puts(&new_path, delta->new_file.path)) < 0 || + (error = git_buf_quote(&old_path)) < 0 || + (error = git_buf_quote(&new_path)) < 0) + goto done; + git_buf_printf(out, "similarity index %d%%\n" "rename from %s\n" "rename to %s\n", delta->similarity, - delta->old_file.path, - delta->new_file.path); + old_path.ptr, + new_path.ptr); - return git_buf_oom(out) ? -1 : 0; + if (git_buf_oom(out)) + error = -1; + +done: + git_buf_free(&old_path); + git_buf_free(&new_path); + + return error; } int git_diff_delta__format_file_header( @@ -349,7 +370,9 @@ int git_diff_delta__format_file_header( const char *newpfx, int oid_strlen) { + git_buf old_path = GIT_BUF_INIT, new_path = GIT_BUF_INIT; bool skip_index; + int error = 0; if (!oldpfx) oldpfx = DIFF_OLD_PREFIX_DEFAULT; @@ -358,13 +381,21 @@ int git_diff_delta__format_file_header( if (!oid_strlen) oid_strlen = GIT_ABBREV_DEFAULT + 1; + if ((error = diff_delta_format_path( + &old_path, oldpfx, delta->old_file.path)) < 0 || + (error = diff_delta_format_path( + &new_path, newpfx, delta->new_file.path)) < 0) + goto done; + git_buf_clear(out); - git_buf_printf(out, "diff --git %s%s %s%s\n", - oldpfx, delta->old_file.path, newpfx, delta->new_file.path); + git_buf_printf(out, "diff --git %s %s\n", + old_path.ptr, new_path.ptr); - if (delta->status == GIT_DELTA_RENAMED) - GITERR_CHECK_ERROR(diff_delta_format_rename_header(out, delta)); + if (delta->status == GIT_DELTA_RENAMED) { + if ((error = diff_delta_format_rename_header(out, delta)) < 0) + goto done; + } skip_index = (delta->status == GIT_DELTA_RENAMED && delta->similarity == 100 && @@ -372,14 +403,22 @@ int git_diff_delta__format_file_header( delta->new_file.mode == 0); if (!skip_index) { - GITERR_CHECK_ERROR(diff_print_oid_range(out, delta, oid_strlen)); + if ((error = diff_print_oid_range(out, delta, oid_strlen)) < 0) + goto done; if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0) - diff_delta_format_with_paths( - out, delta, oldpfx, newpfx, "--- %s%s\n+++ %s%s\n"); + diff_delta_format_with_paths(out, delta, + "--- %s\n+++ %s\n", old_path.ptr, new_path.ptr); } - return git_buf_oom(out) ? -1 : 0; + if (git_buf_oom(out)) + error = -1; + +done: + git_buf_free(&old_path); + git_buf_free(&new_path); + + return error; } static int format_binary( @@ -420,6 +459,33 @@ static int format_binary( return 0; } +static int diff_print_patch_file_binary_noshow( + diff_print_info *pi, git_diff_delta *delta, + const char *old_pfx, const char *new_pfx, + const git_diff_binary *binary) +{ + git_buf old_path = GIT_BUF_INIT, new_path = GIT_BUF_INIT; + int error; + + if ((error = diff_delta_format_path( + &old_path, old_pfx, delta->old_file.path)) < 0 || + (error = diff_delta_format_path( + &new_path, new_pfx, delta->new_file.path)) < 0) + goto done; + + + pi->line.num_lines = 1; + error = diff_delta_format_with_paths( + pi->buf, delta, "Binary files %s and %s differ\n", + old_path.ptr, new_path.ptr); + +done: + git_buf_free(&old_path); + git_buf_free(&new_path); + + return error; +} + static int diff_print_patch_file_binary( diff_print_info *pi, git_diff_delta *delta, const char *old_pfx, const char *new_pfx, @@ -429,7 +495,8 @@ static int diff_print_patch_file_binary( int error; if ((pi->flags & GIT_DIFF_SHOW_BINARY) == 0) - goto noshow; + return diff_print_patch_file_binary_noshow( + pi, delta, old_pfx, new_pfx, binary); if (binary->new_file.datalen == 0 && binary->old_file.datalen == 0) return 0; @@ -446,18 +513,14 @@ static int diff_print_patch_file_binary( if (error == GIT_EBUFS) { giterr_clear(); git_buf_truncate(pi->buf, pre_binary_size); - goto noshow; + + return diff_print_patch_file_binary_noshow( + pi, delta, old_pfx, new_pfx, binary); } } pi->line.num_lines++; return error; - -noshow: - pi->line.num_lines = 1; - return diff_delta_format_with_paths( - pi->buf, delta, old_pfx, new_pfx, - "Binary files %s%s and %s%s differ\n"); } static int diff_print_patch_file( From e2cdc145b9633ed0f7fcfdeed5eb4a7fc65653ae Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 23 Sep 2015 17:48:52 -0400 Subject: [PATCH 28/54] patch: show modes when only the mode has changed --- src/diff_print.c | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/diff_print.c b/src/diff_print.c index 93c887bf5d0..32283d52b4a 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -258,6 +258,15 @@ static int diff_print_one_raw( return pi->print_cb(delta, NULL, &pi->line, pi->payload); } +static int diff_print_modes( + git_buf *out, const git_diff_delta *delta) +{ + git_buf_printf(out, "old mode %o\n", delta->old_file.mode); + git_buf_printf(out, "new mode %o\n", delta->new_file.mode); + + return git_buf_oom(out) ? -1 : 0; +} + static int diff_print_oid_range( git_buf *out, const git_diff_delta *delta, int oid_strlen) { @@ -286,14 +295,13 @@ static int diff_print_oid_range( git_buf_printf(out, "index %s..%s %o\n", start_oid, end_oid, delta->old_file.mode); } else { - if (delta->old_file.mode == 0) { + if (delta->old_file.mode == 0) git_buf_printf(out, "new file mode %o\n", delta->new_file.mode); - } else if (delta->new_file.mode == 0) { + else if (delta->new_file.mode == 0) git_buf_printf(out, "deleted file mode %o\n", delta->old_file.mode); - } else { - git_buf_printf(out, "old mode %o\n", delta->old_file.mode); - git_buf_printf(out, "new mode %o\n", delta->new_file.mode); - } + else + diff_print_modes(out, delta); + git_buf_printf(out, "index %s..%s\n", start_oid, end_oid); } @@ -309,7 +317,6 @@ static int diff_delta_format_path( return git_buf_quote(out); } - static int diff_delta_format_with_paths( git_buf *out, const git_diff_delta *delta, @@ -371,7 +378,7 @@ int git_diff_delta__format_file_header( int oid_strlen) { git_buf old_path = GIT_BUF_INIT, new_path = GIT_BUF_INIT; - bool skip_index; + bool unchanged; int error = 0; if (!oldpfx) @@ -397,12 +404,10 @@ int git_diff_delta__format_file_header( goto done; } - skip_index = (delta->status == GIT_DELTA_RENAMED && - delta->similarity == 100 && - delta->old_file.mode == 0 && - delta->new_file.mode == 0); + unchanged = (git_oid_iszero(&delta->old_file.id) && + git_oid_iszero(&delta->new_file.id)); - if (!skip_index) { + if (!unchanged) { if ((error = diff_print_oid_range(out, delta, oid_strlen)) < 0) goto done; @@ -411,6 +416,9 @@ int git_diff_delta__format_file_header( "--- %s\n+++ %s\n", old_path.ptr, new_path.ptr); } + if (unchanged && delta->old_file.mode != delta->new_file.mode) + diff_print_modes(out, delta); + if (git_buf_oom(out)) error = -1; From 040ec883a47744349c89178114f8ae2c8324ef57 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 23 Sep 2015 18:20:17 -0400 Subject: [PATCH 29/54] patch: use strlen to mean string length `oid_strlen` has meant one more than the length of the string. This is mighty confusing. Make it mean only the string length! Whomsoever needs to allocate a buffer to hold a string can null terminate it like normal. --- src/diff_print.c | 56 +++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/diff_print.c b/src/diff_print.c index 32283d52b4a..daba9f1ca02 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -25,7 +25,7 @@ typedef struct { const char *old_prefix; const char *new_prefix; uint32_t flags; - int oid_strlen; + int id_strlen; int (*strcomp)(const char *, const char *); } diff_print_info; @@ -43,17 +43,15 @@ static int diff_print_info_init__common( pi->payload = payload; pi->buf = out; - if (!pi->oid_strlen) { + if (!pi->id_strlen) { if (!repo) - pi->oid_strlen = GIT_ABBREV_DEFAULT; - else if (git_repository__cvar(&pi->oid_strlen, repo, GIT_CVAR_ABBREV) < 0) + pi->id_strlen = GIT_ABBREV_DEFAULT; + else if (git_repository__cvar(&pi->id_strlen, repo, GIT_CVAR_ABBREV) < 0) return -1; } - pi->oid_strlen += 1; /* for NUL byte */ - - if (pi->oid_strlen > GIT_OID_HEXSZ + 1) - pi->oid_strlen = GIT_OID_HEXSZ + 1; + if (pi->id_strlen > GIT_OID_HEXSZ) + pi->id_strlen = GIT_OID_HEXSZ; memset(&pi->line, 0, sizeof(pi->line)); pi->line.old_lineno = -1; @@ -77,7 +75,7 @@ static int diff_print_info_init_fromdiff( if (diff) { pi->flags = diff->opts.flags; - pi->oid_strlen = diff->opts.id_abbrev; + pi->id_strlen = diff->opts.id_abbrev; pi->old_prefix = diff->opts.old_prefix; pi->new_prefix = diff->opts.new_prefix; @@ -100,7 +98,7 @@ static int diff_print_info_init_frompatch( memset(pi, 0, sizeof(diff_print_info)); pi->flags = patch->diff_opts.flags; - pi->oid_strlen = patch->diff_opts.id_abbrev; + pi->id_strlen = patch->diff_opts.id_abbrev; pi->old_prefix = patch->diff_opts.old_prefix; pi->new_prefix = patch->diff_opts.new_prefix; @@ -222,18 +220,18 @@ static int diff_print_one_raw( id_abbrev = delta->old_file.mode ? delta->old_file.id_abbrev : delta->new_file.id_abbrev; - if (pi->oid_strlen - 1 > id_abbrev) { + if (pi->id_strlen > id_abbrev) { giterr_set(GITERR_PATCH, "The patch input contains %d id characters (cannot print %d)", - id_abbrev, pi->oid_strlen); + id_abbrev, pi->id_strlen); return -1; } - git_oid_tostr(start_oid, pi->oid_strlen, &delta->old_file.id); - git_oid_tostr(end_oid, pi->oid_strlen, &delta->new_file.id); + git_oid_tostr(start_oid, pi->id_strlen + 1, &delta->old_file.id); + git_oid_tostr(end_oid, pi->id_strlen + 1, &delta->new_file.id); git_buf_printf( - out, (pi->oid_strlen <= GIT_OID_HEXSZ) ? + out, (pi->id_strlen <= GIT_OID_HEXSZ) ? ":%06o %06o %s... %s... %c" : ":%06o %06o %s %s %c", delta->old_file.mode, delta->new_file.mode, start_oid, end_oid, code); @@ -268,28 +266,28 @@ static int diff_print_modes( } static int diff_print_oid_range( - git_buf *out, const git_diff_delta *delta, int oid_strlen) + git_buf *out, const git_diff_delta *delta, int id_strlen) { char start_oid[GIT_OID_HEXSZ+1], end_oid[GIT_OID_HEXSZ+1]; if (delta->old_file.mode && - oid_strlen - 1 > delta->old_file.id_abbrev) { + id_strlen > delta->old_file.id_abbrev) { giterr_set(GITERR_PATCH, "The patch input contains %d id characters (cannot print %d)", - delta->old_file.id_abbrev, oid_strlen); + delta->old_file.id_abbrev, id_strlen); return -1; } if ((delta->new_file.mode && - oid_strlen - 1 > delta->new_file.id_abbrev)) { + id_strlen > delta->new_file.id_abbrev)) { giterr_set(GITERR_PATCH, "The patch input contains %d id characters (cannot print %d)", - delta->new_file.id_abbrev, oid_strlen); + delta->new_file.id_abbrev, id_strlen); return -1; } - git_oid_tostr(start_oid, oid_strlen, &delta->old_file.id); - git_oid_tostr(end_oid, oid_strlen, &delta->new_file.id); + git_oid_tostr(start_oid, id_strlen + 1, &delta->old_file.id); + git_oid_tostr(end_oid, id_strlen + 1, &delta->new_file.id); if (delta->old_file.mode == delta->new_file.mode) { git_buf_printf(out, "index %s..%s %o\n", @@ -375,7 +373,7 @@ int git_diff_delta__format_file_header( const git_diff_delta *delta, const char *oldpfx, const char *newpfx, - int oid_strlen) + int id_strlen) { git_buf old_path = GIT_BUF_INIT, new_path = GIT_BUF_INIT; bool unchanged; @@ -385,8 +383,8 @@ int git_diff_delta__format_file_header( oldpfx = DIFF_OLD_PREFIX_DEFAULT; if (!newpfx) newpfx = DIFF_NEW_PREFIX_DEFAULT; - if (!oid_strlen) - oid_strlen = GIT_ABBREV_DEFAULT + 1; + if (!id_strlen) + id_strlen = GIT_ABBREV_DEFAULT; if ((error = diff_delta_format_path( &old_path, oldpfx, delta->old_file.path)) < 0 || @@ -408,7 +406,7 @@ int git_diff_delta__format_file_header( git_oid_iszero(&delta->new_file.id)); if (!unchanged) { - if ((error = diff_print_oid_range(out, delta, oid_strlen)) < 0) + if ((error = diff_print_oid_range(out, delta, id_strlen)) < 0) goto done; if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0) @@ -544,8 +542,8 @@ static int diff_print_patch_file( bool binary = (delta->flags & GIT_DIFF_FLAG_BINARY) || (pi->flags & GIT_DIFF_FORCE_BINARY); bool show_binary = !!(pi->flags & GIT_DIFF_SHOW_BINARY); - int oid_strlen = binary && show_binary ? - GIT_OID_HEXSZ + 1 : pi->oid_strlen; + int id_strlen = binary && show_binary ? + GIT_OID_HEXSZ : pi->id_strlen; GIT_UNUSED(progress); @@ -558,7 +556,7 @@ static int diff_print_patch_file( return 0; if ((error = git_diff_delta__format_file_header( - pi->buf, delta, oldpfx, newpfx, oid_strlen)) < 0) + pi->buf, delta, oldpfx, newpfx, id_strlen)) < 0) return error; pi->line.origin = GIT_DIFF_LINE_FILE_HDR; From f941f035aea73aeda0093a85e514711d006cda22 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Thu, 24 Sep 2015 09:25:10 -0400 Subject: [PATCH 30/54] patch: drop some warnings --- src/diff_print.c | 7 +++---- src/patch_parse.c | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/diff_print.c b/src/diff_print.c index daba9f1ca02..e5238699169 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -467,8 +467,7 @@ static int format_binary( static int diff_print_patch_file_binary_noshow( diff_print_info *pi, git_diff_delta *delta, - const char *old_pfx, const char *new_pfx, - const git_diff_binary *binary) + const char *old_pfx, const char *new_pfx) { git_buf old_path = GIT_BUF_INIT, new_path = GIT_BUF_INIT; int error; @@ -502,7 +501,7 @@ static int diff_print_patch_file_binary( if ((pi->flags & GIT_DIFF_SHOW_BINARY) == 0) return diff_print_patch_file_binary_noshow( - pi, delta, old_pfx, new_pfx, binary); + pi, delta, old_pfx, new_pfx); if (binary->new_file.datalen == 0 && binary->old_file.datalen == 0) return 0; @@ -521,7 +520,7 @@ static int diff_print_patch_file_binary( git_buf_truncate(pi->buf, pre_binary_size); return diff_print_patch_file_binary_noshow( - pi, delta, old_pfx, new_pfx, binary); + pi, delta, old_pfx, new_pfx); } } diff --git a/src/patch_parse.c b/src/patch_parse.c index 8ba75373a04..25193b6b7fb 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -184,7 +184,7 @@ static int parse_header_mode(uint16_t *mode, patch_parse_ctx *ctx) static int parse_header_oid( git_oid *oid, - size_t *oid_len, + int *oid_len, patch_parse_ctx *ctx) { size_t len; @@ -201,7 +201,7 @@ static int parse_header_oid( parse_advance_chars(ctx, len); - *oid_len = len; + *oid_len = (int)len; return 0; } From 6278fbc5dd5467e3f66f31dc9c4bb4a1a3519ba5 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Thu, 24 Sep 2015 09:40:42 -0400 Subject: [PATCH 31/54] patch parsing: squash some memory leaks --- src/patch_parse.c | 7 +++++++ tests/core/buffer.c | 2 ++ 2 files changed, 9 insertions(+) diff --git a/src/patch_parse.c b/src/patch_parse.c index 25193b6b7fb..418ed1e0c6a 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -939,6 +939,10 @@ static void patch_parsed__free(git_patch *p) git__free(patch->rename_new_path); git__free(patch->old_path); git__free(patch->new_path); + git_array_clear(patch->base.hunks); + git_array_clear(patch->base.lines); + git__free(patch->base.delta); + git__free(patch); } int git_patch_from_patchfile( @@ -985,5 +989,8 @@ int git_patch_from_patchfile( *out = &patch->base; done: + if (error < 0) + patch_parsed__free(&patch->base); + return error; } diff --git a/tests/core/buffer.c b/tests/core/buffer.c index 1cf23426b3b..c4308cbbfd2 100644 --- a/tests/core/buffer.c +++ b/tests/core/buffer.c @@ -847,6 +847,8 @@ void test_core_buffer__decode_base85_fails_gracefully(void) cl_git_fail(git_buf_decode_base85(&buf, "truncated", 9, 42)); cl_assert_equal_sz(6, buf.size); cl_assert_equal_s("foobar", buf.ptr); + + git_buf_free(&buf); } void test_core_buffer__classify_with_utf8(void) From 4117a2350f87456d76659d9327193bb708187ba9 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Thu, 24 Sep 2015 10:32:15 -0400 Subject: [PATCH 32/54] patch parse: dup the patch from the callers --- src/patch.c | 6 ------ src/patch_diff.c | 6 ++++++ src/patch_parse.c | 27 ++++++++++++++++++++++----- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/patch.c b/src/patch.c index f05cfb21add..e14ffa9c01f 100644 --- a/src/patch.c +++ b/src/patch.c @@ -196,12 +196,6 @@ int git_patch_get_line_in_hunk( static void git_patch__free(git_patch *patch) { - git_array_clear(patch->lines); - git_array_clear(patch->hunks); - - git__free((char *)patch->binary.old_file.data); - git__free((char *)patch->binary.new_file.data); - if (patch->free_fn) patch->free_fn(patch); } diff --git a/src/patch_diff.c b/src/patch_diff.c index 1a3aeda5e07..b8adcf3e158 100644 --- a/src/patch_diff.c +++ b/src/patch_diff.c @@ -25,6 +25,12 @@ static void patch_diff_free(git_patch *p) { git_patch_diff *patch = (git_patch_diff *)p; + git_array_clear(patch->base.lines); + git_array_clear(patch->base.hunks); + + git__free((char *)patch->base.binary.old_file.data); + git__free((char *)patch->base.binary.new_file.data); + git_diff_file_content__clear(&patch->ofile); git_diff_file_content__clear(&patch->nfile); diff --git a/src/patch_parse.c b/src/patch_parse.c index 418ed1e0c6a..4e4e0a68ac1 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -10,6 +10,10 @@ typedef struct { git_patch_options opts; + /* the patch contents, which lines will point into. */ + /* TODO: allow us to point somewhere refcounted. */ + char *content; + /* the paths from the `diff --git` header, these will be used if this is not * a rename (and rename paths are specified) or if no `+++`/`---` line specify * the paths. @@ -523,7 +527,7 @@ static int parse_hunk_body( int newlines = hunk->hunk.new_lines; for (; - ctx->remain > 4 && (oldlines || newlines) && + ctx->remain > 4 && (oldlines || newlines) && memcmp(ctx->line, "@@ -", 4) != 0; parse_advance_line(ctx)) { @@ -931,6 +935,12 @@ static void patch_parsed__free(git_patch *p) if (!patch) return; + git__free((char *)patch->base.binary.old_file.data); + git__free((char *)patch->base.binary.new_file.data); + git_array_clear(patch->base.hunks); + git_array_clear(patch->base.lines); + git__free(patch->base.delta); + git__free(patch->old_prefix); git__free(patch->new_prefix); git__free(patch->header_old_path); @@ -939,9 +949,7 @@ static void patch_parsed__free(git_patch *p) git__free(patch->rename_new_path); git__free(patch->old_path); git__free(patch->new_path); - git_array_clear(patch->base.hunks); - git_array_clear(patch->base.lines); - git__free(patch->base.delta); + git__free(patch->content); git__free(patch); } @@ -969,10 +977,19 @@ int git_patch_from_patchfile( patch->base.free_fn = patch_parsed__free; patch->base.delta = git__calloc(1, sizeof(git_diff_delta)); + GITERR_CHECK_ALLOC(patch->base.delta); + patch->base.delta->status = GIT_DELTA_MODIFIED; patch->base.delta->nfiles = 2; - ctx.content = content; + if (content_len) { + patch->content = git__malloc(content_len); + GITERR_CHECK_ALLOC(patch->content); + + memcpy(patch->content, content, content_len); + } + + ctx.content = patch->content; ctx.content_len = content_len; ctx.remain = content_len; From 0267c34c0cbcdd3b5935d5988d572564dbe5d939 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Fri, 25 Sep 2015 10:19:50 -0400 Subject: [PATCH 33/54] patch application: drop unnecessary `patch_image_init` --- src/apply.c | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/apply.c b/src/apply.c index a453d3dbbb8..a9b4935198e 100644 --- a/src/apply.c +++ b/src/apply.c @@ -38,11 +38,7 @@ static void patch_line_init( out->content_offset = in_offset; } -static unsigned int patch_image_init(patch_image *out) -{ - memset(out, 0x0, sizeof(patch_image)); - return 0; -} +#define PATCH_IMAGE_INIT { {0} } static int patch_image_init_fromstr( patch_image *out, const char *in, size_t in_len) @@ -165,14 +161,10 @@ static int apply_hunk( git_patch *patch, git_patch_hunk *hunk) { - patch_image preimage, postimage; + patch_image preimage = PATCH_IMAGE_INIT, postimage = PATCH_IMAGE_INIT; size_t line_num, i; int error = 0; - if ((error = patch_image_init(&preimage)) < 0 || - (error = patch_image_init(&postimage)) < 0) - goto done; - for (i = 0; i < hunk->line_count; i++) { size_t linenum = hunk->line_start + i; git_diff_line *line = git_array_get(patch->lines, linenum); From 8cb27223b8eda4767179a1f226f96d2bdec2fe44 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Fri, 25 Sep 2015 10:48:19 -0400 Subject: [PATCH 34/54] git_buf_quote/unquote: handle > \177 Parse values up to and including `\377` (`0xff`) when unquoting. Print octal values as an unsigned char when quoting, lest `printf` think we're talking about negatives. --- src/buffer.c | 4 ++-- tests/buf/quote.c | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/buffer.c b/src/buffer.c index 31341c4b5d0..d135ebe4a3b 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -901,7 +901,7 @@ int git_buf_quote(git_buf *buf) /* escape anything unprintable as octal */ else if (buf->ptr[i] != ' ' && (buf->ptr[i] < '!' || buf->ptr[i] > '~')) { - git_buf_printf("ed, "\\%03o", buf->ptr[i]); + git_buf_printf("ed, "\\%03o", (unsigned char)buf->ptr[i]); } /* yay, printable! */ @@ -959,7 +959,7 @@ int git_buf_unquote(git_buf *buf) case 'v': ch = '\v'; break; /* \xyz digits convert to the char*/ - case '0': case '1': case '2': + case '0': case '1': case '2': case '3': if (j == buf->size-3) { giterr_set(GITERR_INVALID, "Truncated quoted character \\%c", ch); diff --git a/tests/buf/quote.c b/tests/buf/quote.c index ed5021e25fe..6f77ab9c1c9 100644 --- a/tests/buf/quote.c +++ b/tests/buf/quote.c @@ -25,6 +25,7 @@ void test_buf_quote__quote_succeeds(void) expect_quote_pass("\"foo\\r\\nbar\"", "foo\r\nbar"); expect_quote_pass("\"foo\\177bar\"", "foo\177bar"); expect_quote_pass("\"foo\\001bar\"", "foo\001bar"); + expect_quote_pass("\"foo\\377bar\"", "foo\377bar"); } static void expect_unquote_pass(const char *expected, const char *quoted) @@ -64,6 +65,7 @@ void test_buf_quote__unquote_succeeds(void) expect_unquote_pass("foo\r\nbar", "\"foo\\015\\012bar\""); expect_unquote_pass("foo\r\nbar", "\"\\146\\157\\157\\015\\012\\142\\141\\162\""); expect_unquote_pass("newline: \n", "\"newline: \\012\""); + expect_unquote_pass("0xff: \377", "\"0xff: \\377\""); } void test_buf_quote__unquote_fails(void) @@ -76,6 +78,9 @@ void test_buf_quote__unquote_fails(void) expect_unquote_fail("\"invalid escape char \\p\""); expect_unquote_fail("\"invalid \\1 escape char \""); expect_unquote_fail("\"invalid \\14 escape char \""); + expect_unquote_fail("\"invalid \\280 escape char\""); + expect_unquote_fail("\"invalid \\378 escape char\""); + expect_unquote_fail("\"invalid \\380 escape char\""); expect_unquote_fail("\"invalid \\411 escape char\""); expect_unquote_fail("\"truncated escape char \\\""); expect_unquote_fail("\"truncated escape char \\0\""); From 0ff723cc90c258f2b78285e7e4b55352e1bc05cd Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Fri, 25 Sep 2015 12:09:50 -0400 Subject: [PATCH 35/54] apply: test postimages that grow/shrink original Test with some postimages that actually grow/shrink from the original, adding new lines or removing them. (Also do so without context to ensure that we can add/remove from a non-zero part of the line vector.) --- tests/apply/fromfile.c | 42 ++++++++++++++++++ tests/patch/patch_common.h | 89 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/tests/apply/fromfile.c b/tests/apply/fromfile.c index 88a2f458ff3..ec2b889b3dc 100644 --- a/tests/apply/fromfile.c +++ b/tests/apply/fromfile.c @@ -125,6 +125,48 @@ void test_apply_fromfile__lastline(void) "file.txt", 0100644)); } +void test_apply_fromfile__change_middle_shrink(void) +{ + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE_SHRINK, strlen(FILE_CHANGE_MIDDLE_SHRINK), + PATCH_ORIGINAL_TO_CHANGE_MIDDLE_SHRINK, NULL, + "file.txt", 0100644)); +} + +void test_apply_fromfile__change_middle_shrink_nocontext(void) +{ + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + diff_opts.context_lines = 0; + + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE_SHRINK, strlen(FILE_CHANGE_MIDDLE_SHRINK), + PATCH_ORIGINAL_TO_MIDDLE_SHRINK_NOCONTEXT, &diff_opts, + "file.txt", 0100644)); +} + +void test_apply_fromfile__change_middle_grow(void) +{ + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE_GROW, strlen(FILE_CHANGE_MIDDLE_GROW), + PATCH_ORIGINAL_TO_CHANGE_MIDDLE_GROW, NULL, + "file.txt", 0100644)); +} + +void test_apply_fromfile__change_middle_grow_nocontext(void) +{ + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + diff_opts.context_lines = 0; + + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE_GROW, strlen(FILE_CHANGE_MIDDLE_GROW), + PATCH_ORIGINAL_TO_MIDDLE_GROW_NOCONTEXT, &diff_opts, + "file.txt", 0100644)); +} + void test_apply_fromfile__prepend(void) { cl_git_pass(validate_and_apply_patchfile( diff --git a/tests/patch/patch_common.h b/tests/patch/patch_common.h index f4cb2ff84ef..e097062d2c9 100644 --- a/tests/patch/patch_common.h +++ b/tests/patch/patch_common.h @@ -98,6 +98,95 @@ "-below it!\n" \ "+change to the last line.\n" +/* A change of the middle where we remove many lines */ + +#define FILE_CHANGE_MIDDLE_SHRINK \ + "hey!\n" \ + "i've changed a lot, but left the line\n" \ + "below it!\n" + +#define PATCH_ORIGINAL_TO_CHANGE_MIDDLE_SHRINK \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..629cd35 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -1,9 +1,3 @@\n" \ + " hey!\n" \ + "-this is some context!\n" \ + "-around some lines\n" \ + "-that will change\n" \ + "-yes it is!\n" \ + "-(this line is changed)\n" \ + "-and this\n" \ + "-is additional context\n" \ + "+i've changed a lot, but left the line\n" \ + " below it!\n" + +#define PATCH_ORIGINAL_TO_MIDDLE_SHRINK_NOCONTEXT \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..629cd35 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -2,7 +2 @@ hey!\n" \ + "-this is some context!\n" \ + "-around some lines\n" \ + "-that will change\n" \ + "-yes it is!\n" \ + "-(this line is changed)\n" \ + "-and this\n" \ + "-is additional context\n" \ + "+i've changed a lot, but left the line\n" + +/* A change to the middle where we grow many lines */ + +#define FILE_CHANGE_MIDDLE_GROW \ + "hey!\n" \ + "this is some context!\n" \ + "around some lines\n" \ + "that will change\n" \ + "yes it is!\n" \ + "this line is changed\n" \ + "and this line is added\n" \ + "so is this\n" \ + "(this too)\n" \ + "whee...\n" \ + "and this\n" \ + "is additional context\n" \ + "below it!\n" + +#define PATCH_ORIGINAL_TO_CHANGE_MIDDLE_GROW \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..207ebca 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -3,7 +3,11 @@ this is some context!\n" \ + " around some lines\n" \ + " that will change\n" \ + " yes it is!\n" \ + "-(this line is changed)\n" \ + "+this line is changed\n" \ + "+and this line is added\n" \ + "+so is this\n" \ + "+(this too)\n" \ + "+whee...\n" \ + " and this\n" \ + " is additional context\n" \ + " below it!\n" + + +#define PATCH_ORIGINAL_TO_MIDDLE_GROW_NOCONTEXT \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..207ebca 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -6 +6,5 @@ yes it is!\n" \ + "-(this line is changed)\n" \ + "+this line is changed\n" \ + "+and this line is added\n" \ + "+so is this\n" \ + "+(this too)\n" \ + "+whee...\n" + /* An insertion at the beginning of the file (and the resultant patch) */ #define FILE_PREPEND \ From e564fc65b53b67ff0753749caa07b7877fb22420 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Fri, 25 Sep 2015 12:41:15 -0400 Subject: [PATCH 36/54] git_vector_grow/shrink: correct shrink, and tests --- src/vector.c | 29 +++++++------ tests/core/vector.c | 102 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 14 deletions(-) diff --git a/src/vector.c b/src/vector.c index 3684676920a..5ad8a738ca7 100644 --- a/src/vector.c +++ b/src/vector.c @@ -7,6 +7,7 @@ #include "common.h" #include "vector.h" +#include "integer.h" /* In elements, not bytes */ #define MIN_ALLOCSIZE 8 @@ -332,17 +333,16 @@ int git_vector_resize_to(git_vector *v, size_t new_length) int git_vector_grow_at(git_vector *v, size_t idx, size_t grow_len) { - size_t new_length = v->length + grow_len; - size_t new_idx = idx + grow_len; + size_t new_length; - assert(grow_len > 0); - assert (idx <= v->length); + assert(grow_len > 0 && idx <= v->length); - if (new_length < v->length || - (new_length > v->_alloc_size && resize_vector(v, new_length) < 0)) + GITERR_CHECK_ALLOC_ADD(&new_length, v->length, grow_len); + + if (new_length > v->_alloc_size && resize_vector(v, new_length) < 0) return -1; - memmove(&v->contents[new_idx], &v->contents[idx], + memmove(&v->contents[idx + grow_len], &v->contents[idx], sizeof(void *) * (v->length - idx)); memset(&v->contents[idx], 0, sizeof(void *) * grow_len); @@ -353,17 +353,18 @@ int git_vector_grow_at(git_vector *v, size_t idx, size_t grow_len) int git_vector_shrink_at(git_vector *v, size_t idx, size_t shrink_len) { size_t new_length = v->length - shrink_len; - size_t end_idx = idx + shrink_len; + size_t end_idx = 0; + + assert(shrink_len > 0); - assert(shrink_len > 0 && shrink_len <= v->length); - assert(idx <= v->length); + if (git__add_sizet_overflow(&end_idx, idx, shrink_len)) + assert(0); - if (new_length > v->length) - return -1; + assert(end_idx <= v->length); - if (idx > v->length) + if (end_idx < v->length) memmove(&v->contents[idx], &v->contents[end_idx], - sizeof(void *) * (v->length - idx)); + sizeof(void *) * (v->length - end_idx)); memset(&v->contents[new_length], 0, sizeof(void *) * shrink_len); diff --git a/tests/core/vector.c b/tests/core/vector.c index 66f90b82b6f..abc641a2c7d 100644 --- a/tests/core/vector.c +++ b/tests/core/vector.c @@ -274,3 +274,105 @@ void test_core_vector__remove_matching(void) git_vector_free(&x); } + +static void assert_vector(git_vector *x, void *expected[], size_t len) +{ + size_t i; + + cl_assert_equal_i(len, x->length); + + for (i = 0; i < len; i++) + cl_assert(expected[i] == x->contents[i]); +} + +void test_core_vector__grow_and_shrink(void) +{ + git_vector x = GIT_VECTOR_INIT; + void *expected1[] = { + (void *)0x02, (void *)0x03, (void *)0x04, (void *)0x05, + (void *)0x06, (void *)0x07, (void *)0x08, (void *)0x09, + (void *)0x0a + }; + void *expected2[] = { + (void *)0x02, (void *)0x04, (void *)0x05, (void *)0x06, + (void *)0x07, (void *)0x08, (void *)0x09, (void *)0x0a + }; + void *expected3[] = { + (void *)0x02, (void *)0x04, (void *)0x05, (void *)0x06, + (void *)0x0a + }; + void *expected4[] = { + (void *)0x02, (void *)0x04, (void *)0x05 + }; + void *expected5[] = { + (void *)0x00, (void *)0x00, (void *)0x02, (void *)0x04, + (void *)0x05 + }; + void *expected6[] = { + (void *)0x00, (void *)0x00, (void *)0x02, (void *)0x04, + (void *)0x05, (void *)0x00 + }; + void *expected7[] = { + (void *)0x00, (void *)0x00, (void *)0x02, (void *)0x04, + (void *)0x00, (void *)0x00, (void *)0x00, (void *)0x05, + (void *)0x00 + }; + void *expected8[] = { + (void *)0x04, (void *)0x00, (void *)0x00, (void *)0x00, + (void *)0x05, (void *)0x00 + }; + void *expected9[] = { + (void *)0x04, (void *)0x00, (void *)0x05, (void *)0x00 + }; + void *expectedA[] = { (void *)0x04, (void *)0x00 }; + void *expectedB[] = { (void *)0x04 }; + + git_vector_insert(&x, (void *)0x01); + git_vector_insert(&x, (void *)0x02); + git_vector_insert(&x, (void *)0x03); + git_vector_insert(&x, (void *)0x04); + git_vector_insert(&x, (void *)0x05); + git_vector_insert(&x, (void *)0x06); + git_vector_insert(&x, (void *)0x07); + git_vector_insert(&x, (void *)0x08); + git_vector_insert(&x, (void *)0x09); + git_vector_insert(&x, (void *)0x0a); + + git_vector_shrink_at(&x, 0, 1); + assert_vector(&x, expected1, ARRAY_SIZE(expected1)); + + git_vector_shrink_at(&x, 1, 1); + assert_vector(&x, expected2, ARRAY_SIZE(expected2)); + + git_vector_shrink_at(&x, 4, 3); + assert_vector(&x, expected3, ARRAY_SIZE(expected3)); + + git_vector_shrink_at(&x, 3, 2); + assert_vector(&x, expected4, ARRAY_SIZE(expected4)); + + git_vector_grow_at(&x, 0, 2); + assert_vector(&x, expected5, ARRAY_SIZE(expected5)); + + git_vector_grow_at(&x, 5, 1); + assert_vector(&x, expected6, ARRAY_SIZE(expected6)); + + git_vector_grow_at(&x, 4, 3); + assert_vector(&x, expected7, ARRAY_SIZE(expected7)); + + git_vector_shrink_at(&x, 0, 3); + assert_vector(&x, expected8, ARRAY_SIZE(expected8)); + + git_vector_shrink_at(&x, 1, 2); + assert_vector(&x, expected9, ARRAY_SIZE(expected9)); + + git_vector_shrink_at(&x, 2, 2); + assert_vector(&x, expectedA, ARRAY_SIZE(expectedA)); + + git_vector_shrink_at(&x, 1, 1); + assert_vector(&x, expectedB, ARRAY_SIZE(expectedB)); + + git_vector_shrink_at(&x, 0, 1); + assert_vector(&x, NULL, 0); + + git_vector_free(&x); +} From a03952f09547cc72c8edda503aac8ab9652f3c98 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Fri, 25 Sep 2015 12:44:14 -0400 Subject: [PATCH 37/54] patch: formatting cleanups --- src/pack-objects.c | 2 +- src/patch_diff.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pack-objects.c b/src/pack-objects.c index ec15970f301..20a8e47ab43 100644 --- a/src/pack-objects.c +++ b/src/pack-objects.c @@ -888,7 +888,7 @@ static unsigned int check_delta_limit(git_pobject *me, unsigned int n) static unsigned long free_unpacked(struct unpacked *n) { unsigned long freed_mem = 0; - + if (n->index) { freed_mem += git_delta_index_size(n->index); git_delta_index_free(n->index); diff --git a/src/patch_diff.c b/src/patch_diff.c index b8adcf3e158..bae0bd22725 100644 --- a/src/patch_diff.c +++ b/src/patch_diff.c @@ -25,7 +25,7 @@ static void patch_diff_free(git_patch *p) { git_patch_diff *patch = (git_patch_diff *)p; - git_array_clear(patch->base.lines); + git_array_clear(patch->base.lines); git_array_clear(patch->base.hunks); git__free((char *)patch->base.binary.old_file.data); From 00e63b36202be18b1dd1690049f8cbb755132f1b Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sat, 21 Nov 2015 12:37:01 -0500 Subject: [PATCH 38/54] patch: provide static string `advance_expected` --- src/patch_parse.c | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/patch_parse.c b/src/patch_parse.c index 4e4e0a68ac1..46ecae73fb5 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -72,6 +72,9 @@ static int parse_advance_expected( return 0; } +#define parse_advance_expected_s(ctx, str) \ + parse_advance_expected(ctx, str, sizeof(str) - 1) + static int parse_advance_ws(patch_parse_ctx *ctx) { int ret = -1; @@ -215,7 +218,7 @@ static int parse_header_git_index( { if (parse_header_oid(&patch->base.delta->old_file.id, &patch->base.delta->old_file.id_abbrev, ctx) < 0 || - parse_advance_expected(ctx, "..", 2) < 0 || + parse_advance_expected_s(ctx, "..") < 0 || parse_header_oid(&patch->base.delta->new_file.id, &patch->base.delta->new_file.id_abbrev, ctx) < 0) return -1; @@ -317,7 +320,7 @@ static int parse_header_percent(uint16_t *out, patch_parse_ctx *ctx) parse_advance_chars(ctx, (end - ctx->line)); - if (parse_advance_expected(ctx, "%", 1) < 0) + if (parse_advance_expected_s(ctx, "%") < 0) return -1; if (val > 100) @@ -382,7 +385,7 @@ static int parse_header_git( int error = 0; /* Parse the diff --git line */ - if (parse_advance_expected(ctx, "diff --git ", 11) < 0) + if (parse_advance_expected_s(ctx, "diff --git ") < 0) return parse_err("corrupt git diff header at line %d", ctx->line_num); if (parse_header_path(&patch->header_old_path, ctx) < 0) @@ -416,7 +419,7 @@ static int parse_header_git( goto done; parse_advance_ws(ctx); - parse_advance_expected(ctx, "\n", 1); + parse_advance_expected_s(ctx, "\n"); if (ctx->line_len > 0) { error = parse_err("trailing data at line %d", ctx->line_num); @@ -471,27 +474,27 @@ static int parse_hunk_header( hunk->hunk.old_lines = 1; hunk->hunk.new_lines = 1; - if (parse_advance_expected(ctx, "@@ -", 4) < 0 || + if (parse_advance_expected_s(ctx, "@@ -") < 0 || parse_int(&hunk->hunk.old_start, ctx) < 0) goto fail; if (ctx->line_len > 0 && ctx->line[0] == ',') { - if (parse_advance_expected(ctx, ",", 1) < 0 || + if (parse_advance_expected_s(ctx, ",") < 0 || parse_int(&hunk->hunk.old_lines, ctx) < 0) goto fail; } - if (parse_advance_expected(ctx, " +", 2) < 0 || + if (parse_advance_expected_s(ctx, " +") < 0 || parse_int(&hunk->hunk.new_start, ctx) < 0) goto fail; if (ctx->line_len > 0 && ctx->line[0] == ',') { - if (parse_advance_expected(ctx, ",", 1) < 0 || + if (parse_advance_expected_s(ctx, ",") < 0 || parse_int(&hunk->hunk.new_lines, ctx) < 0) goto fail; } - if (parse_advance_expected(ctx, " @@", 3) < 0) + if (parse_advance_expected_s(ctx, " @@") < 0) goto fail; parse_advance_line(ctx); @@ -745,7 +748,7 @@ static int parsed_patch_binary( { int error; - if (parse_advance_expected(ctx, "GIT binary patch", 16) < 0 || + if (parse_advance_expected_s(ctx, "GIT binary patch") < 0 || parse_advance_nl(ctx) < 0) return parse_err("corrupt git binary header at line %d", ctx->line_num); From 440e3bae10a4758d5de8d7b8699bf4c412c1a163 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sat, 21 Nov 2015 12:27:03 -0500 Subject: [PATCH 39/54] patch: `git_patch_from_patchfile` -> `git_patch_from_buffer` --- include/git2/patch.h | 26 -------------------------- src/patch.h | 26 ++++++++++++++++++++++++++ src/patch_parse.c | 2 +- tests/apply/fromfile.c | 25 +++++++++++++------------ tests/patch/parse.c | 3 ++- tests/patch/print.c | 3 ++- 6 files changed, 44 insertions(+), 41 deletions(-) diff --git a/include/git2/patch.h b/include/git2/patch.h index f2e2476d9e1..790cb74fcce 100644 --- a/include/git2/patch.h +++ b/include/git2/patch.h @@ -267,32 +267,6 @@ GIT_EXTERN(int) git_patch_to_buf( git_buf *out, git_patch *patch); -/** Options for parsing patch files. */ -typedef struct { - /** - * The length of the prefix (in path segments) for the filenames. - * This prefix will be removed when looking for files. The default is 1. - */ - uint32_t prefix_len; -} git_patch_options; - -#define GIT_PATCH_OPTIONS_INIT { 1 } - -/** - * Create a patch from the contents of a patch file. - * - * @param out The patch to be created - * @param patchfile The contents of a patch file - * @param patchfile_len The length of the patch file - * @param opts The git_patch_options - * @return 0 on success, <0 on failure. - */ -GIT_EXTERN(int) git_patch_from_patchfile( - git_patch **out, - const char *patchfile, - size_t patchfile_len, - git_patch_options *opts); - GIT_END_DECL /**@}*/ diff --git a/src/patch.h b/src/patch.h index b818c5cbe72..28fe766da15 100644 --- a/src/patch.h +++ b/src/patch.h @@ -50,6 +50,32 @@ extern int git_patch_line_stats( size_t *total_dels, const git_patch *patch); +/** Options for parsing patch files. */ +typedef struct { + /** + * The length of the prefix (in path segments) for the filenames. + * This prefix will be removed when looking for files. The default is 1. + */ + uint32_t prefix_len; +} git_patch_options; + +#define GIT_PATCH_OPTIONS_INIT { 1 } + +/** + * Create a patch for a single file from the contents of a patch buffer. + * + * @param out The patch to be created + * @param contents The contents of a patch file + * @param contents_len The length of the patch file + * @param opts The git_patch_options + * @return 0 on success, <0 on failure. + */ +extern int git_patch_from_buffer( + git_patch **out, + const char *contents, + size_t contents_len, + git_patch_options *opts); + extern void git_patch_free(git_patch *patch); #endif diff --git a/src/patch_parse.c b/src/patch_parse.c index 46ecae73fb5..9421bb39b30 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -956,7 +956,7 @@ static void patch_parsed__free(git_patch *p) git__free(patch); } -int git_patch_from_patchfile( +int git_patch_from_buffer( git_patch **out, const char *content, size_t content_len, diff --git a/tests/apply/fromfile.c b/tests/apply/fromfile.c index ec2b889b3dc..7eb1af90c8f 100644 --- a/tests/apply/fromfile.c +++ b/tests/apply/fromfile.c @@ -2,6 +2,7 @@ #include "git2/sys/repository.h" #include "apply.h" +#include "patch.h" #include "repository.h" #include "buf_text.h" @@ -35,7 +36,7 @@ static int apply_patchfile( unsigned int mode; int error; - cl_git_pass(git_patch_from_patchfile(&patch, patchfile, strlen(patchfile), NULL)); + cl_git_pass(git_patch_from_buffer(&patch, patchfile, strlen(patchfile), NULL)); error = git_apply__patch(&result, &filename, &mode, old, old_len, patch); @@ -308,28 +309,28 @@ void test_apply_fromfile__noisy_nocontext(void) void test_apply_fromfile__fail_truncated_1(void) { git_patch *patch; - cl_git_fail(git_patch_from_patchfile(&patch, PATCH_TRUNCATED_1, + cl_git_fail(git_patch_from_buffer(&patch, PATCH_TRUNCATED_1, strlen(PATCH_TRUNCATED_1), NULL)); } void test_apply_fromfile__fail_truncated_2(void) { git_patch *patch; - cl_git_fail(git_patch_from_patchfile(&patch, PATCH_TRUNCATED_2, + cl_git_fail(git_patch_from_buffer(&patch, PATCH_TRUNCATED_2, strlen(PATCH_TRUNCATED_2), NULL)); } void test_apply_fromfile__fail_truncated_3(void) { git_patch *patch; - cl_git_fail(git_patch_from_patchfile(&patch, PATCH_TRUNCATED_3, + cl_git_fail(git_patch_from_buffer(&patch, PATCH_TRUNCATED_3, strlen(PATCH_TRUNCATED_3), NULL)); } void test_apply_fromfile__fail_corrupt_githeader(void) { git_patch *patch; - cl_git_fail(git_patch_from_patchfile(&patch, PATCH_CORRUPT_GIT_HEADER, + cl_git_fail(git_patch_from_buffer(&patch, PATCH_CORRUPT_GIT_HEADER, strlen(PATCH_CORRUPT_GIT_HEADER), NULL)); } @@ -353,7 +354,7 @@ void test_apply_fromfile__append_no_nl(void) void test_apply_fromfile__fail_missing_new_file(void) { git_patch *patch; - cl_git_fail(git_patch_from_patchfile(&patch, + cl_git_fail(git_patch_from_buffer(&patch, PATCH_CORRUPT_MISSING_NEW_FILE, strlen(PATCH_CORRUPT_MISSING_NEW_FILE), NULL)); } @@ -361,7 +362,7 @@ void test_apply_fromfile__fail_missing_new_file(void) void test_apply_fromfile__fail_missing_old_file(void) { git_patch *patch; - cl_git_fail(git_patch_from_patchfile(&patch, + cl_git_fail(git_patch_from_buffer(&patch, PATCH_CORRUPT_MISSING_OLD_FILE, strlen(PATCH_CORRUPT_MISSING_OLD_FILE), NULL)); } @@ -369,7 +370,7 @@ void test_apply_fromfile__fail_missing_old_file(void) void test_apply_fromfile__fail_no_changes(void) { git_patch *patch; - cl_git_fail(git_patch_from_patchfile(&patch, + cl_git_fail(git_patch_from_buffer(&patch, PATCH_CORRUPT_NO_CHANGES, strlen(PATCH_CORRUPT_NO_CHANGES), NULL)); } @@ -377,7 +378,7 @@ void test_apply_fromfile__fail_no_changes(void) void test_apply_fromfile__fail_missing_hunk_header(void) { git_patch *patch; - cl_git_fail(git_patch_from_patchfile(&patch, + cl_git_fail(git_patch_from_buffer(&patch, PATCH_CORRUPT_MISSING_HUNK_HEADER, strlen(PATCH_CORRUPT_MISSING_HUNK_HEADER), NULL)); } @@ -385,7 +386,7 @@ void test_apply_fromfile__fail_missing_hunk_header(void) void test_apply_fromfile__fail_not_a_patch(void) { git_patch *patch; - cl_git_fail(git_patch_from_patchfile(&patch, PATCH_NOT_A_PATCH, + cl_git_fail(git_patch_from_buffer(&patch, PATCH_NOT_A_PATCH, strlen(PATCH_NOT_A_PATCH), NULL)); } @@ -442,6 +443,6 @@ void test_apply_fromfile__empty_file_not_allowed(void) { git_patch *patch; - cl_git_fail(git_patch_from_patchfile(&patch, "", 0, NULL)); - cl_git_fail(git_patch_from_patchfile(&patch, NULL, 0, NULL)); + cl_git_fail(git_patch_from_buffer(&patch, "", 0, NULL)); + cl_git_fail(git_patch_from_buffer(&patch, NULL, 0, NULL)); } diff --git a/tests/patch/parse.c b/tests/patch/parse.c index 28f61ffcdfa..88cdbf6d73c 100644 --- a/tests/patch/parse.c +++ b/tests/patch/parse.c @@ -1,4 +1,5 @@ #include "clar_libgit2.h" +#include "patch.h" #include "patch_common.h" @@ -8,7 +9,7 @@ void test_patch_parse__original_to_change_middle(void) const git_diff_delta *delta; char idstr[GIT_OID_HEXSZ+1] = {0}; - cl_git_pass(git_patch_from_patchfile( + cl_git_pass(git_patch_from_buffer( &patch, PATCH_ORIGINAL_TO_CHANGE_MIDDLE, strlen(PATCH_ORIGINAL_TO_CHANGE_MIDDLE), NULL)); diff --git a/tests/patch/print.c b/tests/patch/print.c index a07328fa814..047b48e8f3a 100644 --- a/tests/patch/print.c +++ b/tests/patch/print.c @@ -1,4 +1,5 @@ #include "clar_libgit2.h" +#include "patch.h" #include "patch_common.h" @@ -12,7 +13,7 @@ void patch_print_from_patchfile(const char *data, size_t len) git_patch *patch; git_buf buf = GIT_BUF_INIT; - cl_git_pass(git_patch_from_patchfile(&patch, data, len, NULL)); + cl_git_pass(git_patch_from_buffer(&patch, data, len, NULL)); cl_git_pass(git_patch_to_buf(&buf, patch)); cl_assert_equal_s(data, buf.ptr); From 53571f2f0c5cbb30e86aa0b8095f51c09c85761e Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sat, 21 Nov 2015 15:16:01 -0500 Subject: [PATCH 40/54] vector: more sensible names for `grow_at`/`shrink_at` --- src/apply.c | 4 ++-- src/vector.c | 20 ++++++++++---------- src/vector.h | 4 ++-- tests/core/vector.c | 24 ++++++++++++------------ 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/apply.c b/src/apply.c index a9b4935198e..876860754db 100644 --- a/src/apply.c +++ b/src/apply.c @@ -137,10 +137,10 @@ static int update_hunk( int error = 0; if (postlen > prelen) - error = git_vector_grow_at( + error = git_vector_insert_null( &image->lines, linenum, (postlen - prelen)); else if (prelen > postlen) - error = git_vector_shrink_at( + error = git_vector_remove_range( &image->lines, linenum, (prelen - postlen)); if (error) { diff --git a/src/vector.c b/src/vector.c index 5ad8a738ca7..db1dcf89ae3 100644 --- a/src/vector.c +++ b/src/vector.c @@ -331,33 +331,33 @@ int git_vector_resize_to(git_vector *v, size_t new_length) return 0; } -int git_vector_grow_at(git_vector *v, size_t idx, size_t grow_len) +int git_vector_insert_null(git_vector *v, size_t idx, size_t insert_len) { size_t new_length; - assert(grow_len > 0 && idx <= v->length); + assert(insert_len > 0 && idx <= v->length); - GITERR_CHECK_ALLOC_ADD(&new_length, v->length, grow_len); + GITERR_CHECK_ALLOC_ADD(&new_length, v->length, insert_len); if (new_length > v->_alloc_size && resize_vector(v, new_length) < 0) return -1; - memmove(&v->contents[idx + grow_len], &v->contents[idx], + memmove(&v->contents[idx + insert_len], &v->contents[idx], sizeof(void *) * (v->length - idx)); - memset(&v->contents[idx], 0, sizeof(void *) * grow_len); + memset(&v->contents[idx], 0, sizeof(void *) * insert_len); v->length = new_length; return 0; } -int git_vector_shrink_at(git_vector *v, size_t idx, size_t shrink_len) +int git_vector_remove_range(git_vector *v, size_t idx, size_t remove_len) { - size_t new_length = v->length - shrink_len; + size_t new_length = v->length - remove_len; size_t end_idx = 0; - assert(shrink_len > 0); + assert(remove_len > 0); - if (git__add_sizet_overflow(&end_idx, idx, shrink_len)) + if (git__add_sizet_overflow(&end_idx, idx, remove_len)) assert(0); assert(end_idx <= v->length); @@ -366,7 +366,7 @@ int git_vector_shrink_at(git_vector *v, size_t idx, size_t shrink_len) memmove(&v->contents[idx], &v->contents[end_idx], sizeof(void *) * (v->length - end_idx)); - memset(&v->contents[new_length], 0, sizeof(void *) * shrink_len); + memset(&v->contents[new_length], 0, sizeof(void *) * remove_len); v->length = new_length; return 0; diff --git a/src/vector.h b/src/vector.h index 6399a84842e..96d149e163d 100644 --- a/src/vector.h +++ b/src/vector.h @@ -93,8 +93,8 @@ void git_vector_remove_matching( void *payload); int git_vector_resize_to(git_vector *v, size_t new_length); -int git_vector_grow_at(git_vector *v, size_t idx, size_t grow_len); -int git_vector_shrink_at(git_vector *v, size_t idx, size_t shrink_len); +int git_vector_insert_null(git_vector *v, size_t idx, size_t insert_len); +int git_vector_remove_range(git_vector *v, size_t idx, size_t remove_len); int git_vector_set(void **old, git_vector *v, size_t position, void *value); diff --git a/tests/core/vector.c b/tests/core/vector.c index abc641a2c7d..c351655a7fc 100644 --- a/tests/core/vector.c +++ b/tests/core/vector.c @@ -338,40 +338,40 @@ void test_core_vector__grow_and_shrink(void) git_vector_insert(&x, (void *)0x09); git_vector_insert(&x, (void *)0x0a); - git_vector_shrink_at(&x, 0, 1); + git_vector_remove_range(&x, 0, 1); assert_vector(&x, expected1, ARRAY_SIZE(expected1)); - git_vector_shrink_at(&x, 1, 1); + git_vector_remove_range(&x, 1, 1); assert_vector(&x, expected2, ARRAY_SIZE(expected2)); - git_vector_shrink_at(&x, 4, 3); + git_vector_remove_range(&x, 4, 3); assert_vector(&x, expected3, ARRAY_SIZE(expected3)); - git_vector_shrink_at(&x, 3, 2); + git_vector_remove_range(&x, 3, 2); assert_vector(&x, expected4, ARRAY_SIZE(expected4)); - git_vector_grow_at(&x, 0, 2); + git_vector_insert_null(&x, 0, 2); assert_vector(&x, expected5, ARRAY_SIZE(expected5)); - git_vector_grow_at(&x, 5, 1); + git_vector_insert_null(&x, 5, 1); assert_vector(&x, expected6, ARRAY_SIZE(expected6)); - git_vector_grow_at(&x, 4, 3); + git_vector_insert_null(&x, 4, 3); assert_vector(&x, expected7, ARRAY_SIZE(expected7)); - git_vector_shrink_at(&x, 0, 3); + git_vector_remove_range(&x, 0, 3); assert_vector(&x, expected8, ARRAY_SIZE(expected8)); - git_vector_shrink_at(&x, 1, 2); + git_vector_remove_range(&x, 1, 2); assert_vector(&x, expected9, ARRAY_SIZE(expected9)); - git_vector_shrink_at(&x, 2, 2); + git_vector_remove_range(&x, 2, 2); assert_vector(&x, expectedA, ARRAY_SIZE(expectedA)); - git_vector_shrink_at(&x, 1, 1); + git_vector_remove_range(&x, 1, 1); assert_vector(&x, expectedB, ARRAY_SIZE(expectedB)); - git_vector_shrink_at(&x, 0, 1); + git_vector_remove_range(&x, 0, 1); assert_vector(&x, NULL, 0); git_vector_free(&x); From 8d44f8b78f7929c86b9e37acfe40fe707815bca6 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Tue, 24 Nov 2015 15:19:59 -0500 Subject: [PATCH 41/54] patch: `patch_diff` -> `patch_generated` --- src/diff_print.c | 2 +- src/diff_stats.c | 2 +- src/diff_xdiff.c | 16 +-- src/diff_xdiff.h | 10 +- src/{patch_diff.c => patch_generate.c} | 166 +++++++++++++------------ src/{patch_diff.h => patch_generate.h} | 35 +++--- 6 files changed, 119 insertions(+), 112 deletions(-) rename src/{patch_diff.c => patch_generate.c} (82%) rename src/{patch_diff.h => patch_generate.h} (55%) diff --git a/src/diff_print.c b/src/diff_print.c index e5238699169..5bcb5d016c7 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -7,7 +7,7 @@ #include "common.h" #include "diff.h" #include "diff_file.h" -#include "patch_diff.h" +#include "patch_generate.h" #include "fileops.h" #include "zstream.h" #include "blob.h" diff --git a/src/diff_stats.c b/src/diff_stats.c index f2eb6968035..9c186d79327 100644 --- a/src/diff_stats.c +++ b/src/diff_stats.c @@ -7,7 +7,7 @@ #include "common.h" #include "vector.h" #include "diff.h" -#include "patch_diff.h" +#include "patch_generate.h" #define DIFF_RENAME_FILE_SEPARATOR " => " #define STATS_FULL_MIN_SCALE 7 diff --git a/src/diff_xdiff.c b/src/diff_xdiff.c index 8017c954118..5bd6381b5ac 100644 --- a/src/diff_xdiff.c +++ b/src/diff_xdiff.c @@ -9,7 +9,7 @@ #include "diff.h" #include "diff_driver.h" #include "diff_xdiff.h" -#include "patch_diff.h" +#include "patch_generate.h" static int git_xdiff_scan_int(const char **str, int *value) { @@ -56,7 +56,7 @@ static int git_xdiff_parse_hunk(git_diff_hunk *hunk, const char *header) typedef struct { git_xdiff_output *xo; - git_patch_diff *patch; + git_patch_generated *patch; git_diff_hunk hunk; int old_lineno, new_lineno; mmfile_t xd_old_data, xd_new_data; @@ -110,9 +110,9 @@ static int diff_update_lines( static int git_xdiff_cb(void *priv, mmbuffer_t *bufs, int len) { git_xdiff_info *info = priv; - git_patch_diff *patch = info->patch; + git_patch_generated *patch = info->patch; const git_diff_delta *delta = patch->base.delta; - git_patch_diff_output *output = &info->xo->output; + git_patch_generated_output *output = &info->xo->output; git_diff_line line; if (len == 1) { @@ -181,7 +181,7 @@ static int git_xdiff_cb(void *priv, mmbuffer_t *bufs, int len) return output->error; } -static int git_xdiff(git_patch_diff_output *output, git_patch_diff *patch) +static int git_xdiff(git_patch_generated_output *output, git_patch_generated *patch) { git_xdiff_output *xo = (git_xdiff_output *)output; git_xdiff_info info; @@ -194,7 +194,7 @@ static int git_xdiff(git_patch_diff_output *output, git_patch_diff *patch) xo->callback.priv = &info; git_diff_find_context_init( - &xo->config.find_func, &findctxt, git_patch_diff_driver(patch)); + &xo->config.find_func, &findctxt, git_patch_generated_driver(patch)); xo->config.find_func_priv = &findctxt; if (xo->config.find_func != NULL) @@ -206,8 +206,8 @@ static int git_xdiff(git_patch_diff_output *output, git_patch_diff *patch) * updates are needed to xo->params.flags */ - git_patch_diff_old_data(&info.xd_old_data.ptr, &info.xd_old_data.size, patch); - git_patch_diff_new_data(&info.xd_new_data.ptr, &info.xd_new_data.size, patch); + git_patch_generated_old_data(&info.xd_old_data.ptr, &info.xd_old_data.size, patch); + git_patch_generated_new_data(&info.xd_new_data.ptr, &info.xd_new_data.size, patch); if (info.xd_old_data.size > GIT_XDIFF_MAX_SIZE || info.xd_new_data.size > GIT_XDIFF_MAX_SIZE) { diff --git a/src/diff_xdiff.h b/src/diff_xdiff.h index e1c58a22923..88375986b32 100644 --- a/src/diff_xdiff.h +++ b/src/diff_xdiff.h @@ -9,19 +9,19 @@ #include "diff.h" #include "xdiff/xdiff.h" -#include "patch_diff.h" +#include "patch_generate.h" /* xdiff cannot cope with large files. these files should not be passed to * xdiff. callers should treat these large files as binary. */ #define GIT_XDIFF_MAX_SIZE (1024LL * 1024 * 1023) -/* A git_xdiff_output is a git_patch_diff_output with extra fields necessary - * to use libxdiff. Calling git_xdiff_init() will set the diff_cb field - * of the output to use xdiff to generate the diffs. +/* A git_xdiff_output is a git_patch_generate_output with extra fields + * necessary to use libxdiff. Calling git_xdiff_init() will set the diff_cb + * field of the output to use xdiff to generate the diffs. */ typedef struct { - git_patch_diff_output output; + git_patch_generated_output output; xdemitconf_t config; xpparam_t params; diff --git a/src/patch_diff.c b/src/patch_generate.c similarity index 82% rename from src/patch_diff.c rename to src/patch_generate.c index bae0bd22725..80a5a552aea 100644 --- a/src/patch_diff.c +++ b/src/patch_generate.c @@ -9,21 +9,22 @@ #include "diff.h" #include "diff_file.h" #include "diff_driver.h" -#include "patch_diff.h" +#include "patch_generate.h" #include "diff_xdiff.h" #include "delta.h" #include "zstream.h" #include "fileops.h" static void diff_output_init( - git_patch_diff_output *, const git_diff_options *, git_diff_file_cb, + git_patch_generated_output *, const git_diff_options *, git_diff_file_cb, git_diff_binary_cb, git_diff_hunk_cb, git_diff_line_cb, void*); -static void diff_output_to_patch(git_patch_diff_output *, git_patch_diff *); +static void diff_output_to_patch( + git_patch_generated_output *, git_patch_generated *); -static void patch_diff_free(git_patch *p) +static void patch_generated_free(git_patch *p) { - git_patch_diff *patch = (git_patch_diff *)p; + git_patch_generated *patch = (git_patch_generated *)p; git_array_clear(patch->base.lines); git_array_clear(patch->base.hunks); @@ -42,11 +43,11 @@ static void patch_diff_free(git_patch *p) git__free((char *)patch->base.diff_opts.old_prefix); git__free((char *)patch->base.diff_opts.new_prefix); - if (patch->flags & GIT_PATCH_DIFF_ALLOCATED) + if (patch->flags & GIT_PATCH_GENERATED_ALLOCATED) git__free(patch); } -static void patch_diff_update_binary(git_patch_diff *patch) +static void patch_generated_update_binary(git_patch_generated *patch) { if ((patch->base.delta->flags & DIFF_FLAGS_KNOWN_BINARY) != 0) return; @@ -64,19 +65,19 @@ static void patch_diff_update_binary(git_patch_diff *patch) patch->base.delta->flags |= GIT_DIFF_FLAG_NOT_BINARY; } -static void patch_diff_init_common(git_patch_diff *patch) +static void patch_generated_init_common(git_patch_generated *patch) { - patch->base.free_fn = patch_diff_free; + patch->base.free_fn = patch_generated_free; - patch_diff_update_binary(patch); + patch_generated_update_binary(patch); - patch->flags |= GIT_PATCH_DIFF_INITIALIZED; + patch->flags |= GIT_PATCH_GENERATED_INITIALIZED; if (patch->diff) git_diff_addref(patch->diff); } -static int patch_diff_normalize_options( +static int patch_generated_normalize_options( git_diff_options *out, const git_diff_options *opts) { @@ -102,8 +103,8 @@ static int patch_diff_normalize_options( return 0; } -static int patch_diff_init( - git_patch_diff *patch, git_diff *diff, size_t delta_index) +static int patch_generated_init( + git_patch_generated *patch, git_diff *diff, size_t delta_index) { int error = 0; @@ -114,7 +115,7 @@ static int patch_diff_init( patch->base.delta = git_vector_get(&diff->deltas, delta_index); patch->delta_index = delta_index; - if ((error = patch_diff_normalize_options( + if ((error = patch_generated_normalize_options( &patch->base.diff_opts, &diff->opts)) < 0 || (error = git_diff_file_content__init_from_diff( &patch->ofile, diff, patch->base.delta, true)) < 0 || @@ -122,20 +123,20 @@ static int patch_diff_init( &patch->nfile, diff, patch->base.delta, false)) < 0) return error; - patch_diff_init_common(patch); + patch_generated_init_common(patch); return 0; } -static int patch_diff_alloc_from_diff( - git_patch_diff **out, git_diff *diff, size_t delta_index) +static int patch_generated_alloc_from_diff( + git_patch_generated **out, git_diff *diff, size_t delta_index) { int error; - git_patch_diff *patch = git__calloc(1, sizeof(git_patch_diff)); + git_patch_generated *patch = git__calloc(1, sizeof(git_patch_generated)); GITERR_CHECK_ALLOC(patch); - if (!(error = patch_diff_init(patch, diff, delta_index))) { - patch->flags |= GIT_PATCH_DIFF_ALLOCATED; + if (!(error = patch_generated_init(patch, diff, delta_index))) { + patch->flags |= GIT_PATCH_GENERATED_ALLOCATED; GIT_REFCOUNT_INC(patch); } else { git__free(patch); @@ -146,7 +147,7 @@ static int patch_diff_alloc_from_diff( return error; } -GIT_INLINE(bool) should_skip_binary(git_patch_diff *patch, git_diff_file *file) +GIT_INLINE(bool) should_skip_binary(git_patch_generated *patch, git_diff_file *file) { if ((patch->base.diff_opts.flags & GIT_DIFF_SHOW_BINARY) != 0) return false; @@ -154,7 +155,7 @@ GIT_INLINE(bool) should_skip_binary(git_patch_diff *patch, git_diff_file *file) return (file->flags & GIT_DIFF_FLAG_BINARY) != 0; } -static bool patch_diff_diffable(git_patch_diff *patch) +static bool patch_generated_diffable(git_patch_generated *patch) { size_t olen, nlen; @@ -183,12 +184,12 @@ static bool patch_diff_diffable(git_patch_diff *patch) !git_oid_equal(&patch->ofile.file->id, &patch->nfile.file->id)); } -static int patch_diff_load(git_patch_diff *patch, git_patch_diff_output *output) +static int patch_generated_load(git_patch_generated *patch, git_patch_generated_output *output) { int error = 0; bool incomplete_data; - if ((patch->flags & GIT_PATCH_DIFF_LOADED) != 0) + if ((patch->flags & GIT_PATCH_GENERATED_LOADED) != 0) return 0; /* if no hunk and data callbacks and user doesn't care if data looks @@ -245,20 +246,20 @@ static int patch_diff_load(git_patch_diff *patch, git_patch_diff_output *output) patch->base.delta->status = GIT_DELTA_UNMODIFIED; cleanup: - patch_diff_update_binary(patch); + patch_generated_update_binary(patch); if (!error) { - if (patch_diff_diffable(patch)) - patch->flags |= GIT_PATCH_DIFF_DIFFABLE; + if (patch_generated_diffable(patch)) + patch->flags |= GIT_PATCH_GENERATED_DIFFABLE; - patch->flags |= GIT_PATCH_DIFF_LOADED; + patch->flags |= GIT_PATCH_GENERATED_LOADED; } return error; } -static int patch_diff_invoke_file_callback( - git_patch_diff *patch, git_patch_diff_output *output) +static int patch_generated_invoke_file_callback( + git_patch_generated *patch, git_patch_generated_output *output) { float progress = patch->diff ? ((float)patch->delta_index / patch->diff->deltas.length) : 1.0f; @@ -338,7 +339,7 @@ static int create_binary( return error; } -static int diff_binary(git_patch_diff_output *output, git_patch_diff *patch) +static int diff_binary(git_patch_generated_output *output, git_patch_generated *patch) { git_diff_binary binary = {{0}}; const char *old_data = patch->ofile.map.data; @@ -372,22 +373,24 @@ static int diff_binary(git_patch_diff_output *output, git_patch_diff *patch) return error; } -static int patch_diff_generate(git_patch_diff *patch, git_patch_diff_output *output) +static int patch_generated_create( + git_patch_generated *patch, + git_patch_generated_output *output) { int error = 0; - if ((patch->flags & GIT_PATCH_DIFF_DIFFED) != 0) + if ((patch->flags & GIT_PATCH_GENERATED_DIFFED) != 0) return 0; /* if we are not looking at the binary or text data, don't do the diff */ if (!output->binary_cb && !output->hunk_cb && !output->data_cb) return 0; - if ((patch->flags & GIT_PATCH_DIFF_LOADED) == 0 && - (error = patch_diff_load(patch, output)) < 0) + if ((patch->flags & GIT_PATCH_GENERATED_LOADED) == 0 && + (error = patch_generated_load(patch, output)) < 0) return error; - if ((patch->flags & GIT_PATCH_DIFF_DIFFABLE) == 0) + if ((patch->flags & GIT_PATCH_GENERATED_DIFFABLE) == 0) return 0; if ((patch->base.delta->flags & GIT_DIFF_FLAG_BINARY) != 0) { @@ -399,7 +402,7 @@ static int patch_diff_generate(git_patch_diff *patch, git_patch_diff_output *out error = output->diff_cb(output, patch); } - patch->flags |= GIT_PATCH_DIFF_DIFFED; + patch->flags |= GIT_PATCH_GENERATED_DIFFED; return error; } @@ -422,7 +425,7 @@ int git_diff_foreach( int error = 0; git_xdiff_output xo; size_t idx; - git_patch_diff patch; + git_patch_generated patch; if ((error = diff_required(diff, "git_diff_foreach")) < 0) return error; @@ -440,14 +443,14 @@ int git_diff_foreach( continue; if (binary_cb || hunk_cb || data_cb) { - if ((error = patch_diff_init(&patch, diff, idx)) != 0 || - (error = patch_diff_load(&patch, &xo.output)) != 0) + if ((error = patch_generated_init(&patch, diff, idx)) != 0 || + (error = patch_generated_load(&patch, &xo.output)) != 0) return error; } - if ((error = patch_diff_invoke_file_callback(&patch, &xo.output)) == 0) { + if ((error = patch_generated_invoke_file_callback(&patch, &xo.output)) == 0) { if (binary_cb || hunk_cb || data_cb) - error = patch_diff_generate(&patch, &xo.output); + error = patch_generated_create(&patch, &xo.output); } git_patch_free(&patch.base); @@ -460,15 +463,15 @@ int git_diff_foreach( } typedef struct { - git_patch_diff patch; + git_patch_generated patch; git_diff_delta delta; char paths[GIT_FLEX_ARRAY]; -} patch_diff_with_delta; +} patch_generated_with_delta; -static int diff_single_generate(patch_diff_with_delta *pd, git_xdiff_output *xo) +static int diff_single_generate(patch_generated_with_delta *pd, git_xdiff_output *xo) { int error = 0; - git_patch_diff *patch = &pd->patch; + git_patch_generated *patch = &pd->patch; bool has_old = ((patch->ofile.flags & GIT_DIFF_FLAG__NO_DATA) == 0); bool has_new = ((patch->nfile.flags & GIT_DIFF_FLAG__NO_DATA) == 0); @@ -481,22 +484,22 @@ static int diff_single_generate(patch_diff_with_delta *pd, git_xdiff_output *xo) patch->base.delta = &pd->delta; - patch_diff_init_common(patch); + patch_generated_init_common(patch); if (pd->delta.status == GIT_DELTA_UNMODIFIED && !(patch->ofile.opts_flags & GIT_DIFF_INCLUDE_UNMODIFIED)) return error; - error = patch_diff_invoke_file_callback(patch, (git_patch_diff_output *)xo); + error = patch_generated_invoke_file_callback(patch, (git_patch_generated_output *)xo); if (!error) - error = patch_diff_generate(patch, (git_patch_diff_output *)xo); + error = patch_generated_create(patch, (git_patch_generated_output *)xo); return error; } -static int patch_diff_from_sources( - patch_diff_with_delta *pd, +static int patch_generated_from_sources( + patch_generated_with_delta *pd, git_xdiff_output *xo, git_diff_file_content_src *oldsrc, git_diff_file_content_src *newsrc, @@ -509,7 +512,7 @@ static int patch_diff_from_sources( git_diff_file *lfile = &pd->delta.old_file, *rfile = &pd->delta.new_file; git_diff_file_content *ldata = &pd->patch.ofile, *rdata = &pd->patch.nfile; - if ((error = patch_diff_normalize_options(&pd->patch.base.diff_opts, opts)) < 0) + if ((error = patch_generated_normalize_options(&pd->patch.base.diff_opts, opts)) < 0) return error; if (opts && (opts->flags & GIT_DIFF_REVERSE) != 0) { @@ -540,12 +543,12 @@ static int patch_diff_from_sources( return diff_single_generate(pd, xo); } -static int patch_diff_with_delta_alloc( - patch_diff_with_delta **out, +static int patch_generated_with_delta_alloc( + patch_generated_with_delta **out, const char **old_path, const char **new_path) { - patch_diff_with_delta *pd; + patch_generated_with_delta *pd; size_t old_len = *old_path ? strlen(*old_path) : 0; size_t new_len = *new_path ? strlen(*new_path) : 0; size_t alloc_len; @@ -557,7 +560,7 @@ static int patch_diff_with_delta_alloc( *out = pd = git__calloc(1, alloc_len); GITERR_CHECK_ALLOC(pd); - pd->patch.flags = GIT_PATCH_DIFF_ALLOCATED; + pd->patch.flags = GIT_PATCH_GENERATED_ALLOCATED; if (*old_path) { memcpy(&pd->paths[0], *old_path, old_len); @@ -585,7 +588,7 @@ static int diff_from_sources( void *payload) { int error = 0; - patch_diff_with_delta pd; + patch_generated_with_delta pd; git_xdiff_output xo; memset(&xo, 0, sizeof(xo)); @@ -595,7 +598,7 @@ static int diff_from_sources( memset(&pd, 0, sizeof(pd)); - error = patch_diff_from_sources(&pd, &xo, oldsrc, newsrc, opts); + error = patch_generated_from_sources(&pd, &xo, oldsrc, newsrc, opts); git_patch_free(&pd.patch.base); @@ -609,13 +612,13 @@ static int patch_from_sources( const git_diff_options *opts) { int error = 0; - patch_diff_with_delta *pd; + patch_generated_with_delta *pd; git_xdiff_output xo; assert(out); *out = NULL; - if ((error = patch_diff_with_delta_alloc( + if ((error = patch_generated_with_delta_alloc( &pd, &oldsrc->as_path, &newsrc->as_path)) < 0) return error; @@ -623,7 +626,7 @@ static int patch_from_sources( diff_output_to_patch(&xo.output, &pd->patch); git_xdiff_init(&xo, opts); - if (!(error = patch_diff_from_sources(pd, &xo, oldsrc, newsrc, opts))) + if (!(error = patch_generated_from_sources(pd, &xo, oldsrc, newsrc, opts))) *out = (git_patch *)pd; else git_patch_free((git_patch *)pd); @@ -748,7 +751,7 @@ int git_patch_from_diff( int error = 0; git_xdiff_output xo; git_diff_delta *delta = NULL; - git_patch_diff *patch = NULL; + git_patch_generated *patch = NULL; if (patch_ptr) *patch_ptr = NULL; @@ -770,17 +773,17 @@ int git_patch_from_diff( (diff->opts.flags & GIT_DIFF_SKIP_BINARY_CHECK) != 0)) return 0; - if ((error = patch_diff_alloc_from_diff(&patch, diff, idx)) < 0) + if ((error = patch_generated_alloc_from_diff(&patch, diff, idx)) < 0) return error; memset(&xo, 0, sizeof(xo)); diff_output_to_patch(&xo.output, patch); git_xdiff_init(&xo, &diff->opts); - error = patch_diff_invoke_file_callback(patch, &xo.output); + error = patch_generated_invoke_file_callback(patch, &xo.output); if (!error) - error = patch_diff_generate(patch, &xo.output); + error = patch_generated_create(patch, &xo.output); if (!error) { /* TODO: if cumulative diff size is < 0.5 total size, flatten patch */ @@ -795,27 +798,27 @@ int git_patch_from_diff( return error; } -git_diff_driver *git_patch_diff_driver(git_patch_diff *patch) +git_diff_driver *git_patch_generated_driver(git_patch_generated *patch) { /* ofile driver is representative for whole patch */ return patch->ofile.driver; } -void git_patch_diff_old_data( - char **ptr, size_t *len, git_patch_diff *patch) +void git_patch_generated_old_data( + char **ptr, size_t *len, git_patch_generated *patch) { *ptr = patch->ofile.map.data; *len = patch->ofile.map.len; } -void git_patch_diff_new_data( - char **ptr, size_t *len, git_patch_diff *patch) +void git_patch_generated_new_data( + char **ptr, size_t *len, git_patch_generated *patch) { *ptr = patch->nfile.map.data; *len = patch->nfile.map.len; } -static int patch_diff_file_cb( +static int patch_generated_file_cb( const git_diff_delta *delta, float progress, void *payload) @@ -824,7 +827,7 @@ static int patch_diff_file_cb( return 0; } -static int patch_diff_binary_cb( +static int patch_generated_binary_cb( const git_diff_delta *delta, const git_diff_binary *binary, void *payload) @@ -859,7 +862,7 @@ static int git_patch_hunk_cb( const git_diff_hunk *hunk_, void *payload) { - git_patch_diff *patch = payload; + git_patch_generated *patch = payload; git_patch_hunk *hunk; GIT_UNUSED(delta); @@ -877,13 +880,13 @@ static int git_patch_hunk_cb( return 0; } -static int patch_diff_line_cb( +static int patch_generated_line_cb( const git_diff_delta *delta, const git_diff_hunk *hunk_, const git_diff_line *line_, void *payload) { - git_patch_diff *patch = payload; + git_patch_generated *patch = payload; git_patch_hunk *hunk; git_diff_line *line; @@ -917,7 +920,7 @@ static int patch_diff_line_cb( } static void diff_output_init( - git_patch_diff_output *out, + git_patch_generated_output *out, const git_diff_options *opts, git_diff_file_cb file_cb, git_diff_binary_cb binary_cb, @@ -936,14 +939,15 @@ static void diff_output_init( out->payload = payload; } -static void diff_output_to_patch(git_patch_diff_output *out, git_patch_diff *patch) +static void diff_output_to_patch( + git_patch_generated_output *out, git_patch_generated *patch) { diff_output_init( out, NULL, - patch_diff_file_cb, - patch_diff_binary_cb, + patch_generated_file_cb, + patch_generated_binary_cb, git_patch_hunk_cb, - patch_diff_line_cb, + patch_generated_line_cb, patch); } diff --git a/src/patch_diff.h b/src/patch_generate.h similarity index 55% rename from src/patch_diff.h rename to src/patch_generate.h index 076acdf4363..bb26a76e54b 100644 --- a/src/patch_diff.h +++ b/src/patch_generate.h @@ -4,8 +4,8 @@ * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ -#ifndef INCLUDE_diff_patch_h__ -#define INCLUDE_diff_patch_h__ +#ifndef INCLUDE_patch_generate_h__ +#define INCLUDE_patch_generate_h__ #include "common.h" #include "diff.h" @@ -13,17 +13,17 @@ #include "patch.h" enum { - GIT_PATCH_DIFF_ALLOCATED = (1 << 0), - GIT_PATCH_DIFF_INITIALIZED = (1 << 1), - GIT_PATCH_DIFF_LOADED = (1 << 2), + GIT_PATCH_GENERATED_ALLOCATED = (1 << 0), + GIT_PATCH_GENERATED_INITIALIZED = (1 << 1), + GIT_PATCH_GENERATED_LOADED = (1 << 2), /* the two sides are different */ - GIT_PATCH_DIFF_DIFFABLE = (1 << 3), + GIT_PATCH_GENERATED_DIFFABLE = (1 << 3), /* the difference between the two sides has been computed */ - GIT_PATCH_DIFF_DIFFED = (1 << 4), - GIT_PATCH_DIFF_FLATTENED = (1 << 5), + GIT_PATCH_GENERATED_DIFFED = (1 << 4), + GIT_PATCH_GENERATED_FLATTENED = (1 << 5), }; -struct git_patch_diff { +struct git_patch_generated { struct git_patch base; git_diff *diff; /* for refcount purposes, maybe NULL for blob diffs */ @@ -34,16 +34,18 @@ struct git_patch_diff { git_pool flattened; }; -typedef struct git_patch_diff git_patch_diff; +typedef struct git_patch_generated git_patch_generated; -extern git_diff_driver *git_patch_diff_driver(git_patch_diff *); +extern git_diff_driver *git_patch_generated_driver(git_patch_generated *); -extern void git_patch_diff_old_data(char **, size_t *, git_patch_diff *); -extern void git_patch_diff_new_data(char **, size_t *, git_patch_diff *); +extern void git_patch_generated_old_data( + char **, size_t *, git_patch_generated *); +extern void git_patch_generated_new_data( + char **, size_t *, git_patch_generated *); -typedef struct git_patch_diff_output git_patch_diff_output; +typedef struct git_patch_generated_output git_patch_generated_output; -struct git_patch_diff_output { +struct git_patch_generated_output { /* these callbacks are issued with the diff data */ git_diff_file_cb file_cb; git_diff_binary_cb binary_cb; @@ -57,7 +59,8 @@ struct git_patch_diff_output { /* this callback is used to do the diff and drive the other callbacks. * see diff_xdiff.h for how to use this in practice for now. */ - int (*diff_cb)(git_patch_diff_output *output, git_patch_diff *patch); + int (*diff_cb)(git_patch_generated_output *output, + git_patch_generated *patch); }; #endif From aa4bfb32b9662ab47b056a74211636d427f35a43 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sun, 7 Feb 2016 15:08:16 -0800 Subject: [PATCH 42/54] parse: introduce parse_ctx_contains_s --- src/patch_parse.c | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/patch_parse.c b/src/patch_parse.c index 9421bb39b30..5c771d94ae7 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -42,6 +42,15 @@ typedef struct { } patch_parse_ctx; +GIT_INLINE(bool) parse_ctx_contains( + patch_parse_ctx *ctx, const char *str, size_t len) +{ + return (ctx->line_len >= len && memcmp(ctx->line, str, len) == 0); +} + +#define parse_ctx_contains_s(ctx, str) \ + parse_ctx_contains(ctx, str, sizeof(str) - 1) + static void parse_advance_line(patch_parse_ctx *ctx) { ctx->line += ctx->line_len; @@ -589,11 +598,11 @@ static int parse_hunk_body( } /* Handle "\ No newline at end of file". Only expect the leading - * backslash, though, because the rest of the string could be - * localized. Because `diff` optimizes for the case where you - * want to apply the patch by hand. - */ - if (ctx->line_len >= 2 && memcmp(ctx->line, "\\ ", 2) == 0 && + * backslash, though, because the rest of the string could be + * localized. Because `diff` optimizes for the case where you + * want to apply the patch by hand. + */ + if (parse_ctx_contains_s(ctx, "\\ ") && git_array_size(patch->base.lines) > 0) { line = git_array_get(patch->base.lines, git_array_size(patch->base.lines) - 1); @@ -624,8 +633,8 @@ static int parsed_patch_header( continue; /* This might be a hunk header without a patch header, provide a - * sensible error message. */ - if (memcmp(ctx->line, "@@ -", 4) == 0) { + * sensible error message. */ + if (parse_ctx_contains_s(ctx, "@@ -")) { size_t line_num = ctx->line_num; git_patch_hunk hunk; @@ -647,7 +656,7 @@ static int parsed_patch_header( break; /* A proper git patch */ - if (ctx->line_len >= 11 && memcmp(ctx->line, "diff --git ", 11) == 0) { + if (parse_ctx_contains_s(ctx, "diff --git ")) { error = parse_header_git(patch, ctx); goto done; } @@ -671,16 +680,15 @@ static int parsed_patch_binary_side( git_off_t len; int error = 0; - if (ctx->line_len >= 8 && memcmp(ctx->line, "literal ", 8) == 0) { + if (parse_ctx_contains_s(ctx, "literal ")) { type = GIT_DIFF_BINARY_LITERAL; parse_advance_chars(ctx, 8); - } - else if (ctx->line_len >= 6 && memcmp(ctx->line, "delta ", 6) == 0) { + } else if (parse_ctx_contains_s(ctx, "delta ")) { type = GIT_DIFF_BINARY_DELTA; parse_advance_chars(ctx, 6); - } - else { - error = parse_err("unknown binary delta type at line %d", ctx->line_num); + } else { + error = parse_err( + "unknown binary delta type at line %d", ctx->line_num); goto done; } @@ -777,8 +785,7 @@ static int parsed_patch_hunks( git_patch_hunk *hunk; int error = 0; - for (; ctx->line_len > 4 && memcmp(ctx->line, "@@ -", 4) == 0; ) { - + while (parse_ctx_contains_s(ctx, "@@ -")) { hunk = git_array_alloc(patch->base.hunks); GITERR_CHECK_ALLOC(hunk); @@ -799,10 +806,10 @@ static int parsed_patch_hunks( static int parsed_patch_body( git_patch_parsed *patch, patch_parse_ctx *ctx) { - if (ctx->line_len >= 16 && memcmp(ctx->line, "GIT binary patch", 16) == 0) + if (parse_ctx_contains_s(ctx, "GIT binary patch")) return parsed_patch_binary(patch, ctx); - else if (ctx->line_len >= 4 && memcmp(ctx->line, "@@ -", 4) == 0) + else if (parse_ctx_contains_s(ctx, "@@ -")) return parsed_patch_hunks(patch, ctx); return 0; From 9be638ecf0d64ec98b3fd16f2d983a86d1aca131 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Tue, 19 Apr 2016 15:12:18 -0400 Subject: [PATCH 43/54] git_diff_generated: abstract generated diffs --- src/checkout.c | 1 + src/diff.c | 1579 +-------------------------------- src/diff.h | 132 +-- src/diff_file.c | 1 + src/diff_generate.c | 1627 ++++++++++++++++++++++++++++++++++ src/diff_generate.h | 123 +++ src/diff_tform.c | 1 + src/diff_tform.h | 22 + src/merge.c | 2 + src/patch_generate.c | 1 + src/stash.c | 1 + src/status.c | 1 + tests/diff/format_email.c | 1 + tests/diff/stats.c | 1 + tests/merge/trees/treediff.c | 1 + 15 files changed, 1822 insertions(+), 1672 deletions(-) create mode 100644 src/diff_generate.c create mode 100644 src/diff_generate.h create mode 100644 src/diff_tform.h diff --git a/src/checkout.c b/src/checkout.c index b3e95dff82c..6e3b93fd311 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -26,6 +26,7 @@ #include "filter.h" #include "blob.h" #include "diff.h" +#include "diff_generate.h" #include "pathspec.h" #include "buf_text.h" #include "diff_xdiff.h" diff --git a/src/diff.c b/src/diff.c index 9c27511f6a1..c54d3574bec 100644 --- a/src/diff.c +++ b/src/diff.c @@ -4,279 +4,21 @@ * 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/version.h" #include "common.h" #include "diff.h" -#include "fileops.h" -#include "config.h" -#include "attr_file.h" -#include "filter.h" -#include "pathspec.h" +#include "diff_generate.h" +#include "patch.h" +#include "commit.h" #include "index.h" -#include "odb.h" -#include "submodule.h" -#define DIFF_FLAG_IS_SET(DIFF,FLAG) (((DIFF)->opts.flags & (FLAG)) != 0) -#define DIFF_FLAG_ISNT_SET(DIFF,FLAG) (((DIFF)->opts.flags & (FLAG)) == 0) +#define DIFF_FLAG_IS_SET(DIFF,FLAG) \ + (((DIFF)->opts.flags & (FLAG)) != 0) +#define DIFF_FLAG_ISNT_SET(DIFF,FLAG) \ + (((DIFF)->opts.flags & (FLAG)) == 0) #define DIFF_FLAG_SET(DIFF,FLAG,VAL) (DIFF)->opts.flags = \ (VAL) ? ((DIFF)->opts.flags | (FLAG)) : ((DIFF)->opts.flags & ~(VAL)) -static git_diff_delta *diff_delta__alloc( - git_diff *diff, - git_delta_t status, - const char *path) -{ - git_diff_delta *delta = git__calloc(1, sizeof(git_diff_delta)); - if (!delta) - return NULL; - - delta->old_file.path = git_pool_strdup(&diff->pool, path); - if (delta->old_file.path == NULL) { - git__free(delta); - return NULL; - } - - delta->new_file.path = delta->old_file.path; - - 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; - default: break; /* leave other status values alone */ - } - } - delta->status = status; - - return delta; -} - -static int diff_insert_delta( - git_diff *diff, git_diff_delta *delta, const char *matched_pathspec) -{ - int error = 0; - - if (diff->opts.notify_cb) { - error = diff->opts.notify_cb( - diff, delta, matched_pathspec, diff->opts.payload); - - if (error) { - git__free(delta); - - if (error > 0) /* positive value means to skip this delta */ - return 0; - else /* negative value means to cancel diff */ - return giterr_set_after_callback_function(error, "git_diff"); - } - } - - if ((error = git_vector_insert(&diff->deltas, delta)) < 0) - git__free(delta); - - return error; -} - -static bool diff_pathspec_match( - const char **matched_pathspec, - git_diff *diff, - const git_index_entry *entry) -{ - bool disable_pathspec_match = - DIFF_FLAG_IS_SET(diff, GIT_DIFF_DISABLE_PATHSPEC_MATCH); - - /* If we're disabling fnmatch, then the iterator has already applied - * the filters to the files for us and we don't have to do anything. - * However, this only applies to *files* - the iterator will include - * directories that we need to recurse into when not autoexpanding, - * so we still need to apply the pathspec match to directories. - */ - if ((S_ISLNK(entry->mode) || S_ISREG(entry->mode)) && - disable_pathspec_match) { - *matched_pathspec = entry->path; - return true; - } - - return git_pathspec__match( - &diff->pathspec, entry->path, disable_pathspec_match, - DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_CASE), - matched_pathspec, NULL); -} - -static int diff_delta__from_one( - git_diff *diff, - git_delta_t status, - const git_index_entry *oitem, - const git_index_entry *nitem) -{ - const git_index_entry *entry = nitem; - bool has_old = false; - git_diff_delta *delta; - const char *matched_pathspec; - - assert((oitem != NULL) ^ (nitem != NULL)); - - if (oitem) { - entry = oitem; - has_old = true; - } - - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) - has_old = !has_old; - - if ((entry->flags & GIT_IDXENTRY_VALID) != 0) - return 0; - - if (status == GIT_DELTA_IGNORED && - DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_IGNORED)) - return 0; - - if (status == GIT_DELTA_UNTRACKED && - DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNTRACKED)) - return 0; - - if (status == GIT_DELTA_UNREADABLE && - DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNREADABLE)) - return 0; - - if (!diff_pathspec_match(&matched_pathspec, diff, entry)) - return 0; - - delta = diff_delta__alloc(diff, status, entry->path); - GITERR_CHECK_ALLOC(delta); - - /* This fn is just for single-sided diffs */ - assert(status != GIT_DELTA_MODIFIED); - delta->nfiles = 1; - - if (has_old) { - delta->old_file.mode = entry->mode; - delta->old_file.size = entry->file_size; - delta->old_file.flags |= GIT_DIFF_FLAG_EXISTS; - git_oid_cpy(&delta->old_file.id, &entry->id); - delta->old_file.id_abbrev = GIT_OID_HEXSZ; - } else /* ADDED, IGNORED, UNTRACKED */ { - delta->new_file.mode = entry->mode; - delta->new_file.size = entry->file_size; - delta->new_file.flags |= GIT_DIFF_FLAG_EXISTS; - git_oid_cpy(&delta->new_file.id, &entry->id); - delta->new_file.id_abbrev = GIT_OID_HEXSZ; - } - - delta->old_file.flags |= GIT_DIFF_FLAG_VALID_ID; - - if (has_old || !git_oid_iszero(&delta->new_file.id)) - delta->new_file.flags |= GIT_DIFF_FLAG_VALID_ID; - - return diff_insert_delta(diff, delta, matched_pathspec); -} - -static int diff_delta__from_two( - git_diff *diff, - git_delta_t status, - const git_index_entry *old_entry, - uint32_t old_mode, - const git_index_entry *new_entry, - uint32_t new_mode, - const git_oid *new_id, - const char *matched_pathspec) -{ - const git_oid *old_id = &old_entry->id; - git_diff_delta *delta; - const char *canonical_path = old_entry->path; - - if (status == GIT_DELTA_UNMODIFIED && - DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNMODIFIED)) - return 0; - - if (!new_id) - new_id = &new_entry->id; - - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { - uint32_t temp_mode = old_mode; - const git_index_entry *temp_entry = old_entry; - const git_oid *temp_id = old_id; - - old_entry = new_entry; - new_entry = temp_entry; - old_mode = new_mode; - new_mode = temp_mode; - old_id = new_id; - new_id = temp_id; - } - - delta = diff_delta__alloc(diff, status, canonical_path); - GITERR_CHECK_ALLOC(delta); - delta->nfiles = 2; - - if (!git_index_entry_is_conflict(old_entry)) { - delta->old_file.size = old_entry->file_size; - delta->old_file.mode = old_mode; - git_oid_cpy(&delta->old_file.id, old_id); - delta->old_file.id_abbrev = GIT_OID_HEXSZ; - delta->old_file.flags |= GIT_DIFF_FLAG_VALID_ID | - GIT_DIFF_FLAG_EXISTS; - } - - if (!git_index_entry_is_conflict(new_entry)) { - git_oid_cpy(&delta->new_file.id, new_id); - delta->new_file.id_abbrev = GIT_OID_HEXSZ; - delta->new_file.size = new_entry->file_size; - delta->new_file.mode = new_mode; - delta->old_file.flags |= GIT_DIFF_FLAG_EXISTS; - delta->new_file.flags |= GIT_DIFF_FLAG_EXISTS; - - if (!git_oid_iszero(&new_entry->id)) - delta->new_file.flags |= GIT_DIFF_FLAG_VALID_ID; - } - - return diff_insert_delta(diff, delta, matched_pathspec); -} - -static git_diff_delta *diff_delta__last_for_item( - git_diff *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.id, &item->id) == 0) - return delta; - break; - case GIT_DELTA_ADDED: - if (git_oid__cmp(&delta->new_file.id, &item->id) == 0) - return delta; - break; - case GIT_DELTA_UNREADABLE: - case GIT_DELTA_UNTRACKED: - if (diff->strcomp(delta->new_file.path, item->path) == 0 && - git_oid__cmp(&delta->new_file.id, &item->id) == 0) - return delta; - break; - case GIT_DELTA_MODIFIED: - if (git_oid__cmp(&delta->old_file.id, &item->id) == 0 || - git_oid__cmp(&delta->new_file.id, &item->id) == 0) - return delta; - break; - default: - break; - } - - return NULL; -} - -static char *diff_strdup_prefix(git_pool *pool, const char *prefix) -{ - size_t len = strlen(prefix); - - /* append '/' at end if needed */ - if (len > 0 && prefix[len - 1] != '/') - return git_pool_strcat(pool, prefix, "/"); - else - return git_pool_strndup(pool, prefix, len + 1); -} - GIT_INLINE(const char *) diff_delta__path(const git_diff_delta *delta) { const char *str = delta->old_file.path; @@ -309,72 +51,6 @@ int git_diff_delta__casecmp(const void *a, const void *b) return val ? val : ((int)da->status - (int)db->status); } -GIT_INLINE(const char *) diff_delta__i2w_path(const git_diff_delta *delta) -{ - return delta->old_file.path ? - delta->old_file.path : delta->new_file.path; -} - -int git_diff_delta__i2w_cmp(const void *a, const void *b) -{ - const git_diff_delta *da = a, *db = b; - int val = strcmp(diff_delta__i2w_path(da), diff_delta__i2w_path(db)); - return val ? val : ((int)da->status - (int)db->status); -} - -int git_diff_delta__i2w_casecmp(const void *a, const void *b) -{ - const git_diff_delta *da = a, *db = b; - int val = strcasecmp(diff_delta__i2w_path(da), diff_delta__i2w_path(db)); - 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; - - if (delta->status == GIT_DELTA_UNREADABLE && - (flags & GIT_DIFF_INCLUDE_UNREADABLE) == 0) - return true; - - return false; -} - - -static const char *diff_mnemonic_prefix( - git_iterator_type_t type, bool left_side) -{ - const char *pfx = ""; - - switch (type) { - case GIT_ITERATOR_TYPE_EMPTY: pfx = "c"; break; - case GIT_ITERATOR_TYPE_TREE: pfx = "c"; break; - case GIT_ITERATOR_TYPE_INDEX: pfx = "i"; break; - case GIT_ITERATOR_TYPE_WORKDIR: pfx = "w"; break; - case GIT_ITERATOR_TYPE_FS: pfx = left_side ? "1" : "2"; break; - default: break; - } - - /* note: without a deeper look at pathspecs, there is no easy way - * to get the (o)bject / (w)ork tree mnemonics working... - */ - - return pfx; -} - static int diff_entry_cmp(const void *a, const void *b) { const git_index_entry *entry_a = a; @@ -391,198 +67,12 @@ static int diff_entry_icmp(const void *a, const void *b) return strcasecmp(entry_a->path, entry_b->path); } -static void diff_set_ignore_case(git_diff *diff, bool ignore_case) -{ - if (!ignore_case) { - diff->opts.flags &= ~GIT_DIFF_IGNORE_CASE; - - diff->strcomp = git__strcmp; - diff->strncomp = git__strncmp; - diff->pfxcomp = git__prefixcmp; - diff->entrycomp = diff_entry_cmp; - - git_vector_set_cmp(&diff->deltas, git_diff_delta__cmp); - } else { - diff->opts.flags |= GIT_DIFF_IGNORE_CASE; - - diff->strcomp = git__strcasecmp; - diff->strncomp = git__strncasecmp; - diff->pfxcomp = git__prefixcmp_icase; - diff->entrycomp = diff_entry_icmp; - - git_vector_set_cmp(&diff->deltas, git_diff_delta__casecmp); - } - - git_vector_sort(&diff->deltas); -} - -static git_diff *diff_list_alloc( - git_repository *repo, - git_iterator *old_iter, - git_iterator *new_iter) -{ - git_diff_options dflt = GIT_DIFF_OPTIONS_INIT; - git_diff *diff = git__calloc(1, sizeof(git_diff)); - if (!diff) - return NULL; - - assert(repo && old_iter && new_iter); - - GIT_REFCOUNT_INC(diff); - diff->repo = repo; - diff->old_src = old_iter->type; - diff->new_src = new_iter->type; - memcpy(&diff->opts, &dflt, sizeof(diff->opts)); - - git_pool_init(&diff->pool, 1); - - if (git_vector_init(&diff->deltas, 0, git_diff_delta__cmp) < 0) { - git_diff_free(diff); - return NULL; - } - - /* Use case-insensitive compare if either iterator has - * the ignore_case bit set */ - diff_set_ignore_case( - diff, - git_iterator_ignore_case(old_iter) || - git_iterator_ignore_case(new_iter)); - - return diff; -} - -static int diff_list_apply_options( - git_diff *diff, - const git_diff_options *opts) -{ - git_config *cfg = NULL; - git_repository *repo = diff->repo; - git_pool *pool = &diff->pool; - int val; - - if (opts) { - /* copy user options (except case sensitivity info from iterators) */ - bool icase = DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_CASE); - memcpy(&diff->opts, opts, sizeof(diff->opts)); - DIFF_FLAG_SET(diff, GIT_DIFF_IGNORE_CASE, icase); - - /* initialize pathspec from options */ - if (git_pathspec__vinit(&diff->pathspec, &opts->pathspec, pool) < 0) - return -1; - } - - /* flag INCLUDE_TYPECHANGE_TREES implies INCLUDE_TYPECHANGE */ - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES)) - diff->opts.flags |= GIT_DIFF_INCLUDE_TYPECHANGE; - - /* flag INCLUDE_UNTRACKED_CONTENT implies INCLUDE_UNTRACKED */ - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_SHOW_UNTRACKED_CONTENT)) - diff->opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED; - - /* load config values that affect diff behavior */ - if ((val = git_repository_config_snapshot(&cfg, repo)) < 0) - return val; - - if (!git_config__cvar(&val, cfg, GIT_CVAR_SYMLINKS) && val) - diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_HAS_SYMLINKS; - - if (!git_config__cvar(&val, cfg, GIT_CVAR_IGNORESTAT) && val) - diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_IGNORE_STAT; - - if ((diff->opts.flags & GIT_DIFF_IGNORE_FILEMODE) == 0 && - !git_config__cvar(&val, cfg, GIT_CVAR_FILEMODE) && val) - diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_TRUST_MODE_BITS; - - if (!git_config__cvar(&val, cfg, GIT_CVAR_TRUSTCTIME) && val) - diff->diffcaps = diff->diffcaps | GIT_DIFFCAPS_TRUST_CTIME; - - /* Don't set GIT_DIFFCAPS_USE_DEV - compile time option in core git */ - - /* If not given explicit `opts`, check `diff.xyz` configs */ - if (!opts) { - int context = git_config__get_int_force(cfg, "diff.context", 3); - diff->opts.context_lines = context >= 0 ? (uint32_t)context : 3; - - /* add other defaults here */ - } - - /* Reverse src info if diff is reversed */ - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { - git_iterator_type_t tmp_src = diff->old_src; - diff->old_src = diff->new_src; - diff->new_src = tmp_src; - } - - /* Unset UPDATE_INDEX unless diffing workdir and index */ - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_UPDATE_INDEX) && - (!(diff->old_src == GIT_ITERATOR_TYPE_WORKDIR || - diff->new_src == GIT_ITERATOR_TYPE_WORKDIR) || - !(diff->old_src == GIT_ITERATOR_TYPE_INDEX || - diff->new_src == GIT_ITERATOR_TYPE_INDEX))) - diff->opts.flags &= ~GIT_DIFF_UPDATE_INDEX; - - /* if ignore_submodules not explicitly set, check diff config */ - if (diff->opts.ignore_submodules <= 0) { - git_config_entry *entry; - git_config__lookup_entry(&entry, cfg, "diff.ignoresubmodules", true); - - if (entry && git_submodule_parse_ignore( - &diff->opts.ignore_submodules, entry->value) < 0) - giterr_clear(); - git_config_entry_free(entry); - } - - /* if either prefix is not set, figure out appropriate value */ - if (!diff->opts.old_prefix || !diff->opts.new_prefix) { - const char *use_old = DIFF_OLD_PREFIX_DEFAULT; - const char *use_new = DIFF_NEW_PREFIX_DEFAULT; - - if (git_config__get_bool_force(cfg, "diff.noprefix", 0)) - use_old = use_new = ""; - else if (git_config__get_bool_force(cfg, "diff.mnemonicprefix", 0)) { - use_old = diff_mnemonic_prefix(diff->old_src, true); - use_new = diff_mnemonic_prefix(diff->new_src, false); - } - - if (!diff->opts.old_prefix) - diff->opts.old_prefix = use_old; - if (!diff->opts.new_prefix) - diff->opts.new_prefix = use_new; - } - - /* strdup prefix from pool so we're not dependent on external data */ - diff->opts.old_prefix = diff_strdup_prefix(pool, diff->opts.old_prefix); - diff->opts.new_prefix = diff_strdup_prefix(pool, diff->opts.new_prefix); - - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { - const char *tmp_prefix = diff->opts.old_prefix; - diff->opts.old_prefix = diff->opts.new_prefix; - diff->opts.new_prefix = tmp_prefix; - } - - git_config_free(cfg); - - /* check strdup results for error */ - return (!diff->opts.old_prefix || !diff->opts.new_prefix) ? -1 : 0; -} - -static void diff_list_free(git_diff *diff) -{ - git_vector_free_deep(&diff->deltas); - - git_pathspec__vfree(&diff->pathspec); - git_pool_clear(&diff->pool); - - git__memzero(diff, sizeof(*diff)); - git__free(diff); -} - void git_diff_free(git_diff *diff) { if (!diff) return; - GIT_REFCOUNT_DEC(diff, diff_list_free); + GIT_REFCOUNT_DEC(diff, diff->free_fn); } void git_diff_addref(git_diff *diff) @@ -590,896 +80,6 @@ void git_diff_addref(git_diff *diff) GIT_REFCOUNT_INC(diff); } -int git_diff__oid_for_file( - git_oid *out, - git_diff *diff, - const char *path, - uint16_t mode, - git_off_t size) -{ - git_index_entry entry; - - memset(&entry, 0, sizeof(entry)); - entry.mode = mode; - entry.file_size = size; - entry.path = (char *)path; - - return git_diff__oid_for_entry(out, diff, &entry, mode, NULL); -} - -int git_diff__oid_for_entry( - git_oid *out, - git_diff *diff, - const git_index_entry *src, - uint16_t mode, - const git_oid *update_match) -{ - int error = 0; - git_buf full_path = GIT_BUF_INIT; - git_index_entry entry = *src; - git_filter_list *fl = NULL; - - memset(out, 0, sizeof(*out)); - - if (git_buf_joinpath( - &full_path, git_repository_workdir(diff->repo), entry.path) < 0) - return -1; - - if (!mode) { - struct stat st; - - diff->perf.stat_calls++; - - if (p_stat(full_path.ptr, &st) < 0) { - error = git_path_set_error(errno, entry.path, "stat"); - git_buf_free(&full_path); - return error; - } - - git_index_entry__init_from_stat( - &entry, &st, (diff->diffcaps & GIT_DIFFCAPS_TRUST_MODE_BITS) != 0); - } - - /* calculate OID for file if possible */ - if (S_ISGITLINK(mode)) { - git_submodule *sm; - - if (!git_submodule_lookup(&sm, diff->repo, entry.path)) { - const git_oid *sm_oid = git_submodule_wd_id(sm); - if (sm_oid) - git_oid_cpy(out, sm_oid); - git_submodule_free(sm); - } else { - /* if submodule lookup failed probably just in an intermediate - * state where some init hasn't happened, so ignore the error - */ - giterr_clear(); - } - } else if (S_ISLNK(mode)) { - error = git_odb__hashlink(out, full_path.ptr); - diff->perf.oid_calculations++; - } else if (!git__is_sizet(entry.file_size)) { - giterr_set(GITERR_OS, "File size overflow (for 32-bits) on '%s'", - entry.path); - error = -1; - } else if (!(error = git_filter_list_load( - &fl, diff->repo, NULL, entry.path, - GIT_FILTER_TO_ODB, GIT_FILTER_ALLOW_UNSAFE))) - { - int fd = git_futils_open_ro(full_path.ptr); - if (fd < 0) - error = fd; - else { - error = git_odb__hashfd_filtered( - out, fd, (size_t)entry.file_size, GIT_OBJ_BLOB, fl); - p_close(fd); - diff->perf.oid_calculations++; - } - - git_filter_list_free(fl); - } - - /* update index for entry if requested */ - if (!error && update_match && git_oid_equal(out, update_match)) { - git_index *idx; - git_index_entry updated_entry; - - memcpy(&updated_entry, &entry, sizeof(git_index_entry)); - updated_entry.mode = mode; - git_oid_cpy(&updated_entry.id, out); - - if (!(error = git_repository_index__weakptr(&idx, diff->repo))) { - error = git_index_add(idx, &updated_entry); - diff->index_updated = true; - } - } - - git_buf_free(&full_path); - return error; -} - -typedef struct { - git_repository *repo; - git_iterator *old_iter; - git_iterator *new_iter; - const git_index_entry *oitem; - const git_index_entry *nitem; -} diff_in_progress; - -#define MODE_BITS_MASK 0000777 - -static int maybe_modified_submodule( - git_delta_t *status, - git_oid *found_oid, - git_diff *diff, - diff_in_progress *info) -{ - int error = 0; - git_submodule *sub; - unsigned int sm_status = 0; - git_submodule_ignore_t ign = diff->opts.ignore_submodules; - - *status = GIT_DELTA_UNMODIFIED; - - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES) || - ign == GIT_SUBMODULE_IGNORE_ALL) - return 0; - - if ((error = git_submodule_lookup( - &sub, diff->repo, info->nitem->path)) < 0) { - - /* GIT_EEXISTS means dir with .git in it was found - ignore it */ - if (error == GIT_EEXISTS) { - giterr_clear(); - error = 0; - } - return error; - } - - if (ign <= 0 && git_submodule_ignore(sub) == GIT_SUBMODULE_IGNORE_ALL) - /* ignore it */; - else if ((error = git_submodule__status( - &sm_status, NULL, NULL, found_oid, sub, ign)) < 0) - /* return error below */; - - /* check IS_WD_UNMODIFIED because this case is only used - * when the new side of the diff is the working directory - */ - else if (!GIT_SUBMODULE_STATUS_IS_WD_UNMODIFIED(sm_status)) - *status = GIT_DELTA_MODIFIED; - - /* now that we have a HEAD OID, check if HEAD moved */ - else if ((sm_status & GIT_SUBMODULE_STATUS_IN_WD) != 0 && - !git_oid_equal(&info->oitem->id, found_oid)) - *status = GIT_DELTA_MODIFIED; - - git_submodule_free(sub); - return error; -} - -static int maybe_modified( - git_diff *diff, - diff_in_progress *info) -{ - git_oid noid; - git_delta_t status = GIT_DELTA_MODIFIED; - const git_index_entry *oitem = info->oitem; - const git_index_entry *nitem = info->nitem; - unsigned int omode = oitem->mode; - unsigned int nmode = nitem->mode; - bool new_is_workdir = (info->new_iter->type == GIT_ITERATOR_TYPE_WORKDIR); - bool modified_uncertain = false; - const char *matched_pathspec; - int error = 0; - - if (!diff_pathspec_match(&matched_pathspec, diff, oitem)) - return 0; - - memset(&noid, 0, sizeof(noid)); - - /* on platforms with no symlinks, preserve mode of existing symlinks */ - if (S_ISLNK(omode) && S_ISREG(nmode) && new_is_workdir && - !(diff->diffcaps & GIT_DIFFCAPS_HAS_SYMLINKS)) - nmode = omode; - - /* 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); - - /* if one side is a conflict, mark the whole delta as conflicted */ - if (git_index_entry_is_conflict(oitem) || - git_index_entry_is_conflict(nitem)) { - status = GIT_DELTA_CONFLICTED; - - /* support "assume unchanged" (poorly, b/c we still stat everything) */ - } else if ((oitem->flags & GIT_IDXENTRY_VALID) != 0) { - status = GIT_DELTA_UNMODIFIED; - - /* support "skip worktree" index bit */ - } else if ((oitem->flags_extended & GIT_IDXENTRY_SKIP_WORKTREE) != 0) { - status = GIT_DELTA_UNMODIFIED; - - /* if basic type of file changed, then split into delete and add */ - } else if (GIT_MODE_TYPE(omode) != GIT_MODE_TYPE(nmode)) { - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE)) { - status = GIT_DELTA_TYPECHANGE; - } - - else if (nmode == GIT_FILEMODE_UNREADABLE) { - if (!(error = diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem, NULL))) - error = diff_delta__from_one(diff, GIT_DELTA_UNREADABLE, NULL, nitem); - return error; - } - - else { - if (!(error = diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem, NULL))) - error = diff_delta__from_one(diff, GIT_DELTA_ADDED, NULL, nitem); - return error; - } - - /* if oids and modes match (and are valid), then file is unmodified */ - } else if (git_oid_equal(&oitem->id, &nitem->id) && - omode == nmode && - !git_oid_iszero(&oitem->id)) { - status = GIT_DELTA_UNMODIFIED; - - /* if we have an unknown OID and a workdir iterator, then check some - * circumstances that can accelerate things or need special handling - */ - } else if (git_oid_iszero(&nitem->id) && new_is_workdir) { - bool use_ctime = ((diff->diffcaps & GIT_DIFFCAPS_TRUST_CTIME) != 0); - git_index *index = git_iterator_index(info->new_iter); - - status = GIT_DELTA_UNMODIFIED; - - if (S_ISGITLINK(nmode)) { - if ((error = maybe_modified_submodule(&status, &noid, diff, info)) < 0) - return error; - } - - /* if the stat data looks different, then mark modified - this just - * means that the OID will be recalculated below to confirm change - */ - else if (omode != nmode || oitem->file_size != nitem->file_size) { - status = GIT_DELTA_MODIFIED; - modified_uncertain = - (oitem->file_size <= 0 && nitem->file_size > 0); - } - else if (!git_index_time_eq(&oitem->mtime, &nitem->mtime) || - (use_ctime && !git_index_time_eq(&oitem->ctime, &nitem->ctime)) || - oitem->ino != nitem->ino || - oitem->uid != nitem->uid || - oitem->gid != nitem->gid || - git_index_entry_newer_than_index(nitem, index)) - { - status = GIT_DELTA_MODIFIED; - modified_uncertain = true; - } - - /* 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; - } - - /* 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 (modified_uncertain && git_oid_iszero(&nitem->id)) { - const git_oid *update_check = - DIFF_FLAG_IS_SET(diff, GIT_DIFF_UPDATE_INDEX) && omode == nmode ? - &oitem->id : NULL; - - if ((error = git_diff__oid_for_entry( - &noid, diff, nitem, nmode, update_check)) < 0) - return error; - - /* 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->id, &noid)) - status = GIT_DELTA_UNMODIFIED; - } - - /* If we want case changes, then break this into a delete of the old - * and an add of the new so that consumers can act accordingly (eg, - * checkout will update the case on disk.) - */ - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_CASE) && - DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_CASECHANGE) && - strcmp(oitem->path, nitem->path) != 0) { - - if (!(error = diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem, NULL))) - error = diff_delta__from_one(diff, GIT_DELTA_ADDED, NULL, nitem); - - return error; - } - - return diff_delta__from_two( - diff, status, oitem, omode, nitem, nmode, - git_oid_iszero(&noid) ? NULL : &noid, matched_pathspec); -} - -static bool entry_is_prefixed( - git_diff *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 iterator_current( - const git_index_entry **entry, - git_iterator *iterator) -{ - int error; - - if ((error = git_iterator_current(entry, iterator)) == GIT_ITEROVER) { - *entry = NULL; - error = 0; - } - - return error; -} - -static int iterator_advance( - const git_index_entry **entry, - git_iterator *iterator) -{ - const git_index_entry *prev_entry = *entry; - int cmp, error; - - /* if we're looking for conflicts, we only want to report - * one conflict for each file, instead of all three sides. - * so if this entry is a conflict for this file, and the - * previous one was a conflict for the same file, skip it. - */ - while ((error = git_iterator_advance(entry, iterator)) == 0) { - if (!(iterator->flags & GIT_ITERATOR_INCLUDE_CONFLICTS) || - !git_index_entry_is_conflict(prev_entry) || - !git_index_entry_is_conflict(*entry)) - break; - - cmp = (iterator->flags & GIT_ITERATOR_IGNORE_CASE) ? - strcasecmp(prev_entry->path, (*entry)->path) : - strcmp(prev_entry->path, (*entry)->path); - - if (cmp) - break; - } - - if (error == GIT_ITEROVER) { - *entry = NULL; - error = 0; - } - - return error; -} - -static int iterator_advance_into( - const git_index_entry **entry, - git_iterator *iterator) -{ - int error; - - if ((error = git_iterator_advance_into(entry, iterator)) == GIT_ITEROVER) { - *entry = NULL; - error = 0; - } - - return error; -} - -static int iterator_advance_over( - const git_index_entry **entry, - git_iterator_status_t *status, - git_iterator *iterator) -{ - int error = git_iterator_advance_over(entry, status, iterator); - - if (error == GIT_ITEROVER) { - *entry = NULL; - error = 0; - } - - return error; -} - -static int handle_unmatched_new_item( - git_diff *diff, diff_in_progress *info) -{ - int error = 0; - const git_index_entry *nitem = info->nitem; - git_delta_t delta_type = GIT_DELTA_UNTRACKED; - bool contains_oitem; - - /* check if this is a prefix of the other side */ - contains_oitem = entry_is_prefixed(diff, info->oitem, nitem); - - /* update delta_type if this item is conflicted */ - if (git_index_entry_is_conflict(nitem)) - delta_type = GIT_DELTA_CONFLICTED; - - /* update delta_type if this item is ignored */ - else if (git_iterator_current_is_ignored(info->new_iter)) - delta_type = GIT_DELTA_IGNORED; - - if (nitem->mode == GIT_FILEMODE_TREE) { - bool recurse_into_dir = contains_oitem; - - /* check if user requests recursion into this type of dir */ - recurse_into_dir = contains_oitem || - (delta_type == GIT_DELTA_UNTRACKED && - DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) || - (delta_type == GIT_DELTA_IGNORED && - DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)); - - /* do not advance into directories that contain a .git file */ - if (recurse_into_dir && !contains_oitem) { - git_buf *full = NULL; - if (git_iterator_current_workdir_path(&full, info->new_iter) < 0) - return -1; - if (full && git_path_contains(full, DOT_GIT)) { - /* TODO: warning if not a valid git repository */ - recurse_into_dir = false; - } - } - - /* still have to look into untracked directories to match core git - - * with no untracked files, directory is treated as ignored - */ - if (!recurse_into_dir && - delta_type == GIT_DELTA_UNTRACKED && - DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS)) - { - git_diff_delta *last; - git_iterator_status_t untracked_state; - - /* attempt to insert record for this directory */ - if ((error = diff_delta__from_one(diff, delta_type, NULL, nitem)) != 0) - return error; - - /* if delta wasn't created (because of rules), just skip ahead */ - last = diff_delta__last_for_item(diff, nitem); - if (!last) - return iterator_advance(&info->nitem, info->new_iter); - - /* iterate into dir looking for an actual untracked file */ - if ((error = iterator_advance_over( - &info->nitem, &untracked_state, info->new_iter)) < 0) - return error; - - /* if we found nothing that matched our pathlist filter, exclude */ - if (untracked_state == GIT_ITERATOR_STATUS_FILTERED) { - git_vector_pop(&diff->deltas); - git__free(last); - } - - /* if we found nothing or just ignored items, update the record */ - if (untracked_state == GIT_ITERATOR_STATUS_IGNORED || - untracked_state == GIT_ITERATOR_STATUS_EMPTY) { - last->status = GIT_DELTA_IGNORED; - - /* remove the record if we don't want ignored records */ - if (DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_IGNORED)) { - git_vector_pop(&diff->deltas); - git__free(last); - } - } - - return 0; - } - - /* try to advance into directory if necessary */ - if (recurse_into_dir) { - error = iterator_advance_into(&info->nitem, info->new_iter); - - /* if directory is empty, can't advance into it, so skip it */ - if (error == GIT_ENOTFOUND) { - giterr_clear(); - error = iterator_advance(&info->nitem, info->new_iter); - } - - return error; - } - } - - else if (delta_type == GIT_DELTA_IGNORED && - DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS) && - git_iterator_current_tree_is_ignored(info->new_iter)) - /* item contained in ignored directory, so skip over it */ - return iterator_advance(&info->nitem, info->new_iter); - - else if (info->new_iter->type != GIT_ITERATOR_TYPE_WORKDIR) { - if (delta_type != GIT_DELTA_CONFLICTED) - delta_type = GIT_DELTA_ADDED; - } - - else if (nitem->mode == GIT_FILEMODE_COMMIT) { - /* ignore things that are not actual submodules */ - if (git_submodule_lookup(NULL, info->repo, nitem->path) != 0) { - giterr_clear(); - delta_type = GIT_DELTA_IGNORED; - - /* if this contains a tracked item, treat as normal TREE */ - if (contains_oitem) { - error = iterator_advance_into(&info->nitem, info->new_iter); - if (error != GIT_ENOTFOUND) - return error; - - giterr_clear(); - return iterator_advance(&info->nitem, info->new_iter); - } - } - } - - else if (nitem->mode == GIT_FILEMODE_UNREADABLE) { - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED)) - delta_type = GIT_DELTA_UNTRACKED; - else - delta_type = GIT_DELTA_UNREADABLE; - } - - /* Actually create the record for this item if necessary */ - if ((error = diff_delta__from_one(diff, delta_type, NULL, nitem)) != 0) - return error; - - /* If user requested TYPECHANGE records, then check for that instead of - * just generating an ADDED/UNTRACKED record - */ - if (delta_type != GIT_DELTA_IGNORED && - DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) && - contains_oitem) - { - /* this entry was prefixed with a tree - make TYPECHANGE */ - git_diff_delta *last = diff_delta__last_for_item(diff, nitem); - if (last) { - last->status = GIT_DELTA_TYPECHANGE; - last->old_file.mode = GIT_FILEMODE_TREE; - } - } - - return iterator_advance(&info->nitem, info->new_iter); -} - -static int handle_unmatched_old_item( - git_diff *diff, diff_in_progress *info) -{ - git_delta_t delta_type = GIT_DELTA_DELETED; - int error; - - /* update delta_type if this item is conflicted */ - if (git_index_entry_is_conflict(info->oitem)) - delta_type = GIT_DELTA_CONFLICTED; - - if ((error = diff_delta__from_one(diff, delta_type, info->oitem, NULL)) < 0) - return error; - - /* if we are generating TYPECHANGE records then check for that - * instead of just generating a DELETE record - */ - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) && - entry_is_prefixed(diff, info->nitem, info->oitem)) - { - /* this entry has become a tree! convert to TYPECHANGE */ - git_diff_delta *last = diff_delta__last_for_item(diff, info->oitem); - if (last) { - last->status = GIT_DELTA_TYPECHANGE; - last->new_file.mode = GIT_FILEMODE_TREE; - } - - /* If new_iter is a workdir iterator, then this situation - * will certainly be followed by a series of untracked items. - * Unless RECURSE_UNTRACKED_DIRS is set, skip over them... - */ - if (S_ISDIR(info->nitem->mode) && - DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) - return iterator_advance(&info->nitem, info->new_iter); - } - - return iterator_advance(&info->oitem, info->old_iter); -} - -static int handle_matched_item( - git_diff *diff, diff_in_progress *info) -{ - int error = 0; - - if ((error = maybe_modified(diff, info)) < 0) - return error; - - if (!(error = iterator_advance(&info->oitem, info->old_iter))) - error = iterator_advance(&info->nitem, info->new_iter); - - return error; -} - -int git_diff__from_iterators( - git_diff **diff_ptr, - git_repository *repo, - git_iterator *old_iter, - git_iterator *new_iter, - const git_diff_options *opts) -{ - int error = 0; - diff_in_progress info; - git_diff *diff; - - *diff_ptr = NULL; - - diff = diff_list_alloc(repo, old_iter, new_iter); - GITERR_CHECK_ALLOC(diff); - - info.repo = repo; - info.old_iter = old_iter; - info.new_iter = new_iter; - - /* make iterators have matching icase behavior */ - if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_CASE)) { - git_iterator_set_ignore_case(old_iter, true); - git_iterator_set_ignore_case(new_iter, true); - } - - /* finish initialization */ - if ((error = diff_list_apply_options(diff, opts)) < 0) - goto cleanup; - - if ((error = iterator_current(&info.oitem, old_iter)) < 0 || - (error = iterator_current(&info.nitem, new_iter)) < 0) - goto cleanup; - - /* run iterators building diffs */ - while (!error && (info.oitem || info.nitem)) { - int cmp; - - /* report progress */ - if (opts && opts->progress_cb) { - if ((error = opts->progress_cb(diff, - info.oitem ? info.oitem->path : NULL, - info.nitem ? info.nitem->path : NULL, - opts->payload))) - break; - } - - cmp = info.oitem ? - (info.nitem ? diff->entrycomp(info.oitem, info.nitem) : -1) : 1; - - /* create DELETED records for old items not matched in new */ - if (cmp < 0) - error = handle_unmatched_old_item(diff, &info); - - /* create ADDED, TRACKED, or IGNORED records for new items not - * matched in old (and/or descend into directories as needed) - */ - else if (cmp > 0) - error = handle_unmatched_new_item(diff, &info); - - /* otherwise item paths match, so create MODIFIED record - * (or ADDED and DELETED pair if type changed) - */ - else - error = handle_matched_item(diff, &info); - } - - diff->perf.stat_calls += old_iter->stat_calls + new_iter->stat_calls; - -cleanup: - if (!error) - *diff_ptr = diff; - else - git_diff_free(diff); - - return error; -} - -#define DIFF_FROM_ITERATORS(MAKE_FIRST, FLAGS_FIRST, MAKE_SECOND, FLAGS_SECOND) do { \ - git_iterator *a = NULL, *b = NULL; \ - char *pfx = (opts && !(opts->flags & GIT_DIFF_DISABLE_PATHSPEC_MATCH)) ? \ - git_pathspec_prefix(&opts->pathspec) : NULL; \ - git_iterator_options a_opts = GIT_ITERATOR_OPTIONS_INIT, \ - b_opts = GIT_ITERATOR_OPTIONS_INIT; \ - a_opts.flags = FLAGS_FIRST; \ - a_opts.start = pfx; \ - a_opts.end = pfx; \ - b_opts.flags = FLAGS_SECOND; \ - b_opts.start = pfx; \ - b_opts.end = pfx; \ - GITERR_CHECK_VERSION(opts, GIT_DIFF_OPTIONS_VERSION, "git_diff_options"); \ - if (opts && (opts->flags & GIT_DIFF_DISABLE_PATHSPEC_MATCH)) { \ - a_opts.pathlist.strings = opts->pathspec.strings; \ - a_opts.pathlist.count = opts->pathspec.count; \ - b_opts.pathlist.strings = opts->pathspec.strings; \ - b_opts.pathlist.count = opts->pathspec.count; \ - } \ - if (!error && !(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 **diff, - git_repository *repo, - git_tree *old_tree, - git_tree *new_tree, - const git_diff_options *opts) -{ - git_iterator_flag_t iflag = GIT_ITERATOR_DONT_IGNORE_CASE; - int error = 0; - - assert(diff && repo); - - /* for tree to tree diff, be case sensitive even if the index is - * currently case insensitive, unless the user explicitly asked - * for case insensitivity - */ - if (opts && (opts->flags & GIT_DIFF_IGNORE_CASE) != 0) - iflag = GIT_ITERATOR_IGNORE_CASE; - - DIFF_FROM_ITERATORS( - git_iterator_for_tree(&a, old_tree, &a_opts), iflag, - git_iterator_for_tree(&b, new_tree, &b_opts), iflag - ); - - return error; -} - -static int diff_load_index(git_index **index, git_repository *repo) -{ - int error = git_repository_index__weakptr(index, repo); - - /* reload the repository index when user did not pass one in */ - if (!error && git_index_read(*index, false) < 0) - giterr_clear(); - - return error; -} - -int git_diff_tree_to_index( - git_diff **diff, - git_repository *repo, - git_tree *old_tree, - git_index *index, - const git_diff_options *opts) -{ - git_iterator_flag_t iflag = GIT_ITERATOR_DONT_IGNORE_CASE | - GIT_ITERATOR_INCLUDE_CONFLICTS; - bool index_ignore_case = false; - int error = 0; - - assert(diff && repo); - - if (!index && (error = diff_load_index(&index, repo)) < 0) - return error; - - index_ignore_case = index->ignore_case; - - DIFF_FROM_ITERATORS( - git_iterator_for_tree(&a, old_tree, &a_opts), iflag, - git_iterator_for_index(&b, repo, index, &b_opts), iflag - ); - - /* if index is in case-insensitive order, re-sort deltas to match */ - if (!error && index_ignore_case) - diff_set_ignore_case(*diff, true); - - return error; -} - -int git_diff_index_to_workdir( - git_diff **diff, - git_repository *repo, - git_index *index, - const git_diff_options *opts) -{ - int error = 0; - - assert(diff && repo); - - if (!index && (error = diff_load_index(&index, repo)) < 0) - return error; - - DIFF_FROM_ITERATORS( - git_iterator_for_index(&a, repo, index, &a_opts), - GIT_ITERATOR_INCLUDE_CONFLICTS, - - git_iterator_for_workdir(&b, repo, index, NULL, &b_opts), - GIT_ITERATOR_DONT_AUTOEXPAND - ); - - if (!error && DIFF_FLAG_IS_SET(*diff, GIT_DIFF_UPDATE_INDEX) && (*diff)->index_updated) - error = git_index_write(index); - - return error; -} - -int git_diff_tree_to_workdir( - git_diff **diff, - git_repository *repo, - git_tree *old_tree, - const git_diff_options *opts) -{ - int error = 0; - git_index *index; - - assert(diff && repo); - - if ((error = git_repository_index__weakptr(&index, repo))) - return error; - - DIFF_FROM_ITERATORS( - git_iterator_for_tree(&a, old_tree, &a_opts), 0, - git_iterator_for_workdir(&b, repo, index, old_tree, &b_opts), GIT_ITERATOR_DONT_AUTOEXPAND - ); - - return error; -} - -int git_diff_tree_to_workdir_with_index( - git_diff **diff, - git_repository *repo, - git_tree *old_tree, - const git_diff_options *opts) -{ - int error = 0; - git_diff *d1 = NULL, *d2 = NULL; - git_index *index = NULL; - - assert(diff && repo); - - if ((error = diff_load_index(&index, repo)) < 0) - return error; - - if (!(error = git_diff_tree_to_index(&d1, repo, old_tree, index, opts)) && - !(error = git_diff_index_to_workdir(&d2, repo, index, opts))) - error = git_diff_merge(d1, d2); - - git_diff_free(d2); - - if (error) { - git_diff_free(d1); - d1 = NULL; - } - - *diff = d1; - return error; -} - -int git_diff_index_to_index( - git_diff **diff, - git_repository *repo, - git_index *old_index, - git_index *new_index, - const git_diff_options *opts) -{ - int error = 0; - - assert(diff && old_index && new_index); - - DIFF_FROM_ITERATORS( - git_iterator_for_index(&a, repo, old_index, &a_opts), GIT_ITERATOR_DONT_IGNORE_CASE, - git_iterator_for_index(&b, repo, new_index, &b_opts), GIT_ITERATOR_DONT_IGNORE_CASE - ); - - /* if index is in case-insensitive order, re-sort deltas to match */ - if (!error && (old_index->ignore_case || new_index->ignore_case)) - diff_set_ignore_case(*diff, true); - - return error; -} - size_t git_diff_num_deltas(const git_diff *diff) { assert(diff); @@ -1520,137 +120,6 @@ int git_diff_get_perfdata(git_diff_perfdata *out, const git_diff *diff) return 0; } -int git_diff__paired_foreach( - git_diff *head2idx, - git_diff *idx2wd, - int (*cb)(git_diff_delta *h2i, git_diff_delta *i2w, void *payload), - void *payload) -{ - int cmp, error = 0; - git_diff_delta *h2i, *i2w; - size_t i, j, i_max, j_max; - int (*strcomp)(const char *, const char *) = git__strcmp; - bool h2i_icase, i2w_icase, icase_mismatch; - - i_max = head2idx ? head2idx->deltas.length : 0; - j_max = idx2wd ? idx2wd->deltas.length : 0; - if (!i_max && !j_max) - return 0; - - /* At some point, tree-to-index diffs will probably never ignore case, - * even if that isn't true now. Index-to-workdir diffs may or may not - * ignore case, but the index filename for the idx2wd diff should - * still be using the canonical case-preserving name. - * - * Therefore the main thing we need to do here is make sure the diffs - * are traversed in a compatible order. To do this, we temporarily - * resort a mismatched diff to get the order correct. - * - * In order to traverse renames in the index->workdir, we need to - * ensure that we compare the index name on both sides, so we - * always sort by the old name in the i2w list. - */ - h2i_icase = head2idx != NULL && - (head2idx->opts.flags & GIT_DIFF_IGNORE_CASE) != 0; - - i2w_icase = idx2wd != NULL && - (idx2wd->opts.flags & GIT_DIFF_IGNORE_CASE) != 0; - - icase_mismatch = - (head2idx != NULL && idx2wd != NULL && h2i_icase != i2w_icase); - - if (icase_mismatch && h2i_icase) { - git_vector_set_cmp(&head2idx->deltas, git_diff_delta__cmp); - git_vector_sort(&head2idx->deltas); - } - - if (i2w_icase && !icase_mismatch) { - strcomp = git__strcasecmp; - - git_vector_set_cmp(&idx2wd->deltas, git_diff_delta__i2w_casecmp); - git_vector_sort(&idx2wd->deltas); - } else if (idx2wd != NULL) { - git_vector_set_cmp(&idx2wd->deltas, git_diff_delta__i2w_cmp); - git_vector_sort(&idx2wd->deltas); - } - - for (i = 0, j = 0; i < i_max || j < j_max; ) { - h2i = head2idx ? GIT_VECTOR_GET(&head2idx->deltas, i) : NULL; - i2w = idx2wd ? GIT_VECTOR_GET(&idx2wd->deltas, j) : NULL; - - cmp = !i2w ? -1 : !h2i ? 1 : - strcomp(h2i->new_file.path, i2w->old_file.path); - - if (cmp < 0) { - i++; i2w = NULL; - } else if (cmp > 0) { - j++; h2i = NULL; - } else { - i++; j++; - } - - if ((error = cb(h2i, i2w, payload)) != 0) { - giterr_set_after_callback(error); - break; - } - } - - /* restore case-insensitive delta sort */ - if (icase_mismatch && h2i_icase) { - git_vector_set_cmp(&head2idx->deltas, git_diff_delta__casecmp); - git_vector_sort(&head2idx->deltas); - } - - /* restore idx2wd sort by new path */ - if (idx2wd != NULL) { - git_vector_set_cmp(&idx2wd->deltas, - i2w_icase ? git_diff_delta__casecmp : git_diff_delta__cmp); - git_vector_sort(&idx2wd->deltas); - } - - return error; -} - -int git_diff__commit( - git_diff **diff, - git_repository *repo, - const git_commit *commit, - const git_diff_options *opts) -{ - git_commit *parent = NULL; - git_diff *commit_diff = NULL; - git_tree *old_tree = NULL, *new_tree = NULL; - size_t parents; - int error = 0; - - if ((parents = git_commit_parentcount(commit)) > 1) { - char commit_oidstr[GIT_OID_HEXSZ + 1]; - - error = -1; - giterr_set(GITERR_INVALID, "Commit %s is a merge commit", - git_oid_tostr(commit_oidstr, GIT_OID_HEXSZ + 1, git_commit_id(commit))); - goto on_error; - } - - if (parents > 0) - if ((error = git_commit_parent(&parent, commit, 0)) < 0 || - (error = git_commit_tree(&old_tree, parent)) < 0) - goto on_error; - - if ((error = git_commit_tree(&new_tree, commit)) < 0 || - (error = git_diff_tree_to_tree(&commit_diff, repo, old_tree, new_tree, opts)) < 0) - goto on_error; - - *diff = commit_diff; - -on_error: - git_tree_free(new_tree); - git_tree_free(old_tree); - git_commit_free(parent); - - return error; -} - int git_diff_format_email__append_header_tobuf( git_buf *out, const git_oid *id, @@ -1668,7 +137,8 @@ int git_diff_format_email__append_header_tobuf( git_oid_fmt(idstr, id); idstr[GIT_OID_HEXSZ] = '\0'; - if ((error = git__date_rfc2822_fmt(date_str, sizeof(date_str), &author->when)) < 0) + if ((error = git__date_rfc2822_fmt(date_str, sizeof(date_str), + &author->when)) < 0) return error; error = git_buf_printf(out, @@ -1687,7 +157,8 @@ int git_diff_format_email__append_header_tobuf( if (total_patches == 1) { error = git_buf_puts(out, "[PATCH] "); } else { - error = git_buf_printf(out, "[PATCH %"PRIuZ"/%"PRIuZ"] ", patch_no, total_patches); + error = git_buf_printf(out, "[PATCH %"PRIuZ"/%"PRIuZ"] ", + patch_no, total_patches); } if (error < 0) @@ -1745,16 +216,24 @@ int git_diff_format_email( assert(out && diff && opts); assert(opts->summary && opts->id && opts->author); - GITERR_CHECK_VERSION(opts, GIT_DIFF_FORMAT_EMAIL_OPTIONS_VERSION, "git_format_email_options"); + GITERR_CHECK_VERSION(opts, + GIT_DIFF_FORMAT_EMAIL_OPTIONS_VERSION, + "git_format_email_options"); + + ignore_marker = (opts->flags & + GIT_DIFF_FORMAT_EMAIL_EXCLUDE_SUBJECT_PATCH_MARKER) != 0; - if ((ignore_marker = opts->flags & GIT_DIFF_FORMAT_EMAIL_EXCLUDE_SUBJECT_PATCH_MARKER) == false) { + if (!ignore_marker) { if (opts->patch_no > opts->total_patches) { - giterr_set(GITERR_INVALID, "patch %"PRIuZ" out of range. max %"PRIuZ, opts->patch_no, opts->total_patches); + giterr_set(GITERR_INVALID, + "patch %"PRIuZ" out of range. max %"PRIuZ, + opts->patch_no, opts->total_patches); return -1; } if (opts->patch_no == 0) { - giterr_set(GITERR_INVALID, "invalid patch no %"PRIuZ". should be >0", opts->patch_no); + giterr_set(GITERR_INVALID, + "invalid patch no %"PRIuZ". should be >0", opts->patch_no); return -1; } } @@ -1779,8 +258,8 @@ int git_diff_format_email( } error = git_diff_format_email__append_header_tobuf(out, - opts->id, opts->author, summary == NULL ? opts->summary : summary, - opts->body, opts->patch_no, opts->total_patches, ignore_marker); + opts->id, opts->author, summary == NULL ? opts->summary : summary, + opts->body, opts->patch_no, opts->total_patches, ignore_marker); if (error < 0) goto on_error; @@ -1813,7 +292,8 @@ int git_diff_commit_as_email( const git_diff_options *diff_opts) { git_diff *diff = NULL; - git_diff_format_email_options opts = GIT_DIFF_FORMAT_EMAIL_OPTIONS_INIT; + git_diff_format_email_options opts = + GIT_DIFF_FORMAT_EMAIL_OPTIONS_INIT; int error; assert (out && repo && commit); @@ -1858,3 +338,4 @@ int git_diff_format_email_init_options( GIT_DIFF_FORMAT_EMAIL_OPTIONS_INIT); return 0; } + diff --git a/src/diff.h b/src/diff.h index 47743f88b52..153cd350ab6 100644 --- a/src/diff.h +++ b/src/diff.h @@ -22,67 +22,29 @@ #define DIFF_OLD_PREFIX_DEFAULT "a/" #define DIFF_NEW_PREFIX_DEFAULT "b/" -enum { - GIT_DIFFCAPS_HAS_SYMLINKS = (1 << 0), /* symlinks on platform? */ - GIT_DIFFCAPS_IGNORE_STAT = (1 << 1), /* use stat? */ - 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? */ -}; - -#define DIFF_FLAGS_KNOWN_BINARY (GIT_DIFF_FLAG_BINARY|GIT_DIFF_FLAG_NOT_BINARY) -#define DIFF_FLAGS_NOT_BINARY (GIT_DIFF_FLAG_NOT_BINARY|GIT_DIFF_FLAG__NO_DATA) - -enum { - GIT_DIFF_FLAG__FREE_PATH = (1 << 7), /* `path` is allocated memory */ - GIT_DIFF_FLAG__FREE_DATA = (1 << 8), /* internal file data is allocated */ - GIT_DIFF_FLAG__UNMAP_DATA = (1 << 9), /* internal file data is mmap'ed */ - GIT_DIFF_FLAG__NO_DATA = (1 << 10), /* file data should not be loaded */ - GIT_DIFF_FLAG__FREE_BLOB = (1 << 11), /* release the blob when done */ - GIT_DIFF_FLAG__LOADED = (1 << 12), /* file data has been loaded */ - - GIT_DIFF_FLAG__TO_DELETE = (1 << 16), /* delete entry during rename det. */ - GIT_DIFF_FLAG__TO_SPLIT = (1 << 17), /* split entry during rename det. */ - GIT_DIFF_FLAG__IS_RENAME_TARGET = (1 << 18), - GIT_DIFF_FLAG__IS_RENAME_SOURCE = (1 << 19), - GIT_DIFF_FLAG__HAS_SELF_SIMILARITY = (1 << 20), -}; - -#define GIT_DIFF_FLAG__CLEAR_INTERNAL(F) (F) = ((F) & 0x00FFFF) - -#define GIT_DIFF__VERBOSE (1 << 30) +typedef enum { + GIT_DIFF_TYPE_UNKNOWN = 0, + GIT_DIFF_TYPE_GENERATED = 1, +} git_diff_origin_t; struct git_diff { git_refcount rc; git_repository *repo; + git_diff_origin_t type; git_diff_options opts; - git_vector pathspec; 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; git_diff_perfdata perf; - bool index_updated; 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_addref(git_diff *diff); -extern int git_diff_delta__cmp(const void *a, const void *b); -extern int git_diff_delta__casecmp(const void *a, const void *b); - -extern const char *git_diff_delta__path(const git_diff_delta *delta); - -extern bool git_diff_delta__should_skip( - const git_diff_options *opts, const git_diff_delta *delta); + void (*free_fn)(git_diff *diff); +}; extern int git_diff_delta__format_file_header( git_buf *out, @@ -91,84 +53,8 @@ extern int git_diff_delta__format_file_header( const char *newpfx, int oid_strlen); -extern int git_diff__oid_for_file( - git_oid *out, git_diff *, const char *, uint16_t, git_off_t); -extern int git_diff__oid_for_entry( - git_oid *out, git_diff *, const git_index_entry *, uint16_t, const git_oid *update); - -extern int git_diff__from_iterators( - git_diff **diff_ptr, - git_repository *repo, - git_iterator *old_iter, - git_iterator *new_iter, - const git_diff_options *opts); - -extern int git_diff__paired_foreach( - git_diff *idx2head, - git_diff *wd2idx, - int (*cb)(git_diff_delta *i2h, git_diff_delta *w2i, void *payload), - void *payload); - -extern int git_diff_find_similar__hashsig_for_file( - void **out, const git_diff_file *f, const char *path, void *p); - -extern int git_diff_find_similar__hashsig_for_buf( - void **out, const git_diff_file *f, const char *buf, size_t len, void *p); - -extern void git_diff_find_similar__hashsig_free(void *sig, void *payload); - -extern int git_diff_find_similar__calc_similarity( - int *score, void *siga, void *sigb, void *payload); - -extern int git_diff__commit( - git_diff **diff, git_repository *repo, const git_commit *commit, const git_diff_options *opts); - -/* Merge two `git_diff`s according to the callback given by `cb`. */ - -typedef git_diff_delta *(*git_diff__merge_cb)( - const git_diff_delta *left, - const git_diff_delta *right, - git_pool *pool); - -extern int git_diff__merge( - git_diff *onto, const git_diff *from, git_diff__merge_cb cb); - -extern git_diff_delta *git_diff__merge_like_cgit( - const git_diff_delta *a, - const git_diff_delta *b, - git_pool *pool); - -/* Duplicate a `git_diff_delta` out of the `git_pool` */ -extern git_diff_delta *git_diff__delta_dup( - const git_diff_delta *d, git_pool *pool); - -/* - * Sometimes a git_diff_file will have a zero size; this attempts to - * fill in the size without loading the blob if possible. If that is - * not possible, then it will return the git_odb_object that had to be - * loaded and the caller can use it or dispose of it as needed. - */ -GIT_INLINE(int) git_diff_file__resolve_zero_size( - git_diff_file *file, git_odb_object **odb_obj, git_repository *repo) -{ - int error; - git_odb *odb; - size_t len; - git_otype type; - - if ((error = git_repository_odb(&odb, repo)) < 0) - return error; - - error = git_odb__read_header_or_object( - odb_obj, &len, &type, odb, &file->id); - - git_odb_free(odb); - - if (!error) - file->size = (git_off_t)len; - - return error; -} +extern int git_diff_delta__cmp(const void *a, const void *b); +extern int git_diff_delta__casecmp(const void *a, const void *b); #endif diff --git a/src/diff_file.c b/src/diff_file.c index 8b945a5b79b..cc102903841 100644 --- a/src/diff_file.c +++ b/src/diff_file.c @@ -8,6 +8,7 @@ #include "git2/blob.h" #include "git2/submodule.h" #include "diff.h" +#include "diff_generate.h" #include "diff_file.h" #include "odb.h" #include "fileops.h" diff --git a/src/diff_generate.c b/src/diff_generate.c new file mode 100644 index 00000000000..10bc1548639 --- /dev/null +++ b/src/diff_generate.c @@ -0,0 +1,1627 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#include "common.h" +#include "diff.h" +#include "diff_generate.h" +#include "fileops.h" +#include "config.h" +#include "attr_file.h" +#include "filter.h" +#include "pathspec.h" +#include "index.h" +#include "odb.h" +#include "submodule.h" + +#define DIFF_FLAG_IS_SET(DIFF,FLAG) \ + (((DIFF)->base.opts.flags & (FLAG)) != 0) +#define DIFF_FLAG_ISNT_SET(DIFF,FLAG) \ + (((DIFF)->base.opts.flags & (FLAG)) == 0) +#define DIFF_FLAG_SET(DIFF,FLAG,VAL) (DIFF)->base.opts.flags = \ + (VAL) ? ((DIFF)->base.opts.flags | (FLAG)) : \ + ((DIFF)->base.opts.flags & ~(VAL)) + +typedef struct { + struct git_diff base; + + git_vector pathspec; + + uint32_t diffcaps; + bool index_updated; +} git_diff_generated; + +static git_diff_delta *diff_delta__alloc( + git_diff_generated *diff, + git_delta_t status, + const char *path) +{ + git_diff_delta *delta = git__calloc(1, sizeof(git_diff_delta)); + if (!delta) + return NULL; + + delta->old_file.path = git_pool_strdup(&diff->base.pool, path); + if (delta->old_file.path == NULL) { + git__free(delta); + return NULL; + } + + delta->new_file.path = delta->old_file.path; + + 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; + default: break; /* leave other status values alone */ + } + } + delta->status = status; + + return delta; +} + +static int diff_insert_delta( + git_diff_generated *diff, + git_diff_delta *delta, + const char *matched_pathspec) +{ + int error = 0; + + if (diff->base.opts.notify_cb) { + error = diff->base.opts.notify_cb( + &diff->base, delta, matched_pathspec, diff->base.opts.payload); + + if (error) { + git__free(delta); + + if (error > 0) /* positive value means to skip this delta */ + return 0; + else /* negative value means to cancel diff */ + return giterr_set_after_callback_function(error, "git_diff"); + } + } + + if ((error = git_vector_insert(&diff->base.deltas, delta)) < 0) + git__free(delta); + + return error; +} + +static bool diff_pathspec_match( + const char **matched_pathspec, + git_diff_generated *diff, + const git_index_entry *entry) +{ + bool disable_pathspec_match = + DIFF_FLAG_IS_SET(diff, GIT_DIFF_DISABLE_PATHSPEC_MATCH); + + /* If we're disabling fnmatch, then the iterator has already applied + * the filters to the files for us and we don't have to do anything. + * However, this only applies to *files* - the iterator will include + * directories that we need to recurse into when not autoexpanding, + * so we still need to apply the pathspec match to directories. + */ + if ((S_ISLNK(entry->mode) || S_ISREG(entry->mode)) && + disable_pathspec_match) { + *matched_pathspec = entry->path; + return true; + } + + return git_pathspec__match( + &diff->pathspec, entry->path, disable_pathspec_match, + DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_CASE), + matched_pathspec, NULL); +} + +static int diff_delta__from_one( + git_diff_generated *diff, + git_delta_t status, + const git_index_entry *oitem, + const git_index_entry *nitem) +{ + const git_index_entry *entry = nitem; + bool has_old = false; + git_diff_delta *delta; + const char *matched_pathspec; + + assert((oitem != NULL) ^ (nitem != NULL)); + + if (oitem) { + entry = oitem; + has_old = true; + } + + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) + has_old = !has_old; + + if ((entry->flags & GIT_IDXENTRY_VALID) != 0) + return 0; + + if (status == GIT_DELTA_IGNORED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_IGNORED)) + return 0; + + if (status == GIT_DELTA_UNTRACKED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNTRACKED)) + return 0; + + if (status == GIT_DELTA_UNREADABLE && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNREADABLE)) + return 0; + + if (!diff_pathspec_match(&matched_pathspec, diff, entry)) + return 0; + + delta = diff_delta__alloc(diff, status, entry->path); + GITERR_CHECK_ALLOC(delta); + + /* This fn is just for single-sided diffs */ + assert(status != GIT_DELTA_MODIFIED); + delta->nfiles = 1; + + if (has_old) { + delta->old_file.mode = entry->mode; + delta->old_file.size = entry->file_size; + delta->old_file.flags |= GIT_DIFF_FLAG_EXISTS; + git_oid_cpy(&delta->old_file.id, &entry->id); + delta->old_file.id_abbrev = GIT_OID_HEXSZ; + } else /* ADDED, IGNORED, UNTRACKED */ { + delta->new_file.mode = entry->mode; + delta->new_file.size = entry->file_size; + delta->new_file.flags |= GIT_DIFF_FLAG_EXISTS; + git_oid_cpy(&delta->new_file.id, &entry->id); + delta->new_file.id_abbrev = GIT_OID_HEXSZ; + } + + delta->old_file.flags |= GIT_DIFF_FLAG_VALID_ID; + + if (has_old || !git_oid_iszero(&delta->new_file.id)) + delta->new_file.flags |= GIT_DIFF_FLAG_VALID_ID; + + return diff_insert_delta(diff, delta, matched_pathspec); +} + +static int diff_delta__from_two( + git_diff_generated *diff, + git_delta_t status, + const git_index_entry *old_entry, + uint32_t old_mode, + const git_index_entry *new_entry, + uint32_t new_mode, + const git_oid *new_id, + const char *matched_pathspec) +{ + const git_oid *old_id = &old_entry->id; + git_diff_delta *delta; + const char *canonical_path = old_entry->path; + + if (status == GIT_DELTA_UNMODIFIED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_UNMODIFIED)) + return 0; + + if (!new_id) + new_id = &new_entry->id; + + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { + uint32_t temp_mode = old_mode; + const git_index_entry *temp_entry = old_entry; + const git_oid *temp_id = old_id; + + old_entry = new_entry; + new_entry = temp_entry; + old_mode = new_mode; + new_mode = temp_mode; + old_id = new_id; + new_id = temp_id; + } + + delta = diff_delta__alloc(diff, status, canonical_path); + GITERR_CHECK_ALLOC(delta); + delta->nfiles = 2; + + if (!git_index_entry_is_conflict(old_entry)) { + delta->old_file.size = old_entry->file_size; + delta->old_file.mode = old_mode; + git_oid_cpy(&delta->old_file.id, old_id); + delta->old_file.id_abbrev = GIT_OID_HEXSZ; + delta->old_file.flags |= GIT_DIFF_FLAG_VALID_ID | + GIT_DIFF_FLAG_EXISTS; + } + + if (!git_index_entry_is_conflict(new_entry)) { + git_oid_cpy(&delta->new_file.id, new_id); + delta->new_file.id_abbrev = GIT_OID_HEXSZ; + delta->new_file.size = new_entry->file_size; + delta->new_file.mode = new_mode; + delta->old_file.flags |= GIT_DIFF_FLAG_EXISTS; + delta->new_file.flags |= GIT_DIFF_FLAG_EXISTS; + + if (!git_oid_iszero(&new_entry->id)) + delta->new_file.flags |= GIT_DIFF_FLAG_VALID_ID; + } + + return diff_insert_delta(diff, delta, matched_pathspec); +} + +static git_diff_delta *diff_delta__last_for_item( + git_diff_generated *diff, + const git_index_entry *item) +{ + git_diff_delta *delta = git_vector_last(&diff->base.deltas); + if (!delta) + return NULL; + + switch (delta->status) { + case GIT_DELTA_UNMODIFIED: + case GIT_DELTA_DELETED: + if (git_oid__cmp(&delta->old_file.id, &item->id) == 0) + return delta; + break; + case GIT_DELTA_ADDED: + if (git_oid__cmp(&delta->new_file.id, &item->id) == 0) + return delta; + break; + case GIT_DELTA_UNREADABLE: + case GIT_DELTA_UNTRACKED: + if (diff->base.strcomp(delta->new_file.path, item->path) == 0 && + git_oid__cmp(&delta->new_file.id, &item->id) == 0) + return delta; + break; + case GIT_DELTA_MODIFIED: + if (git_oid__cmp(&delta->old_file.id, &item->id) == 0 || + git_oid__cmp(&delta->new_file.id, &item->id) == 0) + return delta; + break; + default: + break; + } + + return NULL; +} + +static char *diff_strdup_prefix(git_pool *pool, const char *prefix) +{ + size_t len = strlen(prefix); + + /* append '/' at end if needed */ + if (len > 0 && prefix[len - 1] != '/') + return git_pool_strcat(pool, prefix, "/"); + else + return git_pool_strndup(pool, prefix, len + 1); +} + +GIT_INLINE(const char *) diff_delta__i2w_path(const git_diff_delta *delta) +{ + return delta->old_file.path ? + delta->old_file.path : delta->new_file.path; +} + +int git_diff_delta__i2w_cmp(const void *a, const void *b) +{ + const git_diff_delta *da = a, *db = b; + int val = strcmp(diff_delta__i2w_path(da), diff_delta__i2w_path(db)); + return val ? val : ((int)da->status - (int)db->status); +} + +int git_diff_delta__i2w_casecmp(const void *a, const void *b) +{ + const git_diff_delta *da = a, *db = b; + int val = strcasecmp(diff_delta__i2w_path(da), diff_delta__i2w_path(db)); + 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; + + if (delta->status == GIT_DELTA_UNREADABLE && + (flags & GIT_DIFF_INCLUDE_UNREADABLE) == 0) + return true; + + return false; +} + + +static const char *diff_mnemonic_prefix( + git_iterator_type_t type, bool left_side) +{ + const char *pfx = ""; + + switch (type) { + case GIT_ITERATOR_TYPE_EMPTY: pfx = "c"; break; + case GIT_ITERATOR_TYPE_TREE: pfx = "c"; break; + case GIT_ITERATOR_TYPE_INDEX: pfx = "i"; break; + case GIT_ITERATOR_TYPE_WORKDIR: pfx = "w"; break; + case GIT_ITERATOR_TYPE_FS: pfx = left_side ? "1" : "2"; break; + default: break; + } + + /* note: without a deeper look at pathspecs, there is no easy way + * to get the (o)bject / (w)ork tree mnemonics working... + */ + + return pfx; +} + +static int diff_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); +} + +static int diff_entry_icmp(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); +} + +void git_diff__set_ignore_case(git_diff *diff, bool ignore_case) +{ + if (!ignore_case) { + diff->opts.flags &= ~GIT_DIFF_IGNORE_CASE; + + diff->strcomp = git__strcmp; + diff->strncomp = git__strncmp; + diff->pfxcomp = git__prefixcmp; + diff->entrycomp = diff_entry_cmp; + + git_vector_set_cmp(&diff->deltas, git_diff_delta__cmp); + } else { + diff->opts.flags |= GIT_DIFF_IGNORE_CASE; + + diff->strcomp = git__strcasecmp; + diff->strncomp = git__strncasecmp; + diff->pfxcomp = git__prefixcmp_icase; + diff->entrycomp = diff_entry_icmp; + + git_vector_set_cmp(&diff->deltas, git_diff_delta__casecmp); + } + + git_vector_sort(&diff->deltas); +} + +static void diff_generated_free(git_diff *d) +{ + git_diff_generated *diff = (git_diff_generated *)d; + + git_vector_free_deep(&diff->base.deltas); + + git_pathspec__vfree(&diff->pathspec); + git_pool_clear(&diff->base.pool); + + git__memzero(diff, sizeof(*diff)); + git__free(diff); +} + +static git_diff_generated *diff_generated_alloc( + git_repository *repo, + git_iterator *old_iter, + git_iterator *new_iter) +{ + git_diff_generated *diff; + git_diff_options dflt = GIT_DIFF_OPTIONS_INIT; + + assert(repo && old_iter && new_iter); + + if ((diff = git__calloc(1, sizeof(git_diff_generated))) == NULL) + return NULL; + + GIT_REFCOUNT_INC(diff); + diff->base.type = GIT_DIFF_TYPE_GENERATED; + diff->base.repo = repo; + diff->base.old_src = old_iter->type; + diff->base.new_src = new_iter->type; + diff->base.free_fn = diff_generated_free; + memcpy(&diff->base.opts, &dflt, sizeof(git_diff_options)); + + git_pool_init(&diff->base.pool, 1); + + if (git_vector_init(&diff->base.deltas, 0, git_diff_delta__cmp) < 0) { + git_diff_free(&diff->base); + return NULL; + } + + /* Use case-insensitive compare if either iterator has + * the ignore_case bit set */ + git_diff__set_ignore_case( + &diff->base, + git_iterator_ignore_case(old_iter) || + git_iterator_ignore_case(new_iter)); + + return diff; +} + +static int diff_generated_apply_options( + git_diff_generated *diff, + const git_diff_options *opts) +{ + git_config *cfg = NULL; + git_repository *repo = diff->base.repo; + git_pool *pool = &diff->base.pool; + int val; + + if (opts) { + /* copy user options (except case sensitivity info from iterators) */ + bool icase = DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_CASE); + memcpy(&diff->base.opts, opts, sizeof(diff->base.opts)); + DIFF_FLAG_SET(diff, GIT_DIFF_IGNORE_CASE, icase); + + /* initialize pathspec from options */ + if (git_pathspec__vinit(&diff->pathspec, &opts->pathspec, pool) < 0) + return -1; + } + + /* flag INCLUDE_TYPECHANGE_TREES implies INCLUDE_TYPECHANGE */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES)) + diff->base.opts.flags |= GIT_DIFF_INCLUDE_TYPECHANGE; + + /* flag INCLUDE_UNTRACKED_CONTENT implies INCLUDE_UNTRACKED */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_SHOW_UNTRACKED_CONTENT)) + diff->base.opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED; + + /* load config values that affect diff behavior */ + if ((val = git_repository_config_snapshot(&cfg, repo)) < 0) + return val; + + if (!git_config__cvar(&val, cfg, GIT_CVAR_SYMLINKS) && val) + diff->diffcaps |= GIT_DIFFCAPS_HAS_SYMLINKS; + + if (!git_config__cvar(&val, cfg, GIT_CVAR_IGNORESTAT) && val) + diff->diffcaps |= GIT_DIFFCAPS_IGNORE_STAT; + + if ((diff->base.opts.flags & GIT_DIFF_IGNORE_FILEMODE) == 0 && + !git_config__cvar(&val, cfg, GIT_CVAR_FILEMODE) && val) + diff->diffcaps |= GIT_DIFFCAPS_TRUST_MODE_BITS; + + if (!git_config__cvar(&val, cfg, GIT_CVAR_TRUSTCTIME) && val) + diff->diffcaps |= GIT_DIFFCAPS_TRUST_CTIME; + + /* Don't set GIT_DIFFCAPS_USE_DEV - compile time option in core git */ + + /* If not given explicit `opts`, check `diff.xyz` configs */ + if (!opts) { + int context = git_config__get_int_force(cfg, "diff.context", 3); + diff->base.opts.context_lines = context >= 0 ? (uint32_t)context : 3; + + /* add other defaults here */ + } + + /* Reverse src info if diff is reversed */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { + git_iterator_type_t tmp_src = diff->base.old_src; + diff->base.old_src = diff->base.new_src; + diff->base.new_src = tmp_src; + } + + /* Unset UPDATE_INDEX unless diffing workdir and index */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_UPDATE_INDEX) && + (!(diff->base.old_src == GIT_ITERATOR_TYPE_WORKDIR || + diff->base.new_src == GIT_ITERATOR_TYPE_WORKDIR) || + !(diff->base.old_src == GIT_ITERATOR_TYPE_INDEX || + diff->base.new_src == GIT_ITERATOR_TYPE_INDEX))) + diff->base.opts.flags &= ~GIT_DIFF_UPDATE_INDEX; + + /* if ignore_submodules not explicitly set, check diff config */ + if (diff->base.opts.ignore_submodules <= 0) { + git_config_entry *entry; + git_config__lookup_entry(&entry, cfg, "diff.ignoresubmodules", true); + + if (entry && git_submodule_parse_ignore( + &diff->base.opts.ignore_submodules, entry->value) < 0) + giterr_clear(); + git_config_entry_free(entry); + } + + /* if either prefix is not set, figure out appropriate value */ + if (!diff->base.opts.old_prefix || !diff->base.opts.new_prefix) { + const char *use_old = DIFF_OLD_PREFIX_DEFAULT; + const char *use_new = DIFF_NEW_PREFIX_DEFAULT; + + if (git_config__get_bool_force(cfg, "diff.noprefix", 0)) + use_old = use_new = ""; + else if (git_config__get_bool_force(cfg, "diff.mnemonicprefix", 0)) { + use_old = diff_mnemonic_prefix(diff->base.old_src, true); + use_new = diff_mnemonic_prefix(diff->base.new_src, false); + } + + if (!diff->base.opts.old_prefix) + diff->base.opts.old_prefix = use_old; + if (!diff->base.opts.new_prefix) + diff->base.opts.new_prefix = use_new; + } + + /* strdup prefix from pool so we're not dependent on external data */ + diff->base.opts.old_prefix = diff_strdup_prefix(pool, diff->base.opts.old_prefix); + diff->base.opts.new_prefix = diff_strdup_prefix(pool, diff->base.opts.new_prefix); + + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_REVERSE)) { + const char *tmp_prefix = diff->base.opts.old_prefix; + diff->base.opts.old_prefix = diff->base.opts.new_prefix; + diff->base.opts.new_prefix = tmp_prefix; + } + + git_config_free(cfg); + + /* check strdup results for error */ + return (!diff->base.opts.old_prefix || !diff->base.opts.new_prefix) ? -1 : 0; +} + +int git_diff__oid_for_file( + git_oid *out, + git_diff *diff, + const char *path, + uint16_t mode, + git_off_t size) +{ + git_index_entry entry; + + memset(&entry, 0, sizeof(entry)); + entry.mode = mode; + entry.file_size = size; + entry.path = (char *)path; + + return git_diff__oid_for_entry(out, diff, &entry, mode, NULL); +} + +int git_diff__oid_for_entry( + git_oid *out, + git_diff *d, + const git_index_entry *src, + uint16_t mode, + const git_oid *update_match) +{ + git_diff_generated *diff; + git_buf full_path = GIT_BUF_INIT; + git_index_entry entry = *src; + git_filter_list *fl = NULL; + int error = 0; + + assert(d->type == GIT_DIFF_TYPE_GENERATED); + diff = (git_diff_generated *)d; + + memset(out, 0, sizeof(*out)); + + if (git_buf_joinpath(&full_path, + git_repository_workdir(diff->base.repo), entry.path) < 0) + return -1; + + if (!mode) { + struct stat st; + + diff->base.perf.stat_calls++; + + if (p_stat(full_path.ptr, &st) < 0) { + error = git_path_set_error(errno, entry.path, "stat"); + git_buf_free(&full_path); + return error; + } + + git_index_entry__init_from_stat(&entry, + &st, (diff->diffcaps & GIT_DIFFCAPS_TRUST_MODE_BITS) != 0); + } + + /* calculate OID for file if possible */ + if (S_ISGITLINK(mode)) { + git_submodule *sm; + + if (!git_submodule_lookup(&sm, diff->base.repo, entry.path)) { + const git_oid *sm_oid = git_submodule_wd_id(sm); + if (sm_oid) + git_oid_cpy(out, sm_oid); + git_submodule_free(sm); + } else { + /* if submodule lookup failed probably just in an intermediate + * state where some init hasn't happened, so ignore the error + */ + giterr_clear(); + } + } else if (S_ISLNK(mode)) { + error = git_odb__hashlink(out, full_path.ptr); + diff->base.perf.oid_calculations++; + } else if (!git__is_sizet(entry.file_size)) { + giterr_set(GITERR_OS, "File size overflow (for 32-bits) on '%s'", + entry.path); + error = -1; + } else if (!(error = git_filter_list_load(&fl, + diff->base.repo, NULL, entry.path, + GIT_FILTER_TO_ODB, GIT_FILTER_ALLOW_UNSAFE))) + { + int fd = git_futils_open_ro(full_path.ptr); + if (fd < 0) + error = fd; + else { + error = git_odb__hashfd_filtered( + out, fd, (size_t)entry.file_size, GIT_OBJ_BLOB, fl); + p_close(fd); + diff->base.perf.oid_calculations++; + } + + git_filter_list_free(fl); + } + + /* update index for entry if requested */ + if (!error && update_match && git_oid_equal(out, update_match)) { + git_index *idx; + git_index_entry updated_entry; + + memcpy(&updated_entry, &entry, sizeof(git_index_entry)); + updated_entry.mode = mode; + git_oid_cpy(&updated_entry.id, out); + + if (!(error = git_repository_index__weakptr(&idx, + diff->base.repo))) { + error = git_index_add(idx, &updated_entry); + diff->index_updated = true; + } + } + + git_buf_free(&full_path); + return error; +} + +typedef struct { + git_repository *repo; + git_iterator *old_iter; + git_iterator *new_iter; + const git_index_entry *oitem; + const git_index_entry *nitem; +} diff_in_progress; + +#define MODE_BITS_MASK 0000777 + +static int maybe_modified_submodule( + git_delta_t *status, + git_oid *found_oid, + git_diff_generated *diff, + diff_in_progress *info) +{ + int error = 0; + git_submodule *sub; + unsigned int sm_status = 0; + git_submodule_ignore_t ign = diff->base.opts.ignore_submodules; + + *status = GIT_DELTA_UNMODIFIED; + + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_SUBMODULES) || + ign == GIT_SUBMODULE_IGNORE_ALL) + return 0; + + if ((error = git_submodule_lookup( + &sub, diff->base.repo, info->nitem->path)) < 0) { + + /* GIT_EEXISTS means dir with .git in it was found - ignore it */ + if (error == GIT_EEXISTS) { + giterr_clear(); + error = 0; + } + return error; + } + + if (ign <= 0 && git_submodule_ignore(sub) == GIT_SUBMODULE_IGNORE_ALL) + /* ignore it */; + else if ((error = git_submodule__status( + &sm_status, NULL, NULL, found_oid, sub, ign)) < 0) + /* return error below */; + + /* check IS_WD_UNMODIFIED because this case is only used + * when the new side of the diff is the working directory + */ + else if (!GIT_SUBMODULE_STATUS_IS_WD_UNMODIFIED(sm_status)) + *status = GIT_DELTA_MODIFIED; + + /* now that we have a HEAD OID, check if HEAD moved */ + else if ((sm_status & GIT_SUBMODULE_STATUS_IN_WD) != 0 && + !git_oid_equal(&info->oitem->id, found_oid)) + *status = GIT_DELTA_MODIFIED; + + git_submodule_free(sub); + return error; +} + +static int maybe_modified( + git_diff_generated *diff, + diff_in_progress *info) +{ + git_oid noid; + git_delta_t status = GIT_DELTA_MODIFIED; + const git_index_entry *oitem = info->oitem; + const git_index_entry *nitem = info->nitem; + unsigned int omode = oitem->mode; + unsigned int nmode = nitem->mode; + bool new_is_workdir = (info->new_iter->type == GIT_ITERATOR_TYPE_WORKDIR); + bool modified_uncertain = false; + const char *matched_pathspec; + int error = 0; + + if (!diff_pathspec_match(&matched_pathspec, diff, oitem)) + return 0; + + memset(&noid, 0, sizeof(noid)); + + /* on platforms with no symlinks, preserve mode of existing symlinks */ + if (S_ISLNK(omode) && S_ISREG(nmode) && new_is_workdir && + !(diff->diffcaps & GIT_DIFFCAPS_HAS_SYMLINKS)) + nmode = omode; + + /* 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); + + /* if one side is a conflict, mark the whole delta as conflicted */ + if (git_index_entry_is_conflict(oitem) || + git_index_entry_is_conflict(nitem)) { + status = GIT_DELTA_CONFLICTED; + + /* support "assume unchanged" (poorly, b/c we still stat everything) */ + } else if ((oitem->flags & GIT_IDXENTRY_VALID) != 0) { + status = GIT_DELTA_UNMODIFIED; + + /* support "skip worktree" index bit */ + } else if ((oitem->flags_extended & GIT_IDXENTRY_SKIP_WORKTREE) != 0) { + status = GIT_DELTA_UNMODIFIED; + + /* if basic type of file changed, then split into delete and add */ + } else if (GIT_MODE_TYPE(omode) != GIT_MODE_TYPE(nmode)) { + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE)) { + status = GIT_DELTA_TYPECHANGE; + } + + else if (nmode == GIT_FILEMODE_UNREADABLE) { + if (!(error = diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem, NULL))) + error = diff_delta__from_one(diff, GIT_DELTA_UNREADABLE, NULL, nitem); + return error; + } + + else { + if (!(error = diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem, NULL))) + error = diff_delta__from_one(diff, GIT_DELTA_ADDED, NULL, nitem); + return error; + } + + /* if oids and modes match (and are valid), then file is unmodified */ + } else if (git_oid_equal(&oitem->id, &nitem->id) && + omode == nmode && + !git_oid_iszero(&oitem->id)) { + status = GIT_DELTA_UNMODIFIED; + + /* if we have an unknown OID and a workdir iterator, then check some + * circumstances that can accelerate things or need special handling + */ + } else if (git_oid_iszero(&nitem->id) && new_is_workdir) { + bool use_ctime = + ((diff->diffcaps & GIT_DIFFCAPS_TRUST_CTIME) != 0); + git_index *index = git_iterator_index(info->new_iter); + + status = GIT_DELTA_UNMODIFIED; + + if (S_ISGITLINK(nmode)) { + if ((error = maybe_modified_submodule(&status, &noid, diff, info)) < 0) + return error; + } + + /* if the stat data looks different, then mark modified - this just + * means that the OID will be recalculated below to confirm change + */ + else if (omode != nmode || oitem->file_size != nitem->file_size) { + status = GIT_DELTA_MODIFIED; + modified_uncertain = + (oitem->file_size <= 0 && nitem->file_size > 0); + } + else if (!git_index_time_eq(&oitem->mtime, &nitem->mtime) || + (use_ctime && !git_index_time_eq(&oitem->ctime, &nitem->ctime)) || + oitem->ino != nitem->ino || + oitem->uid != nitem->uid || + oitem->gid != nitem->gid || + git_index_entry_newer_than_index(nitem, index)) + { + status = GIT_DELTA_MODIFIED; + modified_uncertain = true; + } + + /* 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; + } + + /* 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 (modified_uncertain && git_oid_iszero(&nitem->id)) { + const git_oid *update_check = + DIFF_FLAG_IS_SET(diff, GIT_DIFF_UPDATE_INDEX) && omode == nmode ? + &oitem->id : NULL; + + if ((error = git_diff__oid_for_entry( + &noid, &diff->base, nitem, nmode, update_check)) < 0) + return error; + + /* 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->id, &noid)) + status = GIT_DELTA_UNMODIFIED; + } + + /* If we want case changes, then break this into a delete of the old + * and an add of the new so that consumers can act accordingly (eg, + * checkout will update the case on disk.) + */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_CASE) && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_CASECHANGE) && + strcmp(oitem->path, nitem->path) != 0) { + + if (!(error = diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem, NULL))) + error = diff_delta__from_one(diff, GIT_DELTA_ADDED, NULL, nitem); + + return error; + } + + return diff_delta__from_two( + diff, status, oitem, omode, nitem, nmode, + git_oid_iszero(&noid) ? NULL : &noid, matched_pathspec); +} + +static bool entry_is_prefixed( + git_diff_generated *diff, + const git_index_entry *item, + const git_index_entry *prefix_item) +{ + size_t pathlen; + + if (!item || diff->base.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 iterator_current( + const git_index_entry **entry, + git_iterator *iterator) +{ + int error; + + if ((error = git_iterator_current(entry, iterator)) == GIT_ITEROVER) { + *entry = NULL; + error = 0; + } + + return error; +} + +static int iterator_advance( + const git_index_entry **entry, + git_iterator *iterator) +{ + const git_index_entry *prev_entry = *entry; + int cmp, error; + + /* if we're looking for conflicts, we only want to report + * one conflict for each file, instead of all three sides. + * so if this entry is a conflict for this file, and the + * previous one was a conflict for the same file, skip it. + */ + while ((error = git_iterator_advance(entry, iterator)) == 0) { + if (!(iterator->flags & GIT_ITERATOR_INCLUDE_CONFLICTS) || + !git_index_entry_is_conflict(prev_entry) || + !git_index_entry_is_conflict(*entry)) + break; + + cmp = (iterator->flags & GIT_ITERATOR_IGNORE_CASE) ? + strcasecmp(prev_entry->path, (*entry)->path) : + strcmp(prev_entry->path, (*entry)->path); + + if (cmp) + break; + } + + if (error == GIT_ITEROVER) { + *entry = NULL; + error = 0; + } + + return error; +} + +static int iterator_advance_into( + const git_index_entry **entry, + git_iterator *iterator) +{ + int error; + + if ((error = git_iterator_advance_into(entry, iterator)) == GIT_ITEROVER) { + *entry = NULL; + error = 0; + } + + return error; +} + +static int iterator_advance_over( + const git_index_entry **entry, + git_iterator_status_t *status, + git_iterator *iterator) +{ + int error = git_iterator_advance_over(entry, status, iterator); + + if (error == GIT_ITEROVER) { + *entry = NULL; + error = 0; + } + + return error; +} + +static int handle_unmatched_new_item( + git_diff_generated *diff, diff_in_progress *info) +{ + int error = 0; + const git_index_entry *nitem = info->nitem; + git_delta_t delta_type = GIT_DELTA_UNTRACKED; + bool contains_oitem; + + /* check if this is a prefix of the other side */ + contains_oitem = entry_is_prefixed(diff, info->oitem, nitem); + + /* update delta_type if this item is conflicted */ + if (git_index_entry_is_conflict(nitem)) + delta_type = GIT_DELTA_CONFLICTED; + + /* update delta_type if this item is ignored */ + else if (git_iterator_current_is_ignored(info->new_iter)) + delta_type = GIT_DELTA_IGNORED; + + if (nitem->mode == GIT_FILEMODE_TREE) { + bool recurse_into_dir = contains_oitem; + + /* check if user requests recursion into this type of dir */ + recurse_into_dir = contains_oitem || + (delta_type == GIT_DELTA_UNTRACKED && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) || + (delta_type == GIT_DELTA_IGNORED && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS)); + + /* do not advance into directories that contain a .git file */ + if (recurse_into_dir && !contains_oitem) { + git_buf *full = NULL; + if (git_iterator_current_workdir_path(&full, info->new_iter) < 0) + return -1; + if (full && git_path_contains(full, DOT_GIT)) { + /* TODO: warning if not a valid git repository */ + recurse_into_dir = false; + } + } + + /* still have to look into untracked directories to match core git - + * with no untracked files, directory is treated as ignored + */ + if (!recurse_into_dir && + delta_type == GIT_DELTA_UNTRACKED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS)) + { + git_diff_delta *last; + git_iterator_status_t untracked_state; + + /* attempt to insert record for this directory */ + if ((error = diff_delta__from_one(diff, delta_type, NULL, nitem)) != 0) + return error; + + /* if delta wasn't created (because of rules), just skip ahead */ + last = diff_delta__last_for_item(diff, nitem); + if (!last) + return iterator_advance(&info->nitem, info->new_iter); + + /* iterate into dir looking for an actual untracked file */ + if ((error = iterator_advance_over( + &info->nitem, &untracked_state, info->new_iter)) < 0) + return error; + + /* if we found nothing that matched our pathlist filter, exclude */ + if (untracked_state == GIT_ITERATOR_STATUS_FILTERED) { + git_vector_pop(&diff->base.deltas); + git__free(last); + } + + /* if we found nothing or just ignored items, update the record */ + if (untracked_state == GIT_ITERATOR_STATUS_IGNORED || + untracked_state == GIT_ITERATOR_STATUS_EMPTY) { + last->status = GIT_DELTA_IGNORED; + + /* remove the record if we don't want ignored records */ + if (DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_INCLUDE_IGNORED)) { + git_vector_pop(&diff->base.deltas); + git__free(last); + } + } + + return 0; + } + + /* try to advance into directory if necessary */ + if (recurse_into_dir) { + error = iterator_advance_into(&info->nitem, info->new_iter); + + /* if directory is empty, can't advance into it, so skip it */ + if (error == GIT_ENOTFOUND) { + giterr_clear(); + error = iterator_advance(&info->nitem, info->new_iter); + } + + return error; + } + } + + else if (delta_type == GIT_DELTA_IGNORED && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_IGNORED_DIRS) && + git_iterator_current_tree_is_ignored(info->new_iter)) + /* item contained in ignored directory, so skip over it */ + return iterator_advance(&info->nitem, info->new_iter); + + else if (info->new_iter->type != GIT_ITERATOR_TYPE_WORKDIR) { + if (delta_type != GIT_DELTA_CONFLICTED) + delta_type = GIT_DELTA_ADDED; + } + + else if (nitem->mode == GIT_FILEMODE_COMMIT) { + /* ignore things that are not actual submodules */ + if (git_submodule_lookup(NULL, info->repo, nitem->path) != 0) { + giterr_clear(); + delta_type = GIT_DELTA_IGNORED; + + /* if this contains a tracked item, treat as normal TREE */ + if (contains_oitem) { + error = iterator_advance_into(&info->nitem, info->new_iter); + if (error != GIT_ENOTFOUND) + return error; + + giterr_clear(); + return iterator_advance(&info->nitem, info->new_iter); + } + } + } + + else if (nitem->mode == GIT_FILEMODE_UNREADABLE) { + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED)) + delta_type = GIT_DELTA_UNTRACKED; + else + delta_type = GIT_DELTA_UNREADABLE; + } + + /* Actually create the record for this item if necessary */ + if ((error = diff_delta__from_one(diff, delta_type, NULL, nitem)) != 0) + return error; + + /* If user requested TYPECHANGE records, then check for that instead of + * just generating an ADDED/UNTRACKED record + */ + if (delta_type != GIT_DELTA_IGNORED && + DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) && + contains_oitem) + { + /* this entry was prefixed with a tree - make TYPECHANGE */ + git_diff_delta *last = diff_delta__last_for_item(diff, nitem); + if (last) { + last->status = GIT_DELTA_TYPECHANGE; + last->old_file.mode = GIT_FILEMODE_TREE; + } + } + + return iterator_advance(&info->nitem, info->new_iter); +} + +static int handle_unmatched_old_item( + git_diff_generated *diff, diff_in_progress *info) +{ + git_delta_t delta_type = GIT_DELTA_DELETED; + int error; + + /* update delta_type if this item is conflicted */ + if (git_index_entry_is_conflict(info->oitem)) + delta_type = GIT_DELTA_CONFLICTED; + + if ((error = diff_delta__from_one(diff, delta_type, info->oitem, NULL)) < 0) + return error; + + /* if we are generating TYPECHANGE records then check for that + * instead of just generating a DELETE record + */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) && + entry_is_prefixed(diff, info->nitem, info->oitem)) + { + /* this entry has become a tree! convert to TYPECHANGE */ + git_diff_delta *last = diff_delta__last_for_item(diff, info->oitem); + if (last) { + last->status = GIT_DELTA_TYPECHANGE; + last->new_file.mode = GIT_FILEMODE_TREE; + } + + /* If new_iter is a workdir iterator, then this situation + * will certainly be followed by a series of untracked items. + * Unless RECURSE_UNTRACKED_DIRS is set, skip over them... + */ + if (S_ISDIR(info->nitem->mode) && + DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_RECURSE_UNTRACKED_DIRS)) + return iterator_advance(&info->nitem, info->new_iter); + } + + return iterator_advance(&info->oitem, info->old_iter); +} + +static int handle_matched_item( + git_diff_generated *diff, diff_in_progress *info) +{ + int error = 0; + + if ((error = maybe_modified(diff, info)) < 0) + return error; + + if (!(error = iterator_advance(&info->oitem, info->old_iter))) + error = iterator_advance(&info->nitem, info->new_iter); + + return error; +} + +int git_diff__from_iterators( + git_diff **out, + git_repository *repo, + git_iterator *old_iter, + git_iterator *new_iter, + const git_diff_options *opts) +{ + git_diff_generated *diff; + diff_in_progress info; + int error = 0; + + *out = NULL; + + diff = diff_generated_alloc(repo, old_iter, new_iter); + GITERR_CHECK_ALLOC(diff); + + info.repo = repo; + info.old_iter = old_iter; + info.new_iter = new_iter; + + /* make iterators have matching icase behavior */ + if (DIFF_FLAG_IS_SET(diff, GIT_DIFF_IGNORE_CASE)) { + git_iterator_set_ignore_case(old_iter, true); + git_iterator_set_ignore_case(new_iter, true); + } + + /* finish initialization */ + if ((error = diff_generated_apply_options(diff, opts)) < 0) + goto cleanup; + + if ((error = iterator_current(&info.oitem, old_iter)) < 0 || + (error = iterator_current(&info.nitem, new_iter)) < 0) + goto cleanup; + + /* run iterators building diffs */ + while (!error && (info.oitem || info.nitem)) { + int cmp; + + /* report progress */ + if (opts && opts->progress_cb) { + if ((error = opts->progress_cb(&diff->base, + info.oitem ? info.oitem->path : NULL, + info.nitem ? info.nitem->path : NULL, + opts->payload))) + break; + } + + cmp = info.oitem ? + (info.nitem ? diff->base.entrycomp(info.oitem, info.nitem) : -1) : 1; + + /* create DELETED records for old items not matched in new */ + if (cmp < 0) + error = handle_unmatched_old_item(diff, &info); + + /* create ADDED, TRACKED, or IGNORED records for new items not + * matched in old (and/or descend into directories as needed) + */ + else if (cmp > 0) + error = handle_unmatched_new_item(diff, &info); + + /* otherwise item paths match, so create MODIFIED record + * (or ADDED and DELETED pair if type changed) + */ + else + error = handle_matched_item(diff, &info); + } + + diff->base.perf.stat_calls += + old_iter->stat_calls + new_iter->stat_calls; + +cleanup: + if (!error) + *out = &diff->base; + else + git_diff_free(&diff->base); + + return error; +} + +#define DIFF_FROM_ITERATORS(MAKE_FIRST, FLAGS_FIRST, MAKE_SECOND, FLAGS_SECOND) do { \ + git_iterator *a = NULL, *b = NULL; \ + char *pfx = (opts && !(opts->flags & GIT_DIFF_DISABLE_PATHSPEC_MATCH)) ? \ + git_pathspec_prefix(&opts->pathspec) : NULL; \ + git_iterator_options a_opts = GIT_ITERATOR_OPTIONS_INIT, \ + b_opts = GIT_ITERATOR_OPTIONS_INIT; \ + a_opts.flags = FLAGS_FIRST; \ + a_opts.start = pfx; \ + a_opts.end = pfx; \ + b_opts.flags = FLAGS_SECOND; \ + b_opts.start = pfx; \ + b_opts.end = pfx; \ + GITERR_CHECK_VERSION(opts, GIT_DIFF_OPTIONS_VERSION, "git_diff_options"); \ + if (opts && (opts->flags & GIT_DIFF_DISABLE_PATHSPEC_MATCH)) { \ + a_opts.pathlist.strings = opts->pathspec.strings; \ + a_opts.pathlist.count = opts->pathspec.count; \ + b_opts.pathlist.strings = opts->pathspec.strings; \ + b_opts.pathlist.count = opts->pathspec.count; \ + } \ + if (!error && !(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 **out, + git_repository *repo, + git_tree *old_tree, + git_tree *new_tree, + const git_diff_options *opts) +{ + git_diff *diff = NULL; + git_iterator_flag_t iflag = GIT_ITERATOR_DONT_IGNORE_CASE; + int error = 0; + + assert(out && repo); + + *out = NULL; + + /* for tree to tree diff, be case sensitive even if the index is + * currently case insensitive, unless the user explicitly asked + * for case insensitivity + */ + if (opts && (opts->flags & GIT_DIFF_IGNORE_CASE) != 0) + iflag = GIT_ITERATOR_IGNORE_CASE; + + DIFF_FROM_ITERATORS( + git_iterator_for_tree(&a, old_tree, &a_opts), iflag, + git_iterator_for_tree(&b, new_tree, &b_opts), iflag + ); + + if (!error) + *out = diff; + + return error; +} + +static int diff_load_index(git_index **index, git_repository *repo) +{ + int error = git_repository_index__weakptr(index, repo); + + /* reload the repository index when user did not pass one in */ + if (!error && git_index_read(*index, false) < 0) + giterr_clear(); + + return error; +} + +int git_diff_tree_to_index( + git_diff **out, + git_repository *repo, + git_tree *old_tree, + git_index *index, + const git_diff_options *opts) +{ + git_diff *diff = NULL; + git_iterator_flag_t iflag = GIT_ITERATOR_DONT_IGNORE_CASE | + GIT_ITERATOR_INCLUDE_CONFLICTS; + bool index_ignore_case = false; + int error = 0; + + assert(out && repo); + + *out = NULL; + + if (!index && (error = diff_load_index(&index, repo)) < 0) + return error; + + index_ignore_case = index->ignore_case; + + DIFF_FROM_ITERATORS( + git_iterator_for_tree(&a, old_tree, &a_opts), iflag, + git_iterator_for_index(&b, repo, index, &b_opts), iflag + ); + + /* if index is in case-insensitive order, re-sort deltas to match */ + if (!error && index_ignore_case) + git_diff__set_ignore_case(diff, true); + + if (!error) + *out = diff; + + return error; +} + +int git_diff_index_to_workdir( + git_diff **out, + git_repository *repo, + git_index *index, + const git_diff_options *opts) +{ + git_diff *diff = NULL; + int error = 0; + + assert(out && repo); + + *out = NULL; + + if (!index && (error = diff_load_index(&index, repo)) < 0) + return error; + + DIFF_FROM_ITERATORS( + git_iterator_for_index(&a, repo, index, &a_opts), + GIT_ITERATOR_INCLUDE_CONFLICTS, + + git_iterator_for_workdir(&b, repo, index, NULL, &b_opts), + GIT_ITERATOR_DONT_AUTOEXPAND + ); + + if (!error && (diff->opts.flags & GIT_DIFF_UPDATE_INDEX) != 0 && + ((git_diff_generated *)diff)->index_updated) + error = git_index_write(index); + + if (!error) + *out = diff; + + return error; +} + +int git_diff_tree_to_workdir( + git_diff **out, + git_repository *repo, + git_tree *old_tree, + const git_diff_options *opts) +{ + git_diff *diff = NULL; + git_index *index; + int error = 0; + + assert(out && repo); + + *out = NULL; + + if ((error = git_repository_index__weakptr(&index, repo))) + return error; + + DIFF_FROM_ITERATORS( + git_iterator_for_tree(&a, old_tree, &a_opts), 0, + git_iterator_for_workdir(&b, repo, index, old_tree, &b_opts), GIT_ITERATOR_DONT_AUTOEXPAND + ); + + if (!error) + *out = diff; + + return error; +} + +int git_diff_tree_to_workdir_with_index( + git_diff **out, + git_repository *repo, + git_tree *tree, + const git_diff_options *opts) +{ + git_diff *d1 = NULL, *d2 = NULL; + git_index *index = NULL; + int error = 0; + + assert(out && repo); + + *out = NULL; + + if ((error = diff_load_index(&index, repo)) < 0) + return error; + + if (!(error = git_diff_tree_to_index(&d1, repo, tree, index, opts)) && + !(error = git_diff_index_to_workdir(&d2, repo, index, opts))) + error = git_diff_merge(d1, d2); + + git_diff_free(d2); + + if (error) { + git_diff_free(d1); + d1 = NULL; + } + + *out = d1; + return error; +} + +int git_diff_index_to_index( + git_diff **out, + git_repository *repo, + git_index *old_index, + git_index *new_index, + const git_diff_options *opts) +{ + git_diff *diff; + int error = 0; + + assert(out && old_index && new_index); + + *out = NULL; + + DIFF_FROM_ITERATORS( + git_iterator_for_index(&a, repo, old_index, &a_opts), GIT_ITERATOR_DONT_IGNORE_CASE, + git_iterator_for_index(&b, repo, new_index, &b_opts), GIT_ITERATOR_DONT_IGNORE_CASE + ); + + /* if index is in case-insensitive order, re-sort deltas to match */ + if (!error && (old_index->ignore_case || new_index->ignore_case)) + git_diff__set_ignore_case(diff, true); + + if (!error) + *out = diff; + + return error; +} + +int git_diff__paired_foreach( + git_diff *head2idx, + git_diff *idx2wd, + int (*cb)(git_diff_delta *h2i, git_diff_delta *i2w, void *payload), + void *payload) +{ + int cmp, error = 0; + git_diff_delta *h2i, *i2w; + size_t i, j, i_max, j_max; + int (*strcomp)(const char *, const char *) = git__strcmp; + bool h2i_icase, i2w_icase, icase_mismatch; + + i_max = head2idx ? head2idx->deltas.length : 0; + j_max = idx2wd ? idx2wd->deltas.length : 0; + if (!i_max && !j_max) + return 0; + + /* At some point, tree-to-index diffs will probably never ignore case, + * even if that isn't true now. Index-to-workdir diffs may or may not + * ignore case, but the index filename for the idx2wd diff should + * still be using the canonical case-preserving name. + * + * Therefore the main thing we need to do here is make sure the diffs + * are traversed in a compatible order. To do this, we temporarily + * resort a mismatched diff to get the order correct. + * + * In order to traverse renames in the index->workdir, we need to + * ensure that we compare the index name on both sides, so we + * always sort by the old name in the i2w list. + */ + h2i_icase = head2idx != NULL && git_diff_is_sorted_icase(head2idx); + i2w_icase = idx2wd != NULL && git_diff_is_sorted_icase(idx2wd); + + icase_mismatch = + (head2idx != NULL && idx2wd != NULL && h2i_icase != i2w_icase); + + if (icase_mismatch && h2i_icase) { + git_vector_set_cmp(&head2idx->deltas, git_diff_delta__cmp); + git_vector_sort(&head2idx->deltas); + } + + if (i2w_icase && !icase_mismatch) { + strcomp = git__strcasecmp; + + git_vector_set_cmp(&idx2wd->deltas, git_diff_delta__i2w_casecmp); + git_vector_sort(&idx2wd->deltas); + } else if (idx2wd != NULL) { + git_vector_set_cmp(&idx2wd->deltas, git_diff_delta__i2w_cmp); + git_vector_sort(&idx2wd->deltas); + } + + for (i = 0, j = 0; i < i_max || j < j_max; ) { + h2i = head2idx ? GIT_VECTOR_GET(&head2idx->deltas, i) : NULL; + i2w = idx2wd ? GIT_VECTOR_GET(&idx2wd->deltas, j) : NULL; + + cmp = !i2w ? -1 : !h2i ? 1 : + strcomp(h2i->new_file.path, i2w->old_file.path); + + if (cmp < 0) { + i++; i2w = NULL; + } else if (cmp > 0) { + j++; h2i = NULL; + } else { + i++; j++; + } + + if ((error = cb(h2i, i2w, payload)) != 0) { + giterr_set_after_callback(error); + break; + } + } + + /* restore case-insensitive delta sort */ + if (icase_mismatch && h2i_icase) { + git_vector_set_cmp(&head2idx->deltas, git_diff_delta__casecmp); + git_vector_sort(&head2idx->deltas); + } + + /* restore idx2wd sort by new path */ + if (idx2wd != NULL) { + git_vector_set_cmp(&idx2wd->deltas, + i2w_icase ? git_diff_delta__casecmp : git_diff_delta__cmp); + git_vector_sort(&idx2wd->deltas); + } + + return error; +} + +int git_diff__commit( + git_diff **out, + git_repository *repo, + const git_commit *commit, + const git_diff_options *opts) +{ + git_commit *parent = NULL; + git_diff *commit_diff = NULL; + git_tree *old_tree = NULL, *new_tree = NULL; + size_t parents; + int error = 0; + + *out = NULL; + + if ((parents = git_commit_parentcount(commit)) > 1) { + char commit_oidstr[GIT_OID_HEXSZ + 1]; + + error = -1; + giterr_set(GITERR_INVALID, "Commit %s is a merge commit", + git_oid_tostr(commit_oidstr, GIT_OID_HEXSZ + 1, git_commit_id(commit))); + goto on_error; + } + + if (parents > 0) + if ((error = git_commit_parent(&parent, commit, 0)) < 0 || + (error = git_commit_tree(&old_tree, parent)) < 0) + goto on_error; + + if ((error = git_commit_tree(&new_tree, commit)) < 0 || + (error = git_diff_tree_to_tree(&commit_diff, repo, old_tree, new_tree, opts)) < 0) + goto on_error; + + *out = commit_diff; + +on_error: + git_tree_free(new_tree); + git_tree_free(old_tree); + git_commit_free(parent); + + return error; +} + diff --git a/src/diff_generate.h b/src/diff_generate.h new file mode 100644 index 00000000000..63e7f053243 --- /dev/null +++ b/src/diff_generate.h @@ -0,0 +1,123 @@ +/* + * 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_generate_h__ +#define INCLUDE_diff_generate_h__ + +enum { + GIT_DIFFCAPS_HAS_SYMLINKS = (1 << 0), /* symlinks on platform? */ + GIT_DIFFCAPS_IGNORE_STAT = (1 << 1), /* use stat? */ + 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? */ +}; + +#define DIFF_FLAGS_KNOWN_BINARY (GIT_DIFF_FLAG_BINARY|GIT_DIFF_FLAG_NOT_BINARY) +#define DIFF_FLAGS_NOT_BINARY (GIT_DIFF_FLAG_NOT_BINARY|GIT_DIFF_FLAG__NO_DATA) + +enum { + GIT_DIFF_FLAG__FREE_PATH = (1 << 7), /* `path` is allocated memory */ + GIT_DIFF_FLAG__FREE_DATA = (1 << 8), /* internal file data is allocated */ + GIT_DIFF_FLAG__UNMAP_DATA = (1 << 9), /* internal file data is mmap'ed */ + GIT_DIFF_FLAG__NO_DATA = (1 << 10), /* file data should not be loaded */ + GIT_DIFF_FLAG__FREE_BLOB = (1 << 11), /* release the blob when done */ + GIT_DIFF_FLAG__LOADED = (1 << 12), /* file data has been loaded */ + + GIT_DIFF_FLAG__TO_DELETE = (1 << 16), /* delete entry during rename det. */ + GIT_DIFF_FLAG__TO_SPLIT = (1 << 17), /* split entry during rename det. */ + GIT_DIFF_FLAG__IS_RENAME_TARGET = (1 << 18), + GIT_DIFF_FLAG__IS_RENAME_SOURCE = (1 << 19), + GIT_DIFF_FLAG__HAS_SELF_SIMILARITY = (1 << 20), +}; + +#define GIT_DIFF_FLAG__CLEAR_INTERNAL(F) (F) = ((F) & 0x00FFFF) + +#define GIT_DIFF__VERBOSE (1 << 30) + +extern void git_diff_addref(git_diff *diff); + +extern bool git_diff_delta__should_skip( + const git_diff_options *opts, const git_diff_delta *delta); + +extern int git_diff__from_iterators( + git_diff **diff_ptr, + git_repository *repo, + git_iterator *old_iter, + git_iterator *new_iter, + const git_diff_options *opts); + +extern int git_diff__commit( + git_diff **diff, git_repository *repo, const git_commit *commit, const git_diff_options *opts); + +extern int git_diff__paired_foreach( + git_diff *idx2head, + git_diff *wd2idx, + int (*cb)(git_diff_delta *i2h, git_diff_delta *w2i, void *payload), + void *payload); + +/* Merge two `git_diff`s according to the callback given by `cb`. */ + +typedef git_diff_delta *(*git_diff__merge_cb)( + const git_diff_delta *left, + const git_diff_delta *right, + git_pool *pool); + +extern int git_diff__merge( + git_diff *onto, const git_diff *from, git_diff__merge_cb cb); + +extern git_diff_delta *git_diff__merge_like_cgit( + const git_diff_delta *a, + const git_diff_delta *b, + git_pool *pool); + +/* Duplicate a `git_diff_delta` out of the `git_pool` */ +extern git_diff_delta *git_diff__delta_dup( + const git_diff_delta *d, git_pool *pool); + +extern int git_diff__oid_for_file( + git_oid *out, + git_diff *diff, + const char *path, + uint16_t mode, + git_off_t size); + +extern int git_diff__oid_for_entry( + git_oid *out, + git_diff *diff, + const git_index_entry *src, + uint16_t mode, + const git_oid *update_match); + +/* + * Sometimes a git_diff_file will have a zero size; this attempts to + * fill in the size without loading the blob if possible. If that is + * not possible, then it will return the git_odb_object that had to be + * loaded and the caller can use it or dispose of it as needed. + */ +GIT_INLINE(int) git_diff_file__resolve_zero_size( + git_diff_file *file, git_odb_object **odb_obj, git_repository *repo) +{ + int error; + git_odb *odb; + size_t len; + git_otype type; + + if ((error = git_repository_odb(&odb, repo)) < 0) + return error; + + error = git_odb__read_header_or_object( + odb_obj, &len, &type, odb, &file->id); + + git_odb_free(odb); + + if (!error) + file->size = (git_off_t)len; + + return error; +} + +#endif + diff --git a/src/diff_tform.c b/src/diff_tform.c index 6a6a62811b1..e8848bd4575 100644 --- a/src/diff_tform.c +++ b/src/diff_tform.c @@ -11,6 +11,7 @@ #include "git2/sys/hashsig.h" #include "diff.h" +#include "diff_generate.h" #include "path.h" #include "fileops.h" #include "config.h" diff --git a/src/diff_tform.h b/src/diff_tform.h new file mode 100644 index 00000000000..5bd9712d9c7 --- /dev/null +++ b/src/diff_tform.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_diff_tform_h__ +#define INCLUDE_diff_tform_h__ + +extern int git_diff_find_similar__hashsig_for_file( + void **out, const git_diff_file *f, const char *path, void *p); + +extern int git_diff_find_similar__hashsig_for_buf( + void **out, const git_diff_file *f, const char *buf, size_t len, void *p); + +extern void git_diff_find_similar__hashsig_free(void *sig, void *payload); + +extern int git_diff_find_similar__calc_similarity( + int *score, void *siga, void *sigb, void *payload); + +#endif + diff --git a/src/merge.c b/src/merge.c index b93851b7e8f..6934aa731e4 100644 --- a/src/merge.c +++ b/src/merge.c @@ -18,6 +18,8 @@ #include "iterator.h" #include "refs.h" #include "diff.h" +#include "diff_generate.h" +#include "diff_tform.h" #include "checkout.h" #include "tree.h" #include "blob.h" diff --git a/src/patch_generate.c b/src/patch_generate.c index 80a5a552aea..98c14923de3 100644 --- a/src/patch_generate.c +++ b/src/patch_generate.c @@ -7,6 +7,7 @@ #include "common.h" #include "git2/blob.h" #include "diff.h" +#include "diff_generate.h" #include "diff_file.h" #include "diff_driver.h" #include "patch_generate.h" diff --git a/src/stash.c b/src/stash.c index 43a464e6458..f5f4f36bfd1 100644 --- a/src/stash.c +++ b/src/stash.c @@ -23,6 +23,7 @@ #include "iterator.h" #include "merge.h" #include "diff.h" +#include "diff_generate.h" static int create_error(int error, const char *msg) { diff --git a/src/status.c b/src/status.c index b206b0e2f54..e610f5fe140 100644 --- a/src/status.c +++ b/src/status.c @@ -19,6 +19,7 @@ #include "git2/diff.h" #include "diff.h" +#include "diff_generate.h" static unsigned int index_delta2status(const git_diff_delta *head2idx) { diff --git a/tests/diff/format_email.c b/tests/diff/format_email.c index e55afe958ed..2e6d0368e69 100644 --- a/tests/diff/format_email.c +++ b/tests/diff/format_email.c @@ -4,6 +4,7 @@ #include "buffer.h" #include "commit.h" #include "diff.h" +#include "diff_generate.h" static git_repository *repo; diff --git a/tests/diff/stats.c b/tests/diff/stats.c index f731997dae5..8f146e2a400 100644 --- a/tests/diff/stats.c +++ b/tests/diff/stats.c @@ -4,6 +4,7 @@ #include "buffer.h" #include "commit.h" #include "diff.h" +#include "diff_generate.h" static git_repository *_repo; static git_diff_stats *_stats; diff --git a/tests/merge/trees/treediff.c b/tests/merge/trees/treediff.c index 3634568de5c..cd2cf7827ba 100644 --- a/tests/merge/trees/treediff.c +++ b/tests/merge/trees/treediff.c @@ -3,6 +3,7 @@ #include "merge.h" #include "../merge_helpers.h" #include "diff.h" +#include "diff_tform.h" #include "git2/sys/hashsig.h" static git_repository *repo; From 17572f67ed9a3eb57b981d97468bd216d571bf10 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Thu, 21 Apr 2016 00:04:14 -0400 Subject: [PATCH 44/54] git_patch_parse_ctx: refcount the context --- src/patch.h | 15 --- src/patch_parse.c | 231 +++++++++++++++++++++++++---------------- src/patch_parse.h | 25 +++++ tests/apply/fromfile.c | 1 + tests/patch/parse.c | 1 + tests/patch/print.c | 1 + 6 files changed, 172 insertions(+), 102 deletions(-) create mode 100644 src/patch_parse.h diff --git a/src/patch.h b/src/patch.h index 28fe766da15..525ae700777 100644 --- a/src/patch.h +++ b/src/patch.h @@ -61,21 +61,6 @@ typedef struct { #define GIT_PATCH_OPTIONS_INIT { 1 } -/** - * Create a patch for a single file from the contents of a patch buffer. - * - * @param out The patch to be created - * @param contents The contents of a patch file - * @param contents_len The length of the patch file - * @param opts The git_patch_options - * @return 0 on success, <0 on failure. - */ -extern int git_patch_from_buffer( - git_patch **out, - const char *contents, - size_t contents_len, - git_patch_options *opts); - extern void git_patch_free(git_patch *patch); #endif diff --git a/src/patch_parse.c b/src/patch_parse.c index 5c771d94ae7..70acdbc22ec 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -1,3 +1,9 @@ +/* + * 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/patch.h" #include "patch.h" #include "path.h" @@ -6,13 +12,24 @@ ( giterr_set(GITERR_PATCH, __VA_ARGS__), -1 ) typedef struct { - git_patch base; + git_refcount rc; + + const char *content; + size_t content_len; git_patch_options opts; - /* the patch contents, which lines will point into. */ - /* TODO: allow us to point somewhere refcounted. */ - char *content; + const char *line; + size_t line_len; + size_t line_num; + + size_t remain; +} git_patch_parse_ctx; + +typedef struct { + git_patch base; + + git_patch_parse_ctx *ctx; /* the paths from the `diff --git` header, these will be used if this is not * a rename (and rename paths are specified) or if no `+++`/`---` line specify @@ -30,20 +47,9 @@ typedef struct { char *old_prefix, *new_prefix; } git_patch_parsed; -typedef struct { - const char *content; - size_t content_len; - - const char *line; - size_t line_len; - size_t line_num; - - size_t remain; -} patch_parse_ctx; - GIT_INLINE(bool) parse_ctx_contains( - patch_parse_ctx *ctx, const char *str, size_t len) + git_patch_parse_ctx *ctx, const char *str, size_t len) { return (ctx->line_len >= len && memcmp(ctx->line, str, len) == 0); } @@ -51,7 +57,7 @@ GIT_INLINE(bool) parse_ctx_contains( #define parse_ctx_contains_s(ctx, str) \ parse_ctx_contains(ctx, str, sizeof(str) - 1) -static void parse_advance_line(patch_parse_ctx *ctx) +static void parse_advance_line(git_patch_parse_ctx *ctx) { ctx->line += ctx->line_len; ctx->remain -= ctx->line_len; @@ -59,7 +65,7 @@ static void parse_advance_line(patch_parse_ctx *ctx) ctx->line_num++; } -static void parse_advance_chars(patch_parse_ctx *ctx, size_t char_cnt) +static void parse_advance_chars(git_patch_parse_ctx *ctx, size_t char_cnt) { ctx->line += char_cnt; ctx->remain -= char_cnt; @@ -67,7 +73,7 @@ static void parse_advance_chars(patch_parse_ctx *ctx, size_t char_cnt) } static int parse_advance_expected( - patch_parse_ctx *ctx, + git_patch_parse_ctx *ctx, const char *expected, size_t expected_len) { @@ -84,7 +90,7 @@ static int parse_advance_expected( #define parse_advance_expected_s(ctx, str) \ parse_advance_expected(ctx, str, sizeof(str) - 1) -static int parse_advance_ws(patch_parse_ctx *ctx) +static int parse_advance_ws(git_patch_parse_ctx *ctx) { int ret = -1; @@ -100,7 +106,7 @@ static int parse_advance_ws(patch_parse_ctx *ctx) return ret; } -static int parse_advance_nl(patch_parse_ctx *ctx) +static int parse_advance_nl(git_patch_parse_ctx *ctx) { if (ctx->line_len != 1 || ctx->line[0] != '\n') return -1; @@ -109,7 +115,7 @@ static int parse_advance_nl(patch_parse_ctx *ctx) return 0; } -static int header_path_len(patch_parse_ctx *ctx) +static int header_path_len(git_patch_parse_ctx *ctx) { bool inquote = 0; bool quoted = (ctx->line_len > 0 && ctx->line[0] == '"'); @@ -129,7 +135,7 @@ static int header_path_len(patch_parse_ctx *ctx) return len; } -static int parse_header_path_buf(git_buf *path, patch_parse_ctx *ctx) +static int parse_header_path_buf(git_buf *path, git_patch_parse_ctx *ctx) { int path_len, error = 0; @@ -154,7 +160,7 @@ static int parse_header_path_buf(git_buf *path, patch_parse_ctx *ctx) return error; } -static int parse_header_path(char **out, patch_parse_ctx *ctx) +static int parse_header_path(char **out, git_patch_parse_ctx *ctx) { git_buf path = GIT_BUF_INIT; int error = parse_header_path_buf(&path, ctx); @@ -165,18 +171,18 @@ static int parse_header_path(char **out, patch_parse_ctx *ctx) } static int parse_header_git_oldpath( - git_patch_parsed *patch, patch_parse_ctx *ctx) + git_patch_parsed *patch, git_patch_parse_ctx *ctx) { return parse_header_path(&patch->old_path, ctx); } static int parse_header_git_newpath( - git_patch_parsed *patch, patch_parse_ctx *ctx) + git_patch_parsed *patch, git_patch_parse_ctx *ctx) { return parse_header_path(&patch->new_path, ctx); } -static int parse_header_mode(uint16_t *mode, patch_parse_ctx *ctx) +static int parse_header_mode(uint16_t *mode, git_patch_parse_ctx *ctx) { const char *end; int32_t m; @@ -201,7 +207,7 @@ static int parse_header_mode(uint16_t *mode, patch_parse_ctx *ctx) static int parse_header_oid( git_oid *oid, int *oid_len, - patch_parse_ctx *ctx) + git_patch_parse_ctx *ctx) { size_t len; @@ -223,7 +229,7 @@ static int parse_header_oid( } static int parse_header_git_index( - git_patch_parsed *patch, patch_parse_ctx *ctx) + git_patch_parsed *patch, git_patch_parse_ctx *ctx) { if (parse_header_oid(&patch->base.delta->old_file.id, &patch->base.delta->old_file.id_abbrev, ctx) < 0 || @@ -251,20 +257,20 @@ static int parse_header_git_index( } static int parse_header_git_oldmode( - git_patch_parsed *patch, patch_parse_ctx *ctx) + git_patch_parsed *patch, git_patch_parse_ctx *ctx) { return parse_header_mode(&patch->base.delta->old_file.mode, ctx); } static int parse_header_git_newmode( - git_patch_parsed *patch, patch_parse_ctx *ctx) + git_patch_parsed *patch, git_patch_parse_ctx *ctx) { return parse_header_mode(&patch->base.delta->new_file.mode, ctx); } static int parse_header_git_deletedfilemode( git_patch_parsed *patch, - patch_parse_ctx *ctx) + git_patch_parse_ctx *ctx) { git__free((char *)patch->base.delta->old_file.path); @@ -277,7 +283,7 @@ static int parse_header_git_deletedfilemode( static int parse_header_git_newfilemode( git_patch_parsed *patch, - patch_parse_ctx *ctx) + git_patch_parse_ctx *ctx) { git__free((char *)patch->base.delta->new_file.path); @@ -290,7 +296,7 @@ static int parse_header_git_newfilemode( static int parse_header_rename( char **out, - patch_parse_ctx *ctx) + git_patch_parse_ctx *ctx) { git_buf path = GIT_BUF_INIT; @@ -305,20 +311,20 @@ static int parse_header_rename( } static int parse_header_renamefrom( - git_patch_parsed *patch, patch_parse_ctx *ctx) + git_patch_parsed *patch, git_patch_parse_ctx *ctx) { patch->base.delta->status = GIT_DELTA_RENAMED; return parse_header_rename(&patch->rename_old_path, ctx); } static int parse_header_renameto( - git_patch_parsed *patch, patch_parse_ctx *ctx) + git_patch_parsed *patch, git_patch_parse_ctx *ctx) { patch->base.delta->status = GIT_DELTA_RENAMED; return parse_header_rename(&patch->rename_new_path, ctx); } -static int parse_header_percent(uint16_t *out, patch_parse_ctx *ctx) +static int parse_header_percent(uint16_t *out, git_patch_parse_ctx *ctx) { int32_t val; const char *end; @@ -340,7 +346,7 @@ static int parse_header_percent(uint16_t *out, patch_parse_ctx *ctx) } static int parse_header_similarity( - git_patch_parsed *patch, patch_parse_ctx *ctx) + git_patch_parsed *patch, git_patch_parse_ctx *ctx) { if (parse_header_percent(&patch->base.delta->similarity, ctx) < 0) return parse_err("invalid similarity percentage at line %d", @@ -350,7 +356,7 @@ static int parse_header_similarity( } static int parse_header_dissimilarity( - git_patch_parsed *patch, patch_parse_ctx *ctx) + git_patch_parsed *patch, git_patch_parse_ctx *ctx) { uint16_t dissimilarity; @@ -365,7 +371,7 @@ static int parse_header_dissimilarity( typedef struct { const char *str; - int(*fn)(git_patch_parsed *, patch_parse_ctx *); + int(*fn)(git_patch_parsed *, git_patch_parse_ctx *); } header_git_op; static const header_git_op header_git_ops[] = { @@ -388,7 +394,7 @@ static const header_git_op header_git_ops[] = { static int parse_header_git( git_patch_parsed *patch, - patch_parse_ctx *ctx) + git_patch_parse_ctx *ctx) { size_t i; int error = 0; @@ -443,7 +449,7 @@ static int parse_header_git( return error; } -static int parse_number(git_off_t *out, patch_parse_ctx *ctx) +static int parse_number(git_off_t *out, git_patch_parse_ctx *ctx) { const char *end; int64_t num; @@ -463,7 +469,7 @@ static int parse_number(git_off_t *out, patch_parse_ctx *ctx) return 0; } -static int parse_int(int *out, patch_parse_ctx *ctx) +static int parse_int(int *out, git_patch_parse_ctx *ctx) { git_off_t num; @@ -476,7 +482,7 @@ static int parse_int(int *out, patch_parse_ctx *ctx) static int parse_hunk_header( git_patch_hunk *hunk, - patch_parse_ctx *ctx) + git_patch_parse_ctx *ctx) { const char *header_start = ctx->line; @@ -530,7 +536,7 @@ static int parse_hunk_header( static int parse_hunk_body( git_patch_parsed *patch, git_patch_hunk *hunk, - patch_parse_ctx *ctx) + git_patch_parse_ctx *ctx) { git_diff_line *line; int error = 0; @@ -621,9 +627,9 @@ static int parse_hunk_body( return error; } -static int parsed_patch_header( +static int parse_patch_header( git_patch_parsed *patch, - patch_parse_ctx *ctx) + git_patch_parse_ctx *ctx) { int error = 0; @@ -671,9 +677,9 @@ static int parsed_patch_header( return error; } -static int parsed_patch_binary_side( +static int parse_patch_binary_side( git_diff_binary_file *binary, - patch_parse_ctx *ctx) + git_patch_parse_ctx *ctx) { git_diff_binary_t type = GIT_DIFF_BINARY_NONE; git_buf base85 = GIT_BUF_INIT, decoded = GIT_BUF_INIT; @@ -750,9 +756,9 @@ static int parsed_patch_binary_side( return error; } -static int parsed_patch_binary( +static int parse_patch_binary( git_patch_parsed *patch, - patch_parse_ctx *ctx) + git_patch_parse_ctx *ctx) { int error; @@ -761,7 +767,7 @@ static int parsed_patch_binary( return parse_err("corrupt git binary header at line %d", ctx->line_num); /* parse old->new binary diff */ - if ((error = parsed_patch_binary_side( + if ((error = parse_patch_binary_side( &patch->base.binary.new_file, ctx)) < 0) return error; @@ -770,7 +776,7 @@ static int parsed_patch_binary( ctx->line_num); /* parse new->old binary diff */ - if ((error = parsed_patch_binary_side( + if ((error = parse_patch_binary_side( &patch->base.binary.old_file, ctx)) < 0) return error; @@ -778,9 +784,9 @@ static int parsed_patch_binary( return 0; } -static int parsed_patch_hunks( +static int parse_patch_hunks( git_patch_parsed *patch, - patch_parse_ctx *ctx) + git_patch_parse_ctx *ctx) { git_patch_hunk *hunk; int error = 0; @@ -803,14 +809,14 @@ static int parsed_patch_hunks( return error; } -static int parsed_patch_body( - git_patch_parsed *patch, patch_parse_ctx *ctx) +static int parse_patch_body( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) { if (parse_ctx_contains_s(ctx, "GIT binary patch")) - return parsed_patch_binary(patch, ctx); + return parse_patch_binary(patch, ctx); else if (parse_ctx_contains_s(ctx, "@@ -")) - return parsed_patch_hunks(patch, ctx); + return parse_patch_hunks(patch, ctx); return 0; } @@ -840,12 +846,13 @@ static int check_prefix( const char *path_start) { const char *path = path_start; - uint32_t remain = patch->opts.prefix_len; + size_t prefix_len = patch->ctx->opts.prefix_len; + size_t remain = prefix_len; *out = NULL; *out_len = 0; - if (patch->opts.prefix_len == 0) + if (prefix_len == 0) goto done; /* leading slashes do not count as part of the prefix in git apply */ @@ -860,8 +867,9 @@ static int check_prefix( } if (remain || !*path) - return parse_err("header filename does not contain %d path components", - patch->opts.prefix_len); + return parse_err( + "header filename does not contain %d path components", + prefix_len); done: *out_len = (path - path_start); @@ -938,6 +946,50 @@ static int check_patch(git_patch_parsed *patch) return 0; } +static git_patch_parse_ctx *git_patch_parse_ctx_init( + const char *content, + size_t content_len, + const git_patch_options *opts) +{ + git_patch_parse_ctx *ctx; + git_patch_options default_opts = GIT_PATCH_OPTIONS_INIT; + + if ((ctx = git__calloc(1, sizeof(git_patch_parse_ctx))) == NULL) + return NULL; + + if (content_len) { + if ((ctx->content = git__malloc(content_len)) == NULL) + return NULL; + + memcpy((char *)ctx->content, content, content_len); + } + + ctx->content_len = content_len; + ctx->remain = content_len; + + if (opts) + memcpy(&ctx->opts, opts, sizeof(git_patch_options)); + else + memcpy(&ctx->opts, &default_opts, sizeof(git_patch_options)); + + GIT_REFCOUNT_INC(ctx); + return ctx; +} + +static void patch_parse_ctx_free(git_patch_parse_ctx *ctx) +{ + if (!ctx) + return; + + git__free((char *)ctx->content); + git__free(ctx); +} + +static void git_patch_parse_ctx_free(git_patch_parse_ctx *ctx) +{ + GIT_REFCOUNT_DEC(ctx, patch_parse_ctx_free); +} + static void patch_parsed__free(git_patch *p) { git_patch_parsed *patch = (git_patch_parsed *)p; @@ -945,6 +997,8 @@ static void patch_parsed__free(git_patch *p) if (!patch) return; + git_patch_parse_ctx_free(patch->ctx); + git__free((char *)patch->base.binary.old_file.data); git__free((char *)patch->base.binary.new_file.data); git_array_clear(patch->base.hunks); @@ -959,30 +1013,25 @@ static void patch_parsed__free(git_patch *p) git__free(patch->rename_new_path); git__free(patch->old_path); git__free(patch->new_path); - git__free(patch->content); git__free(patch); } -int git_patch_from_buffer( +static int git_patch_parse( git_patch **out, - const char *content, - size_t content_len, - git_patch_options *opts) + git_patch_parse_ctx *ctx) { - patch_parse_ctx ctx = { 0 }; git_patch_parsed *patch; - git_patch_options default_opts = GIT_PATCH_OPTIONS_INIT; int error = 0; + assert(out && ctx); + *out = NULL; patch = git__calloc(1, sizeof(git_patch_parsed)); GITERR_CHECK_ALLOC(patch); - if (opts) - memcpy(&patch->opts, opts, sizeof(git_patch_options)); - else - memcpy(&patch->opts, &default_opts, sizeof(git_patch_options)); + patch->ctx = ctx; + GIT_REFCOUNT_INC(patch->ctx); patch->base.free_fn = patch_parsed__free; @@ -992,19 +1041,8 @@ int git_patch_from_buffer( patch->base.delta->status = GIT_DELTA_MODIFIED; patch->base.delta->nfiles = 2; - if (content_len) { - patch->content = git__malloc(content_len); - GITERR_CHECK_ALLOC(patch->content); - - memcpy(patch->content, content, content_len); - } - - ctx.content = patch->content; - ctx.content_len = content_len; - ctx.remain = content_len; - - if ((error = parsed_patch_header(patch, &ctx)) < 0 || - (error = parsed_patch_body(patch, &ctx)) < 0 || + if ((error = parse_patch_header(patch, ctx)) < 0 || + (error = parse_patch_body(patch, ctx)) < 0 || (error = check_patch(patch)) < 0) goto done; @@ -1021,3 +1059,22 @@ int git_patch_from_buffer( return error; } + +int git_patch_from_buffer( + git_patch **out, + const char *content, + size_t content_len, + const git_patch_options *opts) +{ + git_patch_parse_ctx *ctx; + int error; + + ctx = git_patch_parse_ctx_init(content, content_len, opts); + GITERR_CHECK_ALLOC(ctx); + + error = git_patch_parse(out, ctx); + + git_patch_parse_ctx_free(ctx); + return error; +} + diff --git a/src/patch_parse.h b/src/patch_parse.h new file mode 100644 index 00000000000..d5e86073f64 --- /dev/null +++ b/src/patch_parse.h @@ -0,0 +1,25 @@ +/* + * 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_patch_parse_h__ +#define INCLUDE_patch_parse_h__ + +/** + * Create a patch for a single file from the contents of a patch buffer. + * + * @param out The patch to be created + * @param contents The contents of a patch file + * @param contents_len The length of the patch file + * @param opts The git_patch_options + * @return 0 on success, <0 on failure. + */ +extern int git_patch_from_buffer( + git_patch **out, + const char *contents, + size_t contents_len, + const git_patch_options *opts); + +#endif diff --git a/tests/apply/fromfile.c b/tests/apply/fromfile.c index 7eb1af90c8f..31fffa1a2bf 100644 --- a/tests/apply/fromfile.c +++ b/tests/apply/fromfile.c @@ -3,6 +3,7 @@ #include "apply.h" #include "patch.h" +#include "patch_parse.h" #include "repository.h" #include "buf_text.h" diff --git a/tests/patch/parse.c b/tests/patch/parse.c index 88cdbf6d73c..92434827a57 100644 --- a/tests/patch/parse.c +++ b/tests/patch/parse.c @@ -1,5 +1,6 @@ #include "clar_libgit2.h" #include "patch.h" +#include "patch_parse.h" #include "patch_common.h" diff --git a/tests/patch/print.c b/tests/patch/print.c index 047b48e8f3a..5a86573b362 100644 --- a/tests/patch/print.c +++ b/tests/patch/print.c @@ -1,5 +1,6 @@ #include "clar_libgit2.h" #include "patch.h" +#include "patch_parse.h" #include "patch_common.h" From 94e488a056942f1bb1ebbe7c9f0c693937726609 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sun, 24 Apr 2016 16:14:25 -0400 Subject: [PATCH 45/54] patch: differentiate not found and invalid patches --- src/patch_parse.c | 3 +- tests/patch/parse.c | 81 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/src/patch_parse.c b/src/patch_parse.c index 70acdbc22ec..991802cb42c 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -671,7 +671,8 @@ static int parse_patch_header( continue; } - error = parse_err("no header in patch file"); + giterr_set(GITERR_PATCH, "no patch found"); + error = GIT_ENOTFOUND; done: return error; diff --git a/tests/patch/parse.c b/tests/patch/parse.c index 92434827a57..8350ac2ddfb 100644 --- a/tests/patch/parse.c +++ b/tests/patch/parse.c @@ -4,16 +4,11 @@ #include "patch_common.h" -void test_patch_parse__original_to_change_middle(void) +static void ensure_patch_validity(git_patch *patch) { - git_patch *patch; const git_diff_delta *delta; char idstr[GIT_OID_HEXSZ+1] = {0}; - cl_git_pass(git_patch_from_buffer( - &patch, PATCH_ORIGINAL_TO_CHANGE_MIDDLE, - strlen(PATCH_ORIGINAL_TO_CHANGE_MIDDLE), NULL)); - cl_assert((delta = git_patch_get_delta(patch)) != NULL); cl_assert_equal_i(2, delta->nfiles); @@ -30,6 +25,80 @@ void test_patch_parse__original_to_change_middle(void) git_oid_nfmt(idstr, delta->new_file.id_abbrev, &delta->new_file.id); cl_assert_equal_s(idstr, "cd8fd12"); cl_assert_equal_i(0, delta->new_file.size); +} + +void test_patch_parse__original_to_change_middle(void) +{ + git_patch *patch; + cl_git_pass(git_patch_from_buffer( + &patch, PATCH_ORIGINAL_TO_CHANGE_MIDDLE, + strlen(PATCH_ORIGINAL_TO_CHANGE_MIDDLE), NULL)); + ensure_patch_validity(patch); git_patch_free(patch); } + +void test_patch_parse__leading_and_trailing_garbage(void) +{ + git_patch *patch; + const char *leading = "This is some leading garbage.\n" + "Maybe it's email headers?\n" + "\n" + PATCH_ORIGINAL_TO_CHANGE_MIDDLE; + const char *trailing = PATCH_ORIGINAL_TO_CHANGE_MIDDLE + "\n" + "This is some trailing garbage.\n" + "Maybe it's an email signature?\n"; + const char *both = "Here's some leading garbage\n" + PATCH_ORIGINAL_TO_CHANGE_MIDDLE + "And here's some trailing.\n"; + + cl_git_pass(git_patch_from_buffer(&patch, leading, strlen(leading), + NULL)); + ensure_patch_validity(patch); + git_patch_free(patch); + + cl_git_pass(git_patch_from_buffer(&patch, trailing, strlen(trailing), + NULL)); + ensure_patch_validity(patch); + git_patch_free(patch); + + cl_git_pass(git_patch_from_buffer(&patch, both, strlen(both), + NULL)); + ensure_patch_validity(patch); + git_patch_free(patch); +} + +void test_patch_parse__nonpatches_fail_with_notfound(void) +{ + git_patch *patch; + + cl_git_fail_with(GIT_ENOTFOUND, + git_patch_from_buffer(&patch, PATCH_NOT_A_PATCH, + strlen(PATCH_NOT_A_PATCH), NULL)); +} + +void test_patch_parse__invalid_patches_fails(void) +{ + git_patch *patch; + + cl_git_fail_with(GIT_ERROR, + git_patch_from_buffer(&patch, PATCH_CORRUPT_GIT_HEADER, + strlen(PATCH_CORRUPT_GIT_HEADER), NULL)); + cl_git_fail_with(GIT_ERROR, + git_patch_from_buffer(&patch, + PATCH_CORRUPT_MISSING_NEW_FILE, + strlen(PATCH_CORRUPT_MISSING_NEW_FILE), NULL)); + cl_git_fail_with(GIT_ERROR, + git_patch_from_buffer(&patch, + PATCH_CORRUPT_MISSING_OLD_FILE, + strlen(PATCH_CORRUPT_MISSING_OLD_FILE), NULL)); + cl_git_fail_with(GIT_ERROR, + git_patch_from_buffer(&patch, PATCH_CORRUPT_NO_CHANGES, + strlen(PATCH_CORRUPT_NO_CHANGES), NULL)); + cl_git_fail_with(GIT_ERROR, + git_patch_from_buffer(&patch, + PATCH_CORRUPT_MISSING_HUNK_HEADER, + strlen(PATCH_CORRUPT_MISSING_HUNK_HEADER), NULL)); +} + From 7166bb16659790ae2b398e1e95c752f784f6f1d3 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 25 Apr 2016 00:35:48 -0400 Subject: [PATCH 46/54] introduce `git_diff_from_buffer` to parse diffs Parse diff files into a `git_diff` structure. --- include/git2/diff.h | 5 +++ src/diff.c | 4 +- src/diff.h | 4 ++ src/diff_generate.c | 20 +-------- src/diff_parse.c | 105 ++++++++++++++++++++++++++++++++++++++++++++ src/patch_parse.c | 75 +++++++++++++++++-------------- src/patch_parse.h | 29 ++++++++++++ tests/diff/parse.c | 60 +++++++++++++++++++++++++ 8 files changed, 250 insertions(+), 52 deletions(-) create mode 100644 src/diff_parse.c create mode 100644 tests/diff/parse.c diff --git a/include/git2/diff.h b/include/git2/diff.h index 065a786e920..880292a1fc7 100644 --- a/include/git2/diff.h +++ b/include/git2/diff.h @@ -1174,6 +1174,11 @@ GIT_EXTERN(int) git_diff_buffers( git_diff_line_cb line_cb, void *payload); +GIT_EXTERN(int) git_diff_from_buffer( + git_diff **out, + const char *content, + size_t content_len); + /** * This is an opaque structure which is allocated by `git_diff_get_stats`. * You are responsible for releasing the object memory when done, using the diff --git a/src/diff.c b/src/diff.c index c54d3574bec..317d495972d 100644 --- a/src/diff.c +++ b/src/diff.c @@ -51,7 +51,7 @@ int git_diff_delta__casecmp(const void *a, const void *b) return val ? val : ((int)da->status - (int)db->status); } -static int diff_entry_cmp(const void *a, const void *b) +int git_diff__entry_cmp(const void *a, const void *b) { const git_index_entry *entry_a = a; const git_index_entry *entry_b = b; @@ -59,7 +59,7 @@ static int diff_entry_cmp(const void *a, const void *b) return strcmp(entry_a->path, entry_b->path); } -static int diff_entry_icmp(const void *a, const void *b) +int git_diff__entry_icmp(const void *a, const void *b) { const git_index_entry *entry_a = a; const git_index_entry *entry_b = b; diff --git a/src/diff.h b/src/diff.h index 153cd350ab6..2c0e52ca21d 100644 --- a/src/diff.h +++ b/src/diff.h @@ -25,6 +25,7 @@ typedef enum { GIT_DIFF_TYPE_UNKNOWN = 0, GIT_DIFF_TYPE_GENERATED = 1, + GIT_DIFF_TYPE_PARSED = 2, } git_diff_origin_t; struct git_diff { @@ -56,5 +57,8 @@ extern int git_diff_delta__format_file_header( extern int git_diff_delta__cmp(const void *a, const void *b); extern int git_diff_delta__casecmp(const void *a, const void *b); +extern int git_diff__entry_cmp(const void *a, const void *b); +extern int git_diff__entry_icmp(const void *a, const void *b); + #endif diff --git a/src/diff_generate.c b/src/diff_generate.c index 10bc1548639..a996bf156a2 100644 --- a/src/diff_generate.c +++ b/src/diff_generate.c @@ -358,22 +358,6 @@ static const char *diff_mnemonic_prefix( return pfx; } -static int diff_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); -} - -static int diff_entry_icmp(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); -} - void git_diff__set_ignore_case(git_diff *diff, bool ignore_case) { if (!ignore_case) { @@ -382,7 +366,7 @@ void git_diff__set_ignore_case(git_diff *diff, bool ignore_case) diff->strcomp = git__strcmp; diff->strncomp = git__strncmp; diff->pfxcomp = git__prefixcmp; - diff->entrycomp = diff_entry_cmp; + diff->entrycomp = git_diff__entry_cmp; git_vector_set_cmp(&diff->deltas, git_diff_delta__cmp); } else { @@ -391,7 +375,7 @@ void git_diff__set_ignore_case(git_diff *diff, bool ignore_case) diff->strcomp = git__strcasecmp; diff->strncomp = git__strncasecmp; diff->pfxcomp = git__prefixcmp_icase; - diff->entrycomp = diff_entry_icmp; + diff->entrycomp = git_diff__entry_icmp; git_vector_set_cmp(&diff->deltas, git_diff_delta__casecmp); } diff --git a/src/diff_parse.c b/src/diff_parse.c new file mode 100644 index 00000000000..ffdc8df8885 --- /dev/null +++ b/src/diff_parse.c @@ -0,0 +1,105 @@ +/* + * 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 "patch.h" +#include "patch_parse.h" + +typedef struct { + struct git_diff base; + + git_vector patches; +} git_diff_parsed; + +static void diff_parsed_free(git_diff *d) +{ + git_diff_parsed *diff = (git_diff_parsed *)d; + git_patch *patch; + size_t i; + + git_vector_foreach(&diff->patches, i, patch) + git_patch_free(patch); + + git_vector_free(&diff->patches); + + git_vector_free(&diff->base.deltas); + git_pool_clear(&diff->base.pool); + + git__memzero(diff, sizeof(*diff)); + git__free(diff); +} + +static git_diff_parsed *diff_parsed_alloc(void) +{ + git_diff_parsed *diff; + + if ((diff = git__calloc(1, sizeof(git_diff_parsed))) == NULL) + return NULL; + + GIT_REFCOUNT_INC(diff); + diff->base.type = GIT_DIFF_TYPE_PARSED; + diff->base.opts.flags &= ~GIT_DIFF_IGNORE_CASE; + diff->base.strcomp = git__strcmp; + diff->base.strncomp = git__strncmp; + diff->base.pfxcomp = git__prefixcmp; + diff->base.entrycomp = git_diff__entry_cmp; + diff->base.free_fn = diff_parsed_free; + + git_pool_init(&diff->base.pool, 1); + + if (git_vector_init(&diff->patches, 0, NULL) < 0 || + git_vector_init(&diff->base.deltas, 0, git_diff_delta__cmp) < 0) { + git_diff_free(&diff->base); + return NULL; + } + + git_vector_set_cmp(&diff->base.deltas, git_diff_delta__cmp); + + return diff; +} + +int git_diff_from_buffer( + git_diff **out, + const char *content, + size_t content_len) +{ + git_diff_parsed *diff; + git_patch *patch; + git_patch_parse_ctx *ctx = NULL; + int error = 0; + + *out = NULL; + + diff = diff_parsed_alloc(); + GITERR_CHECK_ALLOC(diff); + + ctx = git_patch_parse_ctx_init(content, content_len, NULL); + GITERR_CHECK_ALLOC(ctx); + + while (ctx->remain_len) { + if ((error = git_patch_parse(&patch, ctx)) < 0) + break; + + git_vector_insert(&diff->patches, patch); + git_vector_insert(&diff->base.deltas, patch->delta); + } + + if (error == GIT_ENOTFOUND && git_vector_length(&diff->patches) > 0) { + giterr_clear(); + error = 0; + } + + git_patch_parse_ctx_free(ctx); + + if (error < 0) + git_diff_free(&diff->base); + else + *out = &diff->base; + + return error; +} + diff --git a/src/patch_parse.c b/src/patch_parse.c index 991802cb42c..ee75663e673 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -6,26 +6,12 @@ */ #include "git2/patch.h" #include "patch.h" +#include "patch_parse.h" #include "path.h" #define parse_err(...) \ ( giterr_set(GITERR_PATCH, __VA_ARGS__), -1 ) -typedef struct { - git_refcount rc; - - const char *content; - size_t content_len; - - git_patch_options opts; - - const char *line; - size_t line_len; - size_t line_num; - - size_t remain; -} git_patch_parse_ctx; - typedef struct { git_patch base; @@ -60,15 +46,15 @@ GIT_INLINE(bool) parse_ctx_contains( static void parse_advance_line(git_patch_parse_ctx *ctx) { ctx->line += ctx->line_len; - ctx->remain -= ctx->line_len; - ctx->line_len = git__linenlen(ctx->line, ctx->remain); + ctx->remain_len -= ctx->line_len; + ctx->line_len = git__linenlen(ctx->line, ctx->remain_len); ctx->line_num++; } static void parse_advance_chars(git_patch_parse_ctx *ctx, size_t char_cnt) { ctx->line += char_cnt; - ctx->remain -= char_cnt; + ctx->remain_len -= char_cnt; ctx->line_len -= char_cnt; } @@ -99,7 +85,7 @@ static int parse_advance_ws(git_patch_parse_ctx *ctx) git__isspace(ctx->line[0])) { ctx->line++; ctx->line_len--; - ctx->remain--; + ctx->remain_len--; ret = 0; } @@ -413,7 +399,12 @@ static int parse_header_git( ctx->line_num); /* Parse remaining header lines */ - for (parse_advance_line(ctx); ctx->remain > 0; parse_advance_line(ctx)) { + for (parse_advance_line(ctx); + ctx->remain_len > 0; + parse_advance_line(ctx)) { + + bool found = false; + if (ctx->line_len == 0 || ctx->line[ctx->line_len - 1] != '\n') break; @@ -441,8 +432,14 @@ static int parse_header_git( goto done; } + found = true; break; } + + if (!found) { + error = parse_err("invalid patch header at line %d", ctx->line_num); + goto done; + } } done: @@ -545,7 +542,7 @@ static int parse_hunk_body( int newlines = hunk->hunk.new_lines; for (; - ctx->remain > 4 && (oldlines || newlines) && + ctx->remain_len > 4 && (oldlines || newlines) && memcmp(ctx->line, "@@ -", 4) != 0; parse_advance_line(ctx)) { @@ -590,7 +587,7 @@ static int parse_hunk_body( line->content = ctx->line + prefix; line->content_len = ctx->line_len - prefix; - line->content_offset = ctx->content_len - ctx->remain; + line->content_offset = ctx->content_len - ctx->remain_len; line->origin = origin; hunk->line_count++; @@ -633,7 +630,10 @@ static int parse_patch_header( { int error = 0; - for (ctx->line = ctx->content; ctx->remain > 0; parse_advance_line(ctx)) { + for (ctx->line = ctx->remain; + ctx->remain_len > 0; + parse_advance_line(ctx)) { + /* This line is too short to be a patch header. */ if (ctx->line_len < 6) continue; @@ -658,7 +658,7 @@ static int parse_patch_header( } /* This buffer is too short to contain a patch. */ - if (ctx->remain < ctx->line_len + 6) + if (ctx->remain_len < ctx->line_len + 6) break; /* A proper git patch */ @@ -781,6 +781,10 @@ static int parse_patch_binary( &patch->base.binary.old_file, ctx)) < 0) return error; + if (parse_advance_nl(ctx) < 0) + return parse_err("corrupt git binary patch separator at line %d", + ctx->line_num); + patch->base.delta->flags |= GIT_DIFF_FLAG_BINARY; return 0; } @@ -848,7 +852,7 @@ static int check_prefix( { const char *path = path_start; size_t prefix_len = patch->ctx->opts.prefix_len; - size_t remain = prefix_len; + size_t remain_len = prefix_len; *out = NULL; *out_len = 0; @@ -860,14 +864,14 @@ static int check_prefix( while (*path == '/') path++; - while (*path && remain) { + while (*path && remain_len) { if (*path == '/') - remain--; + remain_len--; path++; } - if (remain || !*path) + if (remain_len || !*path) return parse_err( "header filename does not contain %d path components", prefix_len); @@ -947,7 +951,7 @@ static int check_patch(git_patch_parsed *patch) return 0; } -static git_patch_parse_ctx *git_patch_parse_ctx_init( +git_patch_parse_ctx *git_patch_parse_ctx_init( const char *content, size_t content_len, const git_patch_options *opts) @@ -966,7 +970,8 @@ static git_patch_parse_ctx *git_patch_parse_ctx_init( } ctx->content_len = content_len; - ctx->remain = content_len; + ctx->remain = ctx->content; + ctx->remain_len = ctx->content_len; if (opts) memcpy(&ctx->opts, opts, sizeof(git_patch_options)); @@ -986,7 +991,7 @@ static void patch_parse_ctx_free(git_patch_parse_ctx *ctx) git__free(ctx); } -static void git_patch_parse_ctx_free(git_patch_parse_ctx *ctx) +void git_patch_parse_ctx_free(git_patch_parse_ctx *ctx) { GIT_REFCOUNT_DEC(ctx, patch_parse_ctx_free); } @@ -1017,11 +1022,12 @@ static void patch_parsed__free(git_patch *p) git__free(patch); } -static int git_patch_parse( +int git_patch_parse( git_patch **out, git_patch_parse_ctx *ctx) { git_patch_parsed *patch; + size_t start, used; int error = 0; assert(out && ctx); @@ -1042,11 +1048,16 @@ static int git_patch_parse( patch->base.delta->status = GIT_DELTA_MODIFIED; patch->base.delta->nfiles = 2; + start = ctx->remain_len; + if ((error = parse_patch_header(patch, ctx)) < 0 || (error = parse_patch_body(patch, ctx)) < 0 || (error = check_patch(patch)) < 0) goto done; + used = start - ctx->remain_len; + ctx->remain += used; + patch->base.diff_opts.old_prefix = patch->old_prefix; patch->base.diff_opts.new_prefix = patch->new_prefix; patch->base.diff_opts.flags |= GIT_DIFF_SHOW_BINARY; diff --git a/src/patch_parse.h b/src/patch_parse.h index d5e86073f64..da56dad7c70 100644 --- a/src/patch_parse.h +++ b/src/patch_parse.h @@ -7,6 +7,31 @@ #ifndef INCLUDE_patch_parse_h__ #define INCLUDE_patch_parse_h__ +typedef struct { + git_refcount rc; + + /* Original content buffer */ + const char *content; + size_t content_len; + + git_patch_options opts; + + /* The remaining (unparsed) buffer */ + const char *remain; + size_t remain_len; + + const char *line; + size_t line_len; + size_t line_num; +} git_patch_parse_ctx; + +extern git_patch_parse_ctx *git_patch_parse_ctx_init( + const char *content, + size_t content_len, + const git_patch_options *opts); + +extern void git_patch_parse_ctx_free(git_patch_parse_ctx *ctx); + /** * Create a patch for a single file from the contents of a patch buffer. * @@ -22,4 +47,8 @@ extern int git_patch_from_buffer( size_t contents_len, const git_patch_options *opts); +extern int git_patch_parse( + git_patch **out, + git_patch_parse_ctx *ctx); + #endif diff --git a/tests/diff/parse.c b/tests/diff/parse.c new file mode 100644 index 00000000000..8eb98423bbb --- /dev/null +++ b/tests/diff/parse.c @@ -0,0 +1,60 @@ +#include "clar_libgit2.h" +#include "patch.h" +#include "patch_parse.h" + +#include "../patch/patch_common.h" + +void test_diff_parse__nonpatches_fail_with_notfound(void) +{ + git_diff *diff; + const char *not = PATCH_NOT_A_PATCH; + const char *not_with_leading = "Leading text.\n" PATCH_NOT_A_PATCH; + const char *not_with_trailing = PATCH_NOT_A_PATCH "Trailing text.\n"; + const char *not_with_both = "Lead.\n" PATCH_NOT_A_PATCH "Trail.\n"; + + cl_git_fail_with(GIT_ENOTFOUND, + git_diff_from_buffer(&diff, + not, + strlen(not))); + cl_git_fail_with(GIT_ENOTFOUND, + git_diff_from_buffer(&diff, + not_with_leading, + strlen(not_with_leading))); + cl_git_fail_with(GIT_ENOTFOUND, + git_diff_from_buffer(&diff, + not_with_trailing, + strlen(not_with_trailing))); + cl_git_fail_with(GIT_ENOTFOUND, + git_diff_from_buffer(&diff, + not_with_both, + strlen(not_with_both))); +} + +static void test_parse_invalid_diff(const char *invalid_diff) +{ + git_diff *diff; + git_buf buf = GIT_BUF_INIT; + + /* throw some random (legitimate) diffs in with the given invalid + * one. + */ + git_buf_puts(&buf, PATCH_ORIGINAL_TO_CHANGE_FIRSTLINE); + git_buf_puts(&buf, PATCH_BINARY_DELTA); + git_buf_puts(&buf, invalid_diff); + git_buf_puts(&buf, PATCH_ORIGINAL_TO_CHANGE_MIDDLE); + git_buf_puts(&buf, PATCH_BINARY_LITERAL); + + cl_git_fail_with(GIT_ERROR, + git_diff_from_buffer(&diff, buf.ptr, buf.size)); + + git_buf_free(&buf); +} + +void test_diff_parse__invalid_patches_fails(void) +{ + test_parse_invalid_diff(PATCH_CORRUPT_MISSING_NEW_FILE); + test_parse_invalid_diff(PATCH_CORRUPT_MISSING_OLD_FILE); + test_parse_invalid_diff(PATCH_CORRUPT_NO_CHANGES); + test_parse_invalid_diff(PATCH_CORRUPT_MISSING_HUNK_HEADER); +} + From 728274904f69fef48752d77c8cf75fc3aaf7808c Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 25 Apr 2016 12:40:19 -0400 Subject: [PATCH 47/54] Introduce `git_diff_to_buf` Like `git_patch_to_buf`, provide a simple helper method that can print an entire diff directory to a `git_buf`. --- include/git2/diff.h | 15 +++++++++++++++ src/diff_print.c | 9 +++++++++ 2 files changed, 24 insertions(+) diff --git a/include/git2/diff.h b/include/git2/diff.h index 880292a1fc7..005b339655f 100644 --- a/include/git2/diff.h +++ b/include/git2/diff.h @@ -1054,6 +1054,21 @@ GIT_EXTERN(int) git_diff_print( git_diff_line_cb print_cb, void *payload); +/** + * Produce the complete formatted text output from a diff into a + * buffer. + * + * @param out A pointer to a user-allocated git_buf that will + * contain the diff text + * @param diff A git_diff generated by one of the above functions. + * @param format A git_diff_format_t value to pick the text format. + * @return 0 on success or error code + */ +GIT_EXTERN(int) git_diff_to_buf( + git_buf *out, + git_diff *diff, + git_diff_format_t format); + /**@}*/ diff --git a/src/diff_print.c b/src/diff_print.c index 5bcb5d016c7..5a5a70b6fd4 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -714,6 +714,15 @@ int git_diff_print_callback__to_file_handle( return 0; } +/* print a git_diff to a git_buf */ +int git_diff_to_buf(git_buf *out, git_diff *diff, git_diff_format_t format) +{ + assert(out && diff); + git_buf_sanitize(out); + return git_diff_print( + diff, format, git_diff_print_callback__to_buf, out); +} + /* print a git_patch to an output callback */ int git_patch_print( git_patch *patch, From 33ae8762392e6577ee8801a00facaea5abda00f5 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 25 Apr 2016 13:07:18 -0400 Subject: [PATCH 48/54] patch: identify non-binary patches as `NOT_BINARY` --- src/patch_parse.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/patch_parse.c b/src/patch_parse.c index ee75663e673..c5cf9fc5a46 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -810,6 +810,8 @@ static int parse_patch_hunks( goto done; } + patch->base.delta->flags |= GIT_DIFF_FLAG_NOT_BINARY; + done: return error; } @@ -819,11 +821,8 @@ static int parse_patch_body( { if (parse_ctx_contains_s(ctx, "GIT binary patch")) return parse_patch_binary(patch, ctx); - - else if (parse_ctx_contains_s(ctx, "@@ -")) + else return parse_patch_hunks(patch, ctx); - - return 0; } int check_header_names( From 853e585fb13475073c7000d74934f6c96c1e1a47 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 25 Apr 2016 16:32:30 -0400 Subject: [PATCH 49/54] patch: zero id and abbrev length for empty files --- src/patch_parse.c | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/patch_parse.c b/src/patch_parse.c index c5cf9fc5a46..cdf48502d48 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -933,20 +933,32 @@ static int check_filenames(git_patch_parsed *patch) static int check_patch(git_patch_parsed *patch) { + git_diff_delta *delta = patch->base.delta; + if (check_filenames(patch) < 0) return -1; - if (patch->base.delta->old_file.path && - patch->base.delta->status != GIT_DELTA_DELETED && - !patch->base.delta->new_file.mode) - patch->base.delta->new_file.mode = patch->base.delta->old_file.mode; + if (delta->old_file.path && + delta->status != GIT_DELTA_DELETED && + !delta->new_file.mode) + delta->new_file.mode = delta->old_file.mode; - if (patch->base.delta->status == GIT_DELTA_MODIFIED && - !(patch->base.delta->flags & GIT_DIFF_FLAG_BINARY) && - patch->base.delta->new_file.mode == patch->base.delta->old_file.mode && - git_array_size(patch->base.hunks) == 0) + if (delta->status == GIT_DELTA_MODIFIED && + !(delta->flags & GIT_DIFF_FLAG_BINARY) && + delta->new_file.mode == delta->old_file.mode && + git_array_size(patch->base.hunks) == 0) return parse_err("patch with no hunks"); + if (delta->status == GIT_DELTA_ADDED) { + memset(&delta->old_file.id, 0x0, sizeof(git_oid)); + delta->old_file.id_abbrev = 0; + } + + if (delta->status == GIT_DELTA_DELETED) { + memset(&delta->new_file.id, 0x0, sizeof(git_oid)); + delta->new_file.id_abbrev = 0; + } + return 0; } From e774d5af764c521f3a5740f8c0f7b859ebb109c8 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 25 Apr 2016 16:47:48 -0400 Subject: [PATCH 50/54] diff::parse tests: test parsing a diff Test that we can create a diff file, then parse the results and that the two are identical in-memory. --- tests/diff/diff_helpers.c | 36 +++++++++++++++++++++ tests/diff/diff_helpers.h | 3 ++ tests/diff/parse.c | 68 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/tests/diff/diff_helpers.c b/tests/diff/diff_helpers.c index c6cdf803fc4..8fa8e3eb55a 100644 --- a/tests/diff/diff_helpers.c +++ b/tests/diff/diff_helpers.c @@ -241,3 +241,39 @@ void diff_print_raw(FILE *fp, git_diff *diff) git_diff_print(diff, GIT_DIFF_FORMAT_RAW, git_diff_print_callback__to_file_handle, fp ? fp : stderr)); } + +void diff_assert_equal(git_diff *a, git_diff *b) +{ + const git_diff_delta *ad, *bd; + size_t i; + + assert(a && b); + + cl_assert_equal_i(git_diff_num_deltas(a), git_diff_num_deltas(b)); + + for (i = 0; i < git_diff_num_deltas(a); i++) { + ad = git_diff_get_delta(a, i); + bd = git_diff_get_delta(b, i); + + cl_assert_equal_i(ad->status, bd->status); + cl_assert_equal_i(ad->flags, bd->flags); + cl_assert_equal_i(ad->similarity, bd->similarity); + cl_assert_equal_i(ad->nfiles, bd->nfiles); + + /* Don't examine the size or the flags of the deltas; + * computed deltas have sizes (parsed deltas do not) and + * computed deltas will have flags of `VALID_ID` and + * `EXISTS` (parsed deltas will not query the ODB.) + */ + cl_assert_equal_oid(&ad->old_file.id, &bd->old_file.id); + cl_assert_equal_i(ad->old_file.id_abbrev, bd->old_file.id_abbrev); + cl_assert_equal_s(ad->old_file.path, bd->old_file.path); + cl_assert_equal_i(ad->old_file.mode, bd->old_file.mode); + + cl_assert_equal_oid(&ad->new_file.id, &bd->new_file.id); + cl_assert_equal_i(ad->new_file.id_abbrev, bd->new_file.id_abbrev); + cl_assert_equal_s(ad->new_file.path, bd->new_file.path); + cl_assert_equal_i(ad->new_file.mode, bd->new_file.mode); + } +} + diff --git a/tests/diff/diff_helpers.h b/tests/diff/diff_helpers.h index 4d3cd347440..520b654d3cc 100644 --- a/tests/diff/diff_helpers.h +++ b/tests/diff/diff_helpers.h @@ -68,3 +68,6 @@ extern int diff_foreach_via_iterator( extern void diff_print(FILE *fp, git_diff *diff); extern void diff_print_raw(FILE *fp, git_diff *diff); + +extern void diff_assert_equal(git_diff *a, git_diff *b); + diff --git a/tests/diff/parse.c b/tests/diff/parse.c index 8eb98423bbb..2d912c08e2e 100644 --- a/tests/diff/parse.c +++ b/tests/diff/parse.c @@ -1,9 +1,15 @@ #include "clar_libgit2.h" #include "patch.h" #include "patch_parse.h" +#include "diff_helpers.h" #include "../patch/patch_common.h" +void test_diff_parse__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + void test_diff_parse__nonpatches_fail_with_notfound(void) { git_diff *diff; @@ -58,3 +64,65 @@ void test_diff_parse__invalid_patches_fails(void) test_parse_invalid_diff(PATCH_CORRUPT_MISSING_HUNK_HEADER); } +static void test_tree_to_tree_computed_to_parsed( + const char *sandbox, const char *a_id, const char *b_id) +{ + git_repository *repo; + git_diff *computed, *parsed; + git_tree *a, *b; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + git_diff_find_options findopts = GIT_DIFF_FIND_OPTIONS_INIT; + git_buf computed_buf = GIT_BUF_INIT; + + repo = cl_git_sandbox_init(sandbox); + + opts.id_abbrev = GIT_OID_HEXSZ; + opts.flags = GIT_DIFF_SHOW_BINARY; + + cl_assert((a = resolve_commit_oid_to_tree(repo, a_id)) != NULL); + cl_assert((b = resolve_commit_oid_to_tree(repo, b_id)) != NULL); + + cl_git_pass(git_diff_tree_to_tree(&computed, repo, a, b, &opts)); + cl_git_pass(git_diff_to_buf(&computed_buf, + computed, GIT_DIFF_FORMAT_PATCH)); + + cl_git_pass(git_diff_from_buffer(&parsed, + computed_buf.ptr, computed_buf.size)); + + diff_assert_equal(computed, parsed); + + git_tree_free(a); + git_tree_free(b); + + git_diff_free(computed); + git_diff_free(parsed); + + git_buf_free(&computed_buf); + + cl_git_sandbox_cleanup(); +} + +void test_diff_parse__can_parse_generated_diff(void) +{ + test_tree_to_tree_computed_to_parsed("diff", "d70d245e", "7a9e0b02"); + test_tree_to_tree_computed_to_parsed( + "unsymlinked.git", "806999", "a8595c"); + test_tree_to_tree_computed_to_parsed("diff", + "d70d245ed97ed2aa596dd1af6536e4bfdb047b69", + "7a9e0b02e63179929fed24f0a3e0f19168114d10"); + test_tree_to_tree_computed_to_parsed( + "unsymlinked.git", "7fccd7", "806999"); + test_tree_to_tree_computed_to_parsed( + "unsymlinked.git", "7fccd7", "a8595c"); + test_tree_to_tree_computed_to_parsed("attr", "605812a", "370fe9ec22"); + test_tree_to_tree_computed_to_parsed( + "attr", "f5b0af1fb4f5c", "370fe9ec22"); + test_tree_to_tree_computed_to_parsed("diff", "d70d245e", "d70d245e"); + test_tree_to_tree_computed_to_parsed("diff_format_email", + "873806f6f27e631eb0b23e4b56bea2bfac14a373", + "897d3af16ca9e420cd071b1c4541bd2b91d04c8c"); + test_tree_to_tree_computed_to_parsed("diff_format_email", + "897d3af16ca9e420cd071b1c4541bd2b91d04c8c", + "873806f6f27e631eb0b23e4b56bea2bfac14a373"); +} + From 38a347ea5d9d039735d6b8e436c0c144b342aabe Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 25 Apr 2016 17:52:39 -0400 Subject: [PATCH 51/54] patch::parse: handle patches with no hunks Patches may have no hunks when there's no modifications (for example, in a rename). Handle them. --- src/patch_parse.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/patch_parse.c b/src/patch_parse.c index cdf48502d48..72c4d148fe5 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -361,6 +361,7 @@ typedef struct { } header_git_op; static const header_git_op header_git_ops[] = { + { "diff --git ", NULL }, { "@@ -", NULL }, { "GIT binary patch", NULL }, { "--- ", parse_header_git_oldpath }, @@ -437,7 +438,8 @@ static int parse_header_git( } if (!found) { - error = parse_err("invalid patch header at line %d", ctx->line_num); + error = parse_err("invalid patch header at line %d", + ctx->line_num); goto done; } } From 8a670dc4c0a5e64986273f2c79d21afcefa38a05 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 25 Apr 2016 18:08:03 -0400 Subject: [PATCH 52/54] patch::parse: test diff with simple rename --- tests/diff/parse.c | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/tests/diff/parse.c b/tests/diff/parse.c index 2d912c08e2e..24d6a019211 100644 --- a/tests/diff/parse.c +++ b/tests/diff/parse.c @@ -65,7 +65,8 @@ void test_diff_parse__invalid_patches_fails(void) } static void test_tree_to_tree_computed_to_parsed( - const char *sandbox, const char *a_id, const char *b_id) + const char *sandbox, const char *a_id, const char *b_id, + uint32_t diff_flags, uint32_t find_flags) { git_repository *repo; git_diff *computed, *parsed; @@ -77,12 +78,17 @@ static void test_tree_to_tree_computed_to_parsed( repo = cl_git_sandbox_init(sandbox); opts.id_abbrev = GIT_OID_HEXSZ; - opts.flags = GIT_DIFF_SHOW_BINARY; + opts.flags = GIT_DIFF_SHOW_BINARY | diff_flags; + findopts.flags = find_flags; cl_assert((a = resolve_commit_oid_to_tree(repo, a_id)) != NULL); cl_assert((b = resolve_commit_oid_to_tree(repo, b_id)) != NULL); cl_git_pass(git_diff_tree_to_tree(&computed, repo, a, b, &opts)); + + if (find_flags) + cl_git_pass(git_diff_find_similar(computed, &findopts)); + cl_git_pass(git_diff_to_buf(&computed_buf, computed, GIT_DIFF_FORMAT_PATCH)); @@ -104,25 +110,34 @@ static void test_tree_to_tree_computed_to_parsed( void test_diff_parse__can_parse_generated_diff(void) { - test_tree_to_tree_computed_to_parsed("diff", "d70d245e", "7a9e0b02"); test_tree_to_tree_computed_to_parsed( - "unsymlinked.git", "806999", "a8595c"); + "diff", "d70d245e", "7a9e0b02", 0, 0); + test_tree_to_tree_computed_to_parsed( + "unsymlinked.git", "806999", "a8595c", 0, 0); test_tree_to_tree_computed_to_parsed("diff", "d70d245ed97ed2aa596dd1af6536e4bfdb047b69", - "7a9e0b02e63179929fed24f0a3e0f19168114d10"); + "7a9e0b02e63179929fed24f0a3e0f19168114d10", 0, 0); + test_tree_to_tree_computed_to_parsed( + "unsymlinked.git", "7fccd7", "806999", 0, 0); test_tree_to_tree_computed_to_parsed( - "unsymlinked.git", "7fccd7", "806999"); + "unsymlinked.git", "7fccd7", "a8595c", 0, 0); test_tree_to_tree_computed_to_parsed( - "unsymlinked.git", "7fccd7", "a8595c"); - test_tree_to_tree_computed_to_parsed("attr", "605812a", "370fe9ec22"); + "attr", "605812a", "370fe9ec22", 0, 0); test_tree_to_tree_computed_to_parsed( - "attr", "f5b0af1fb4f5c", "370fe9ec22"); - test_tree_to_tree_computed_to_parsed("diff", "d70d245e", "d70d245e"); + "attr", "f5b0af1fb4f5c", "370fe9ec22", 0, 0); + test_tree_to_tree_computed_to_parsed( + "diff", "d70d245e", "d70d245e", 0, 0); test_tree_to_tree_computed_to_parsed("diff_format_email", "873806f6f27e631eb0b23e4b56bea2bfac14a373", - "897d3af16ca9e420cd071b1c4541bd2b91d04c8c"); + "897d3af16ca9e420cd071b1c4541bd2b91d04c8c", + GIT_DIFF_SHOW_BINARY, 0); test_tree_to_tree_computed_to_parsed("diff_format_email", "897d3af16ca9e420cd071b1c4541bd2b91d04c8c", - "873806f6f27e631eb0b23e4b56bea2bfac14a373"); + "873806f6f27e631eb0b23e4b56bea2bfac14a373", + GIT_DIFF_SHOW_BINARY, 0); + test_tree_to_tree_computed_to_parsed("renames", + "31e47d8c1fa36d7f8d537b96158e3f024de0a9f2", + "2bc7f351d20b53f1c72c16c4b036e491c478c49a", + 0, GIT_DIFF_FIND_RENAMES); } From 9eb19381348bca66eedc4d2e541448443311007a Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 25 Apr 2016 22:35:55 -0400 Subject: [PATCH 53/54] patch::parse: test diff with exact rename and copy --- tests/diff/parse.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/diff/parse.c b/tests/diff/parse.c index 24d6a019211..56b98903bbe 100644 --- a/tests/diff/parse.c +++ b/tests/diff/parse.c @@ -139,5 +139,10 @@ void test_diff_parse__can_parse_generated_diff(void) "31e47d8c1fa36d7f8d537b96158e3f024de0a9f2", "2bc7f351d20b53f1c72c16c4b036e491c478c49a", 0, GIT_DIFF_FIND_RENAMES); + test_tree_to_tree_computed_to_parsed("renames", + "31e47d8c1fa36d7f8d537b96158e3f024de0a9f2", + "2bc7f351d20b53f1c72c16c4b036e491c478c49a", + GIT_DIFF_INCLUDE_UNMODIFIED, + GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED | GIT_DIFF_FIND_EXACT_MATCH_ONLY); } From 1a79cd959ba2991dd3295f9940b28b606e494ccf Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Tue, 26 Apr 2016 01:18:01 -0400 Subject: [PATCH 54/54] patch: show copy information for identical copies When showing copy information because we are duplicating contents, for example, when performing a `diff --find-copies-harder -M100 -B100`, then show copy from/to lines in a patch, and do not show context. Ensure that we can also parse such patches. --- src/diff_print.c | 44 ++++++++++++++++++++++-------- src/patch_parse.c | 16 +++++++++++ tests/diff/diff_helpers.c | 57 ++++++++++++++++++++++++++++++++------- tests/diff/format_email.c | 3 --- tests/diff/parse.c | 5 ++++ tests/diff/rename.c | 46 +++++++++++++++++++++++++++++++ 6 files changed, 147 insertions(+), 24 deletions(-) diff --git a/src/diff_print.c b/src/diff_print.c index 5a5a70b6fd4..f72ca8935ef 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -331,11 +331,12 @@ static int diff_delta_format_with_paths( return git_buf_printf(out, template, oldpath, newpath); } -int diff_delta_format_rename_header( +int diff_delta_format_similarity_header( git_buf *out, const git_diff_delta *delta) { git_buf old_path = GIT_BUF_INIT, new_path = GIT_BUF_INIT; + const char *type; int error = 0; if (delta->similarity > 100) { @@ -344,6 +345,13 @@ int diff_delta_format_rename_header( goto done; } + if (delta->status == GIT_DELTA_RENAMED) + type = "rename"; + else if (delta->status == GIT_DELTA_COPIED) + type = "copy"; + else + abort(); + if ((error = git_buf_puts(&old_path, delta->old_file.path)) < 0 || (error = git_buf_puts(&new_path, delta->new_file.path)) < 0 || (error = git_buf_quote(&old_path)) < 0 || @@ -352,11 +360,11 @@ int diff_delta_format_rename_header( git_buf_printf(out, "similarity index %d%%\n" - "rename from %s\n" - "rename to %s\n", + "%s from %s\n" + "%s to %s\n", delta->similarity, - old_path.ptr, - new_path.ptr); + type, old_path.ptr, + type, new_path.ptr); if (git_buf_oom(out)) error = -1; @@ -368,6 +376,22 @@ int diff_delta_format_rename_header( return error; } +static bool delta_is_unchanged(const git_diff_delta *delta) +{ + if (git_oid_iszero(&delta->old_file.id) && + git_oid_iszero(&delta->new_file.id)) + return true; + + if (delta->old_file.mode == GIT_FILEMODE_COMMIT || + delta->new_file.mode == GIT_FILEMODE_COMMIT) + return false; + + if (git_oid_equal(&delta->old_file.id, &delta->new_file.id)) + return true; + + return false; +} + int git_diff_delta__format_file_header( git_buf *out, const git_diff_delta *delta, @@ -376,7 +400,7 @@ int git_diff_delta__format_file_header( int id_strlen) { git_buf old_path = GIT_BUF_INIT, new_path = GIT_BUF_INIT; - bool unchanged; + bool unchanged = delta_is_unchanged(delta); int error = 0; if (!oldpfx) @@ -397,14 +421,12 @@ int git_diff_delta__format_file_header( git_buf_printf(out, "diff --git %s %s\n", old_path.ptr, new_path.ptr); - if (delta->status == GIT_DELTA_RENAMED) { - if ((error = diff_delta_format_rename_header(out, delta)) < 0) + if (delta->status == GIT_DELTA_RENAMED || + (delta->status == GIT_DELTA_COPIED && unchanged)) { + if ((error = diff_delta_format_similarity_header(out, delta)) < 0) goto done; } - unchanged = (git_oid_iszero(&delta->old_file.id) && - git_oid_iszero(&delta->new_file.id)); - if (!unchanged) { if ((error = diff_print_oid_range(out, delta, id_strlen)) < 0) goto done; diff --git a/src/patch_parse.c b/src/patch_parse.c index 72c4d148fe5..7f21e3f8e52 100644 --- a/src/patch_parse.c +++ b/src/patch_parse.c @@ -310,6 +310,20 @@ static int parse_header_renameto( return parse_header_rename(&patch->rename_new_path, ctx); } +static int parse_header_copyfrom( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + patch->base.delta->status = GIT_DELTA_COPIED; + return parse_header_rename(&patch->rename_old_path, ctx); +} + +static int parse_header_copyto( + git_patch_parsed *patch, git_patch_parse_ctx *ctx) +{ + patch->base.delta->status = GIT_DELTA_COPIED; + return parse_header_rename(&patch->rename_new_path, ctx); +} + static int parse_header_percent(uint16_t *out, git_patch_parse_ctx *ctx) { int32_t val; @@ -375,6 +389,8 @@ static const header_git_op header_git_ops[] = { { "rename to ", parse_header_renameto }, { "rename old ", parse_header_renamefrom }, { "rename new ", parse_header_renameto }, + { "copy from ", parse_header_copyfrom }, + { "copy to ", parse_header_copyto }, { "similarity index ", parse_header_similarity }, { "dissimilarity index ", parse_header_dissimilarity }, }; diff --git a/tests/diff/diff_helpers.c b/tests/diff/diff_helpers.c index 8fa8e3eb55a..50752b203ad 100644 --- a/tests/diff/diff_helpers.c +++ b/tests/diff/diff_helpers.c @@ -242,18 +242,44 @@ void diff_print_raw(FILE *fp, git_diff *diff) git_diff_print_callback__to_file_handle, fp ? fp : stderr)); } +static size_t num_modified_deltas(git_diff *diff) +{ + const git_diff_delta *delta; + size_t i, cnt = 0; + + for (i = 0; i < git_diff_num_deltas(diff); i++) { + delta = git_diff_get_delta(diff, i); + + if (delta->status != GIT_DELTA_UNMODIFIED) + cnt++; + } + + return cnt; +} + void diff_assert_equal(git_diff *a, git_diff *b) { const git_diff_delta *ad, *bd; - size_t i; + size_t i, j; assert(a && b); - cl_assert_equal_i(git_diff_num_deltas(a), git_diff_num_deltas(b)); + cl_assert_equal_i(num_modified_deltas(a), num_modified_deltas(b)); + + for (i = 0, j = 0; + i < git_diff_num_deltas(a) && j < git_diff_num_deltas(b); ) { - for (i = 0; i < git_diff_num_deltas(a); i++) { ad = git_diff_get_delta(a, i); - bd = git_diff_get_delta(b, i); + bd = git_diff_get_delta(b, j); + + if (ad->status == GIT_DELTA_UNMODIFIED) { + i++; + continue; + } + if (bd->status == GIT_DELTA_UNMODIFIED) { + j++; + continue; + } cl_assert_equal_i(ad->status, bd->status); cl_assert_equal_i(ad->flags, bd->flags); @@ -265,15 +291,26 @@ void diff_assert_equal(git_diff *a, git_diff *b) * computed deltas will have flags of `VALID_ID` and * `EXISTS` (parsed deltas will not query the ODB.) */ - cl_assert_equal_oid(&ad->old_file.id, &bd->old_file.id); - cl_assert_equal_i(ad->old_file.id_abbrev, bd->old_file.id_abbrev); + + /* an empty id indicates that it wasn't presented, because + * the diff was identical. (eg, pure rename, mode change only, etc) + */ + if (ad->old_file.id_abbrev && bd->old_file.id_abbrev) { + cl_assert_equal_i(ad->old_file.id_abbrev, bd->old_file.id_abbrev); + cl_assert_equal_oid(&ad->old_file.id, &bd->old_file.id); + cl_assert_equal_i(ad->old_file.mode, bd->old_file.mode); + } cl_assert_equal_s(ad->old_file.path, bd->old_file.path); - cl_assert_equal_i(ad->old_file.mode, bd->old_file.mode); - cl_assert_equal_oid(&ad->new_file.id, &bd->new_file.id); - cl_assert_equal_i(ad->new_file.id_abbrev, bd->new_file.id_abbrev); + if (ad->new_file.id_abbrev && bd->new_file.id_abbrev) { + cl_assert_equal_oid(&ad->new_file.id, &bd->new_file.id); + cl_assert_equal_i(ad->new_file.id_abbrev, bd->new_file.id_abbrev); + cl_assert_equal_i(ad->new_file.mode, bd->new_file.mode); + } cl_assert_equal_s(ad->new_file.path, bd->new_file.path); - cl_assert_equal_i(ad->new_file.mode, bd->new_file.mode); + + i++; + j++; } } diff --git a/tests/diff/format_email.c b/tests/diff/format_email.c index 2e6d0368e69..9f8fe314271 100644 --- a/tests/diff/format_email.c +++ b/tests/diff/format_email.c @@ -351,9 +351,6 @@ void test_diff_format_email__mode_change(void) "diff --git a/file1.txt.renamed b/file1.txt.renamed\n" \ "old mode 100644\n" \ "new mode 100755\n" \ - "index a97157a..a97157a\n" \ - "--- a/file1.txt.renamed\n" \ - "+++ b/file1.txt.renamed\n" \ "--\n" \ "libgit2 " LIBGIT2_VERSION "\n" \ "\n"; diff --git a/tests/diff/parse.c b/tests/diff/parse.c index 56b98903bbe..83000a92daa 100644 --- a/tests/diff/parse.c +++ b/tests/diff/parse.c @@ -139,6 +139,11 @@ void test_diff_parse__can_parse_generated_diff(void) "31e47d8c1fa36d7f8d537b96158e3f024de0a9f2", "2bc7f351d20b53f1c72c16c4b036e491c478c49a", 0, GIT_DIFF_FIND_RENAMES); + test_tree_to_tree_computed_to_parsed("renames", + "31e47d8c1fa36d7f8d537b96158e3f024de0a9f2", + "2bc7f351d20b53f1c72c16c4b036e491c478c49a", + GIT_DIFF_INCLUDE_UNMODIFIED, + 0); test_tree_to_tree_computed_to_parsed("renames", "31e47d8c1fa36d7f8d537b96158e3f024de0a9f2", "2bc7f351d20b53f1c72c16c4b036e491c478c49a", diff --git a/tests/diff/rename.c b/tests/diff/rename.c index 5cfd8e23566..c1cd2523918 100644 --- a/tests/diff/rename.c +++ b/tests/diff/rename.c @@ -1702,3 +1702,49 @@ void test_diff_rename__blank_files_not_renamed_when_not_ignoring_whitespace(void expect_files_not_renamed("", "\n\n\n\n", GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE); expect_files_not_renamed("\n\n\n\n", "\r\n\r\n\r\n", GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE); } + +/* test that 100% renames and copies emit the correct patch file + * git diff --find-copies-harder -M100 -B100 \ + * 31e47d8c1fa36d7f8d537b96158e3f024de0a9f2 \ + * 2bc7f351d20b53f1c72c16c4b036e491c478c49a + */ +void test_diff_rename__identical(void) +{ + const char *old_sha = "31e47d8c1fa36d7f8d537b96158e3f024de0a9f2"; + const char *new_sha = "2bc7f351d20b53f1c72c16c4b036e491c478c49a"; + git_tree *old_tree, *new_tree; + git_diff *diff; + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + git_diff_find_options find_opts = GIT_DIFF_FIND_OPTIONS_INIT; + git_buf diff_buf = GIT_BUF_INIT; + const char *expected = + "diff --git a/serving.txt b/sixserving.txt\n" + "similarity index 100%\n" + "rename from serving.txt\n" + "rename to sixserving.txt\n" + "diff --git a/sevencities.txt b/songofseven.txt\n" + "similarity index 100%\n" + "copy from sevencities.txt\n" + "copy to songofseven.txt\n"; + + old_tree = resolve_commit_oid_to_tree(g_repo, old_sha); + new_tree = resolve_commit_oid_to_tree(g_repo, new_sha); + + diff_opts.flags |= GIT_DIFF_INCLUDE_UNMODIFIED; + find_opts.flags = GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED | + GIT_DIFF_FIND_EXACT_MATCH_ONLY; + + cl_git_pass(git_diff_tree_to_tree(&diff, + g_repo, old_tree, new_tree, &diff_opts)); + cl_git_pass(git_diff_find_similar(diff, &find_opts)); + + cl_git_pass(git_diff_to_buf(&diff_buf, diff, GIT_DIFF_FORMAT_PATCH)); + + cl_assert_equal_s(expected, diff_buf.ptr); + + git_buf_free(&diff_buf); + git_diff_free(diff); + git_tree_free(old_tree); + git_tree_free(new_tree); +} +