diff options
author | Edward Thomson <ethomson@edwardthomson.com> | 2018-11-11 16:40:56 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-11-11 16:40:56 +0000 |
commit | 11fbead80b425eacf483fe16beaf8891f582f905 (patch) | |
tree | 347128ac534ff09809772ac5e87f6ccbbb742793 | |
parent | 2f5f3cfdcd7b15fc74fa1ed4b1695150ed071504 (diff) | |
parent | 4e746d80d229a77b08c5689a64d880bde5fd960f (diff) | |
download | libgit2-11fbead80b425eacf483fe16beaf8891f582f905.tar.gz |
Merge pull request #4705 from libgit2/ethomson/apply
Patch (diff) application
-rw-r--r-- | include/git2.h | 1 | ||||
-rw-r--r-- | include/git2/apply.h | 129 | ||||
-rw-r--r-- | include/git2/errors.h | 1 | ||||
-rw-r--r-- | src/apply.c | 490 | ||||
-rw-r--r-- | src/apply.h | 4 | ||||
-rw-r--r-- | src/iterator.c | 90 | ||||
-rw-r--r-- | src/iterator.h | 15 | ||||
-rw-r--r-- | src/reader.c | 259 | ||||
-rw-r--r-- | src/reader.h | 107 | ||||
-rw-r--r-- | tests/apply/apply_helpers.h | 587 | ||||
-rw-r--r-- | tests/apply/both.c | 736 | ||||
-rw-r--r-- | tests/apply/callbacks.c | 128 | ||||
-rw-r--r-- | tests/apply/fromdiff.c | 48 | ||||
-rw-r--r-- | tests/apply/fromfile.c | 2 | ||||
-rw-r--r-- | tests/apply/index.c | 321 | ||||
-rw-r--r-- | tests/apply/partial.c | 232 | ||||
-rw-r--r-- | tests/apply/tree.c | 58 | ||||
-rw-r--r-- | tests/apply/workdir.c | 358 | ||||
-rw-r--r-- | tests/iterator/workdir.c | 46 | ||||
-rw-r--r-- | tests/patch/patch_common.h | 106 | ||||
-rw-r--r-- | tests/resources/merge-recursive/.gitted/objects/06/d3fefb8726ab1099acc76e02dfb85e034b2538 | bin | 0 -> 375 bytes |
21 files changed, 3697 insertions, 21 deletions
diff --git a/include/git2.h b/include/git2.h index e182ce924..9239b48c8 100644 --- a/include/git2.h +++ b/include/git2.h @@ -9,6 +9,7 @@ #define INCLUDE_git_git_h__ #include "git2/annotated_commit.h" +#include "git2/apply.h" #include "git2/attr.h" #include "git2/blob.h" #include "git2/blame.h" diff --git a/include/git2/apply.h b/include/git2/apply.h new file mode 100644 index 000000000..7cc1c22a8 --- /dev/null +++ b/include/git2/apply.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_git_apply_h__ +#define INCLUDE_git_apply_h__ + +#include "common.h" +#include "types.h" +#include "oid.h" +#include "diff.h" + +/** + * @file git2/apply.h + * @brief Git patch application routines + * @defgroup git_apply Git patch application routines + * @ingroup Git + * @{ + */ +GIT_BEGIN_DECL + +/** + * When applying a patch, callback that will be made per delta (file). + * + * When the callback: + * - returns < 0, the apply process will be aborted. + * - returns > 0, the delta will not be applied, but the apply process + * continues + * - returns 0, the delta is applied, and the apply process continues. + * + * @param delta The delta to be applied + * @param payload User-specified payload + */ +typedef int (*git_apply_delta_cb)( + const git_diff_delta *delta, + void *payload); + +/** + * When applying a patch, callback that will be made per hunk. + * + * When the callback: + * - returns < 0, the apply process will be aborted. + * - returns > 0, the hunk will not be applied, but the apply process + * continues + * - returns 0, the hunk is applied, and the apply process continues. + * + * @param hunk The hunk to be applied + * @param payload User-specified payload + */ +typedef int (*git_apply_hunk_cb)( + const git_diff_hunk *hunk, + void *payload); + +/** + * Apply options structure + * + * Initialize with `GIT_APPLY_OPTIONS_INIT`. Alternatively, you can + * use `git_apply_init_options`. + * + * @see git_apply_to_tree, git_apply + */ +typedef struct { + unsigned int version; + + git_apply_delta_cb delta_cb; + git_apply_hunk_cb hunk_cb; + void *payload; +} git_apply_options; + +#define GIT_APPLY_OPTIONS_VERSION 1 +#define GIT_APPLY_OPTIONS_INIT {GIT_APPLY_OPTIONS_VERSION} + +/** + * Apply a `git_diff` to a `git_tree`, and return the resulting image + * as an index. + * + * @param out the postimage of the application + * @param repo the repository to apply + * @param preimage the tree to apply the diff to + * @param diff the diff to apply + * @param options the options for the apply (or null for defaults) + */ +GIT_EXTERN(int) git_apply_to_tree( + git_index **out, + git_repository *repo, + git_tree *preimage, + git_diff *diff, + const git_apply_options *options); + +typedef enum { + /** + * Apply the patch to the workdir, leaving the index untouched. + * This is the equivalent of `git apply` with no location argument. + */ + GIT_APPLY_LOCATION_WORKDIR = 0, + + /** + * Apply the patch to the index, leaving the working directory + * untouched. This is the equivalent of `git apply --cached`. + */ + GIT_APPLY_LOCATION_INDEX = 1, + + /** + * Apply the patch to both the working directory and the index. + * This is the equivalent of `git apply --index`. + */ + GIT_APPLY_LOCATION_BOTH = 2, +} git_apply_location_t; + +/** + * Apply a `git_diff` to the given repository, making changes directly + * in the working directory, the index, or both. + * + * @param repo the repository to apply to + * @param diff the diff to apply + * @param location the location to apply (workdir, index or both) + * @param options the options for the apply (or null for defaults) + */ +GIT_EXTERN(int) git_apply( + git_repository *repo, + git_diff *diff, + git_apply_location_t location, + const git_apply_options *options); + +/** @} */ +GIT_END_DECL +#endif diff --git a/include/git2/errors.h b/include/git2/errors.h index c2a01de76..b0ce45fe5 100644 --- a/include/git2/errors.h +++ b/include/git2/errors.h @@ -57,6 +57,7 @@ typedef enum { GIT_RETRY = -32, /**< Internal only */ GIT_EMISMATCH = -33, /**< Hashsum mismatch in object */ GIT_EINDEXDIRTY = -34, /**< Unsaved changes in the index would be overwritten */ + GIT_EAPPLYFAIL = -35, /**< Patch application failed */ } git_error_code; /** diff --git a/src/apply.c b/src/apply.c index 8c7bb6bf3..614baf7a3 100644 --- a/src/apply.c +++ b/src/apply.c @@ -9,16 +9,23 @@ #include <assert.h> +#include "git2/apply.h" #include "git2/patch.h" #include "git2/filter.h" +#include "git2/blob.h" +#include "git2/index.h" +#include "git2/checkout.h" +#include "git2/repository.h" #include "array.h" #include "patch.h" #include "fileops.h" #include "delta.h" #include "zstream.h" +#include "reader.h" +#include "index.h" #define apply_err(...) \ - ( giterr_set(GITERR_PATCH, __VA_ARGS__), -1 ) + ( giterr_set(GITERR_PATCH, __VA_ARGS__), GIT_EAPPLYFAIL ) typedef struct { /* The lines that we allocate ourself are allocated out of the pool. @@ -160,15 +167,36 @@ static int update_hunk( return 0; } +typedef struct { + git_apply_options opts; + size_t skipped_new_lines; + size_t skipped_old_lines; +} apply_hunks_ctx; + static int apply_hunk( patch_image *image, git_patch *patch, - git_patch_hunk *hunk) + git_patch_hunk *hunk, + apply_hunks_ctx *ctx) { patch_image preimage = PATCH_IMAGE_INIT, postimage = PATCH_IMAGE_INIT; size_t line_num, i; int error = 0; + if (ctx->opts.hunk_cb) { + error = ctx->opts.hunk_cb(&hunk->hunk, ctx->opts.payload); + + if (error) { + if (error > 0) { + ctx->skipped_new_lines += hunk->hunk.new_lines; + ctx->skipped_old_lines += hunk->hunk.old_lines; + error = 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); @@ -191,7 +219,14 @@ static int apply_hunk( } } - line_num = hunk->hunk.new_start ? hunk->hunk.new_start - 1 : 0; + if (hunk->hunk.new_start) { + line_num = hunk->hunk.new_start - + ctx->skipped_new_lines + + ctx->skipped_old_lines - + 1; + } else { + line_num = 0; + } if (!find_hunk_linenum(&line_num, image, &preimage, line_num)) { error = apply_err("hunk at line %d did not apply", @@ -212,7 +247,8 @@ static int apply_hunks( git_buf *out, const char *source, size_t source_len, - git_patch *patch) + git_patch *patch, + apply_hunks_ctx *ctx) { git_patch_hunk *hunk; git_diff_line *line; @@ -224,7 +260,7 @@ static int apply_hunks( goto done; git_array_foreach(patch->hunks, i, hunk) { - if ((error = apply_hunk(&image, patch, hunk)) < 0) + if ((error = apply_hunk(&image, patch, hunk, ctx)) < 0) goto done; } @@ -332,14 +368,19 @@ int git_apply__patch( unsigned int *mode_out, const char *source, size_t source_len, - git_patch *patch) + git_patch *patch, + const git_apply_options *given_opts) { + apply_hunks_ctx ctx = { GIT_APPLY_OPTIONS_INIT }; char *filename = NULL; unsigned int mode = 0; int error = 0; assert(contents_out && filename_out && mode_out && (source || !source_len) && patch); + if (given_opts) + memcpy(&ctx.opts, given_opts, sizeof(git_apply_options)); + *filename_out = NULL; *mode_out = 0; @@ -354,7 +395,7 @@ int git_apply__patch( 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); + error = apply_hunks(contents_out, source, source_len, patch, &ctx); else error = git_buf_put(contents_out, source, source_len); @@ -376,3 +417,438 @@ done: return error; } + +static int apply_one( + git_repository *repo, + git_reader *preimage_reader, + git_index *preimage, + git_reader *postimage_reader, + git_index *postimage, + git_diff *diff, + git_strmap *removed_paths, + size_t i, + const git_apply_options *opts) +{ + git_patch *patch = NULL; + git_buf pre_contents = GIT_BUF_INIT, post_contents = GIT_BUF_INIT; + const git_diff_delta *delta; + char *filename = NULL; + unsigned int mode; + git_oid pre_id, post_id; + git_filemode_t pre_filemode; + git_index_entry pre_entry, post_entry; + bool skip_preimage = false; + size_t pos; + int error; + + if ((error = git_patch_from_diff(&patch, diff, i)) < 0) + goto done; + + delta = git_patch_get_delta(patch); + + if (opts->delta_cb) { + error = opts->delta_cb(delta, opts->payload); + + if (error) { + if (error > 0) + error = 0; + + goto done; + } + } + + /* + * Ensure that the file has not been deleted or renamed if we're + * applying a modification delta. + */ + if (delta->status != GIT_DELTA_RENAMED && + delta->status != GIT_DELTA_ADDED) { + pos = git_strmap_lookup_index(removed_paths, delta->old_file.path); + if (git_strmap_valid_index(removed_paths, pos)) { + error = apply_err("path '%s' has been renamed or deleted", delta->old_file.path); + goto done; + } + } + + /* + * We may be applying a second delta to an already seen file. If so, + * use the already modified data in the postimage instead of the + * content from the index or working directory. (Don't do this in + * the case of a rename, which must be specified before additional + * deltas since we apply deltas to the target filename.) + */ + if (delta->status != GIT_DELTA_RENAMED) { + if ((error = git_reader_read(&pre_contents, &pre_id, &pre_filemode, + postimage_reader, delta->old_file.path)) == 0) { + skip_preimage = true; + } else if (error == GIT_ENOTFOUND) { + giterr_clear(); + error = 0; + } else { + goto done; + } + } + + if (!skip_preimage && delta->status != GIT_DELTA_ADDED) { + error = git_reader_read(&pre_contents, &pre_id, &pre_filemode, + preimage_reader, delta->old_file.path); + + /* ENOTFOUND means the preimage was not found; apply failed. */ + if (error == GIT_ENOTFOUND) + error = GIT_EAPPLYFAIL; + + /* When applying to BOTH, the index did not match the workdir. */ + if (error == GIT_READER_MISMATCH) + error = apply_err("%s: does not match index", delta->old_file.path); + + if (error < 0) + goto done; + + /* + * We need to populate the preimage data structure with the + * contents that we are using as the preimage for this file. + * This allows us to apply patches to files that have been + * modified in the working directory. During checkout, + * we will use this expected preimage as the baseline, and + * limit checkout to only the paths affected by patch + * application. (Without this, we would fail to write the + * postimage contents to any file that had been modified + * from HEAD on-disk, even if the patch application succeeded.) + * Use the contents from the delta where available - some + * fields may not be available, like the old file mode (eg in + * an exact rename situation) so trust the patch parsing to + * validate and use the preimage data in that case. + */ + if (preimage) { + memset(&pre_entry, 0, sizeof(git_index_entry)); + pre_entry.path = delta->old_file.path; + pre_entry.mode = delta->old_file.mode ? delta->old_file.mode : pre_filemode; + git_oid_cpy(&pre_entry.id, &pre_id); + + if ((error = git_index_add(preimage, &pre_entry)) < 0) + goto done; + } + } + + if (delta->status != GIT_DELTA_DELETED) { + if ((error = git_apply__patch(&post_contents, &filename, &mode, + pre_contents.ptr, pre_contents.size, patch, opts)) < 0 || + (error = git_blob_create_frombuffer(&post_id, repo, + post_contents.ptr, post_contents.size)) < 0) + goto done; + + memset(&post_entry, 0, sizeof(git_index_entry)); + post_entry.path = filename; + post_entry.mode = mode; + git_oid_cpy(&post_entry.id, &post_id); + + if ((error = git_index_add(postimage, &post_entry)) < 0) + goto done; + } + + if (delta->status == GIT_DELTA_RENAMED || + delta->status == GIT_DELTA_DELETED) + git_strmap_insert(removed_paths, delta->old_file.path, (char *)delta->old_file.path, &error); + + if (delta->status == GIT_DELTA_RENAMED || + delta->status == GIT_DELTA_ADDED) + git_strmap_delete(removed_paths, delta->new_file.path); + +done: + git_buf_dispose(&pre_contents); + git_buf_dispose(&post_contents); + git__free(filename); + git_patch_free(patch); + + return error; +} + +static int apply_deltas( + git_repository *repo, + git_reader *pre_reader, + git_index *preimage, + git_reader *post_reader, + git_index *postimage, + git_diff *diff, + const git_apply_options *opts) +{ + git_strmap *removed_paths; + size_t i; + int error; + + if (git_strmap_alloc(&removed_paths) < 0) + return -1; + + for (i = 0; i < git_diff_num_deltas(diff); i++) { + if ((error = apply_one(repo, pre_reader, preimage, post_reader, postimage, diff, removed_paths, i, opts)) < 0) + goto done; + } + +done: + git_strmap_free(removed_paths); + return error; +} + +int git_apply_to_tree( + git_index **out, + git_repository *repo, + git_tree *preimage, + git_diff *diff, + const git_apply_options *given_opts) +{ + git_index *postimage = NULL; + git_reader *pre_reader = NULL, *post_reader = NULL; + git_apply_options opts = GIT_APPLY_OPTIONS_INIT; + const git_diff_delta *delta; + size_t i; + int error = 0; + + assert(out && repo && preimage && diff); + + *out = NULL; + + if (given_opts) + memcpy(&opts, given_opts, sizeof(git_apply_options)); + + if ((error = git_reader_for_tree(&pre_reader, preimage)) < 0) + goto done; + + /* + * put the current tree into the postimage as-is - the diff will + * replace any entries contained therein + */ + if ((error = git_index_new(&postimage)) < 0 || + (error = git_index_read_tree(postimage, preimage)) < 0 || + (error = git_reader_for_index(&post_reader, repo, postimage)) < 0) + goto done; + + /* + * Remove the old paths from the index before applying diffs - + * we need to do a full pass to remove them before adding deltas, + * in order to handle rename situations. + */ + for (i = 0; i < git_diff_num_deltas(diff); i++) { + delta = git_diff_get_delta(diff, i); + + if ((error = git_index_remove(postimage, + delta->old_file.path, 0)) < 0) + goto done; + } + + if ((error = apply_deltas(repo, pre_reader, NULL, post_reader, postimage, diff, &opts)) < 0) + goto done; + + *out = postimage; + +done: + if (error < 0) + git_index_free(postimage); + + git_reader_free(pre_reader); + git_reader_free(post_reader); + + return error; +} + +static int git_apply__to_workdir( + git_repository *repo, + git_diff *diff, + git_index *preimage, + git_index *postimage, + git_apply_location_t location, + git_apply_options *opts) +{ + git_vector paths = GIT_VECTOR_INIT; + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + const git_diff_delta *delta; + size_t i; + int error; + + GIT_UNUSED(opts); + + /* + * Limit checkout to the paths affected by the diff; this ensures + * that other modifications in the working directory are unaffected. + */ + if ((error = git_vector_init(&paths, git_diff_num_deltas(diff), NULL)) < 0) + goto done; + + for (i = 0; i < git_diff_num_deltas(diff); i++) { + delta = git_diff_get_delta(diff, i); + + if ((error = git_vector_insert(&paths, (void *)delta->old_file.path)) < 0) + goto done; + + if (strcmp(delta->old_file.path, delta->new_file.path) && + (error = git_vector_insert(&paths, (void *)delta->new_file.path)) < 0) + goto done; + } + + checkout_opts.checkout_strategy |= GIT_CHECKOUT_SAFE; + checkout_opts.checkout_strategy |= GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH; + checkout_opts.checkout_strategy |= GIT_CHECKOUT_DONT_WRITE_INDEX; + + if (location == GIT_APPLY_LOCATION_WORKDIR) + checkout_opts.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; + + checkout_opts.paths.strings = (char **)paths.contents; + checkout_opts.paths.count = paths.length; + + checkout_opts.baseline_index = preimage; + + error = git_checkout_index(repo, postimage, &checkout_opts); + +done: + git_vector_free(&paths); + return error; +} + +static int git_apply__to_index( + git_repository *repo, + git_diff *diff, + git_index *preimage, + git_index *postimage, + git_apply_options *opts) +{ + git_index *index = NULL; + const git_diff_delta *delta; + const git_index_entry *entry; + size_t i; + int error; + + GIT_UNUSED(preimage); + GIT_UNUSED(opts); + + if ((error = git_repository_index(&index, repo)) < 0) + goto done; + + /* Remove deleted (or renamed) paths from the index. */ + for (i = 0; i < git_diff_num_deltas(diff); i++) { + delta = git_diff_get_delta(diff, i); + + if (delta->status == GIT_DELTA_DELETED || + delta->status == GIT_DELTA_RENAMED) { + if ((error = git_index_remove(index, delta->old_file.path, 0)) < 0) + goto done; + } + } + + /* Then add the changes back to the index. */ + for (i = 0; i < git_index_entrycount(postimage); i++) { + entry = git_index_get_byindex(postimage, i); + + if ((error = git_index_add(index, entry)) < 0) + goto done; + } + +done: + git_index_free(index); + return error; +} + +/* + * Handle the three application options ("locations"): + * + * GIT_APPLY_LOCATION_WORKDIR: the default, emulates `git apply`. + * Applies the diff only to the workdir items and ignores the index + * entirely. + * + * GIT_APPLY_LOCATION_INDEX: emulates `git apply --cached`. + * Applies the diff only to the index items and ignores the workdir + * completely. + * + * GIT_APPLY_LOCATION_BOTH: emulates `git apply --index`. + * Applies the diff to both the index items and the working directory + * items. + */ + +int git_apply( + git_repository *repo, + git_diff *diff, + git_apply_location_t location, + const git_apply_options *given_opts) +{ + git_indexwriter indexwriter = GIT_INDEXWRITER_INIT; + git_index *index = NULL, *preimage = NULL, *postimage = NULL; + git_reader *pre_reader = NULL, *post_reader = NULL; + git_apply_options opts = GIT_APPLY_OPTIONS_INIT; + int error = GIT_EINVALID; + + assert(repo && diff); + + GITERR_CHECK_VERSION( + given_opts, GIT_APPLY_OPTIONS_VERSION, "git_apply_options"); + + if (given_opts) + memcpy(&opts, given_opts, sizeof(git_apply_options)); + + /* + * by default, we apply a patch directly to the working directory; + * in `--cached` or `--index` mode, we apply to the contents already + * in the index. + */ + switch (location) { + case GIT_APPLY_LOCATION_BOTH: + error = git_reader_for_workdir(&pre_reader, repo, true); + break; + case GIT_APPLY_LOCATION_INDEX: + error = git_reader_for_index(&pre_reader, repo, NULL); + break; + case GIT_APPLY_LOCATION_WORKDIR: + error = git_reader_for_workdir(&pre_reader, repo, false); + break; + default: + assert(false); + } + + if (error < 0) + goto done; + + /* + * Build the preimage and postimage (differences). Note that + * this is not the complete preimage or postimage, it only + * contains the files affected by the patch. We want to avoid + * having the full repo index, so we will limit our checkout + * to only write these files that were affected by the diff. + */ + if ((error = git_index_new(&preimage)) < 0 || + (error = git_index_new(&postimage)) < 0 || + (error = git_reader_for_index(&post_reader, repo, postimage)) < 0) + goto done; + + if ((error = git_repository_index(&index, repo)) < 0 || + (error = git_indexwriter_init(&indexwriter, index)) < 0) + goto done; + + if ((error = apply_deltas(repo, pre_reader, preimage, post_reader, postimage, diff, &opts)) < 0) + goto done; + + switch (location) { + case GIT_APPLY_LOCATION_BOTH: + error = git_apply__to_workdir(repo, diff, preimage, postimage, location, &opts); + break; + case GIT_APPLY_LOCATION_INDEX: + error = git_apply__to_index(repo, diff, preimage, postimage, &opts); + break; + case GIT_APPLY_LOCATION_WORKDIR: + error = git_apply__to_workdir(repo, diff, preimage, postimage, location, &opts); + break; + default: + assert(false); + } + + if (error < 0) + goto done; + + error = git_indexwriter_commit(&indexwriter); + +done: + git_indexwriter_cleanup(&indexwriter); + git_index_free(postimage); + git_index_free(preimage); + git_index_free(index); + git_reader_free(pre_reader); + git_reader_free(post_reader); + + return error; +} diff --git a/src/apply.h b/src/apply.h index b29460c0b..11ec75637 100644 --- a/src/apply.h +++ b/src/apply.h @@ -10,6 +10,7 @@ #include "common.h" #include "git2/patch.h" +#include "git2/apply.h" #include "buffer.h" extern int git_apply__patch( @@ -18,6 +19,7 @@ extern int git_apply__patch( unsigned int *mode, const char *source, size_t source_len, - git_patch *patch); + git_patch *patch, + const git_apply_options *opts); #endif diff --git a/src/iterator.c b/src/iterator.c index 6e7300af3..40f675957 100644 --- a/src/iterator.c +++ b/src/iterator.c @@ -1015,6 +1015,7 @@ typedef struct { struct stat st; size_t path_len; iterator_pathlist_search_t match; + git_oid id; char path[GIT_FLEX_ARRAY]; } filesystem_iterator_entry; @@ -1265,7 +1266,32 @@ GIT_INLINE(bool) filesystem_iterator_is_dot_git( return (len == 4 || path[len - 5] == '/'); } -static filesystem_iterator_entry *filesystem_iterator_entry_init( +static int filesystem_iterator_entry_hash( + filesystem_iterator *iter, + filesystem_iterator_entry *entry) +{ + git_buf fullpath = GIT_BUF_INIT; + int error; + + if (S_ISDIR(entry->st.st_mode)) { + memset(&entry->id, 0, GIT_OID_RAWSZ); + return 0; + } + + if (iter->base.type == GIT_ITERATOR_TYPE_WORKDIR) + return git_repository_hashfile(&entry->id, + iter->base.repo, entry->path, GIT_OBJ_BLOB, NULL); + + if (!(error = git_buf_joinpath(&fullpath, iter->root, entry->path))) + error = git_odb_hashfile(&entry->id, fullpath.ptr, GIT_OBJ_BLOB); + + git_buf_dispose(&fullpath); + return error; +} + +static int filesystem_iterator_entry_init( + filesystem_iterator_entry **out, + filesystem_iterator *iter, filesystem_iterator_frame *frame, const char *path, size_t path_len, @@ -1274,15 +1300,19 @@ static filesystem_iterator_entry *filesystem_iterator_entry_init( { filesystem_iterator_entry *entry; size_t entry_size; + int error = 0; + + *out = NULL; /* Make sure to append two bytes, one for the path's null * termination, one for a possible trailing '/' for folders. */ - if (GIT_ADD_SIZET_OVERFLOW(&entry_size, - sizeof(filesystem_iterator_entry), path_len) || - GIT_ADD_SIZET_OVERFLOW(&entry_size, entry_size, 2) || - (entry = git_pool_malloc(&frame->entry_pool, entry_size)) == NULL) - return NULL; + GITERR_CHECK_ALLOC_ADD(&entry_size, + sizeof(filesystem_iterator_entry), path_len); + GITERR_CHECK_ALLOC_ADD(&entry_size, entry_size, 2); + + entry = git_pool_malloc(&frame->entry_pool, entry_size); + GITERR_CHECK_ALLOC(entry); entry->path_len = path_len; entry->match = pathlist_match; @@ -1295,7 +1325,13 @@ static filesystem_iterator_entry *filesystem_iterator_entry_init( entry->path[entry->path_len] = '\0'; - return entry; + if (iter->base.flags & GIT_ITERATOR_INCLUDE_HASH) + error = filesystem_iterator_entry_hash(iter, entry); + + if (!error) + *out = entry; + + return error; } static int filesystem_iterator_frame_push( @@ -1418,9 +1454,9 @@ static int filesystem_iterator_frame_push( else if (dir_expected) continue; - entry = filesystem_iterator_entry_init(new_frame, - path, path_len, &statbuf, pathlist_match); - GITERR_CHECK_ALLOC(entry); + if ((error = filesystem_iterator_entry_init(&entry, + iter, new_frame, path, path_len, &statbuf, pathlist_match)) < 0) + goto done; git_vector_insert(&new_frame->entries, entry); } @@ -1460,7 +1496,7 @@ static void filesystem_iterator_set_current( iter->entry.ctime.seconds = entry->st.st_ctime; iter->entry.mtime.seconds = entry->st.st_mtime; -#if defined(GIT_USE_NSEC) +#if defined(GIT_USE_NSEC) iter->entry.ctime.nanoseconds = entry->st.st_ctime_nsec; iter->entry.mtime.nanoseconds = entry->st.st_mtime_nsec; #else @@ -1475,6 +1511,9 @@ static void filesystem_iterator_set_current( iter->entry.gid = entry->st.st_gid; iter->entry.file_size = entry->st.st_size; + if (iter->base.flags & GIT_ITERATOR_INCLUDE_HASH) + git_oid_cpy(&iter->entry.id, &entry->id); + iter->entry.path = entry->path; iter->current_is_ignored = GIT_IGNORE_UNCHECKED; @@ -2259,6 +2298,35 @@ void git_iterator_free(git_iterator *iter) git__free(iter); } +int git_iterator_foreach( + git_iterator *iterator, + git_iterator_foreach_cb cb, + void *data) +{ + const git_index_entry *iterator_item; + int error = 0; + + if ((error = git_iterator_current(&iterator_item, iterator)) < 0) + goto done; + + if ((error = cb(iterator_item, data)) != 0) + goto done; + + while (true) { + if ((error = git_iterator_advance(&iterator_item, iterator)) < 0) + goto done; + + if ((error = cb(iterator_item, data)) != 0) + goto done; + } + +done: + if (error == GIT_ITEROVER) + error = 0; + + return error; +} + int git_iterator_walk( git_iterator **iterators, size_t cnt, diff --git a/src/iterator.h b/src/iterator.h index a6497d87b..bbe357fbd 100644 --- a/src/iterator.h +++ b/src/iterator.h @@ -41,6 +41,8 @@ typedef enum { GIT_ITERATOR_INCLUDE_CONFLICTS = (1u << 6), /** descend into symlinked directories */ GIT_ITERATOR_DESCEND_SYMLINKS = (1u << 7), + /** hash files in workdir or filesystem iterators */ + GIT_ITERATOR_INCLUDE_HASH = (1u << 8), } git_iterator_flag_t; typedef enum { @@ -289,6 +291,19 @@ extern int git_iterator_current_workdir_path( */ extern git_index *git_iterator_index(git_iterator *iter); +typedef int (*git_iterator_foreach_cb)( + const git_index_entry *entry, + void *data); + +/** + * Walk the given iterator and invoke the callback for each path + * contained in the iterator. + */ +extern int git_iterator_foreach( + git_iterator *iterator, + git_iterator_foreach_cb cb, + void *data); + typedef int (*git_iterator_walk_cb)( const git_index_entry **entries, void *data); diff --git a/src/reader.c b/src/reader.c new file mode 100644 index 000000000..9375ff3f5 --- /dev/null +++ b/src/reader.c @@ -0,0 +1,259 @@ +/* + * 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 "reader.h" + +#include "fileops.h" +#include "blob.h" + +#include "git2/tree.h" +#include "git2/blob.h" +#include "git2/index.h" +#include "git2/repository.h" + +/* tree reader */ + +typedef struct { + git_reader reader; + git_tree *tree; +} tree_reader; + +static int tree_reader_read( + git_buf *out, + git_oid *out_id, + git_filemode_t *out_filemode, + git_reader *_reader, + const char *filename) +{ + tree_reader *reader = (tree_reader *)_reader; + git_tree_entry *tree_entry = NULL; + git_blob *blob = NULL; + int error; + + if ((error = git_tree_entry_bypath(&tree_entry, reader->tree, filename)) < 0 || + (error = git_blob_lookup(&blob, git_tree_owner(reader->tree), git_tree_entry_id(tree_entry))) < 0 || + (error = git_buf_set(out, git_blob_rawcontent(blob), git_blob_rawsize(blob))) < 0) + goto done; + + if (out_id) + git_oid_cpy(out_id, git_tree_entry_id(tree_entry)); + + if (out_filemode) + *out_filemode = git_tree_entry_filemode(tree_entry); + +done: + git_blob_free(blob); + git_tree_entry_free(tree_entry); + return error; +} + +int git_reader_for_tree(git_reader **out, git_tree *tree) +{ + tree_reader *reader; + + assert(out && tree); + + reader = git__calloc(1, sizeof(tree_reader)); + GITERR_CHECK_ALLOC(reader); + + reader->reader.read = tree_reader_read; + reader->tree = tree; + + *out = (git_reader *)reader; + return 0; +} + +/* workdir reader */ + +typedef struct { + git_reader reader; + git_repository *repo; + git_index *index; +} workdir_reader; + +static int workdir_reader_read( + git_buf *out, + git_oid *out_id, + git_filemode_t *out_filemode, + git_reader *_reader, + const char *filename) +{ + workdir_reader *reader = (workdir_reader *)_reader; + git_buf path = GIT_BUF_INIT; + struct stat st; + git_filemode_t filemode; + git_filter_list *filters = NULL; + const git_index_entry *idx_entry; + git_oid id; + int error; + + if ((error = git_buf_joinpath(&path, + git_repository_workdir(reader->repo), filename)) < 0) + goto done; + + if ((error = p_lstat(path.ptr, &st)) < 0) { + if (error == -1 && errno == ENOENT) + error = GIT_ENOTFOUND; + + giterr_set(GITERR_OS, "could not stat '%s'", path.ptr); + goto done; + } + + filemode = git_futils_canonical_mode(st.st_mode); + + /* + * Patch application - for example - uses the filtered version of + * the working directory data to match git. So we will run the + * workdir -> ODB filter on the contents in this workdir reader. + */ + if ((error = git_filter_list_load(&filters, reader->repo, NULL, filename, + GIT_FILTER_TO_ODB, GIT_FILTER_DEFAULT)) < 0) + goto done; + + if ((error = git_filter_list_apply_to_file(out, + filters, reader->repo, path.ptr)) < 0) + goto done; + + if (out_id || reader->index) { + if ((error = git_odb_hash(&id, out->ptr, out->size, GIT_OBJ_BLOB)) < 0) + goto done; + } + + if (reader->index) { + if (!(idx_entry = git_index_get_bypath(reader->index, filename, 0)) || + filemode != idx_entry->mode || + !git_oid_equal(&id, &idx_entry->id)) { + error = GIT_READER_MISMATCH; + goto done; + } + } + + if (out_id) + git_oid_cpy(out_id, &id); + + if (out_filemode) + *out_filemode = filemode; + +done: + git_filter_list_free(filters); + git_buf_dispose(&path); + return error; +} + +int git_reader_for_workdir( + git_reader **out, + git_repository *repo, + bool validate_index) +{ + workdir_reader *reader; + int error; + + assert(out && repo); + + reader = git__calloc(1, sizeof(workdir_reader)); + GITERR_CHECK_ALLOC(reader); + + reader->reader.read = workdir_reader_read; + reader->repo = repo; + + if (validate_index && + (error = git_repository_index__weakptr(&reader->index, repo)) < 0) { + git__free(reader); + return error; + } + + *out = (git_reader *)reader; + return 0; +} + +/* index reader */ + +typedef struct { + git_reader reader; + git_repository *repo; + git_index *index; +} index_reader; + +static int index_reader_read( + git_buf *out, + git_oid *out_id, + git_filemode_t *out_filemode, + git_reader *_reader, + const char *filename) +{ + index_reader *reader = (index_reader *)_reader; + const git_index_entry *entry; + git_blob *blob; + int error; + + if ((entry = git_index_get_bypath(reader->index, filename, 0)) == NULL) + return GIT_ENOTFOUND; + + if ((error = git_blob_lookup(&blob, reader->repo, &entry->id)) < 0) + goto done; + + if (out_id) + git_oid_cpy(out_id, &entry->id); + + if (out_filemode) + *out_filemode = entry->mode; + + error = git_blob__getbuf(out, blob); + +done: + git_blob_free(blob); + return error; +} + +int git_reader_for_index( + git_reader **out, + git_repository *repo, + git_index *index) +{ + index_reader *reader; + int error; + + assert(out && repo); + + reader = git__calloc(1, sizeof(index_reader)); + GITERR_CHECK_ALLOC(reader); + + reader->reader.read = index_reader_read; + reader->repo = repo; + + if (index) { + reader->index = index; + } else if ((error = git_repository_index__weakptr(&reader->index, repo)) < 0) { + git__free(reader); + return error; + } + + *out = (git_reader *)reader; + return 0; +} + +/* generic */ + +int git_reader_read( + git_buf *out, + git_oid *out_id, + git_filemode_t *out_filemode, + git_reader *reader, + const char *filename) +{ + assert(out && reader && filename); + + return reader->read(out, out_id, out_filemode, reader, filename); +} + +void git_reader_free(git_reader *reader) +{ + if (!reader) + return; + + git__free(reader); +} diff --git a/src/reader.h b/src/reader.h new file mode 100644 index 000000000..18a6a1103 --- /dev/null +++ b/src/reader.h @@ -0,0 +1,107 @@ +/* + * 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_reader_h__ +#define INCLUDE_reader_h__ + +#include "common.h" + +/* Returned when the workdir does not match the index */ +#define GIT_READER_MISMATCH 1 + +typedef struct git_reader git_reader; + +/* + * The `git_reader` structure is a generic interface for reading the + * contents of a file by its name, and implementations are provided + * for reading out of a tree, the index, and the working directory. + * + * Note that the reader implementation is meant to have a short + * lifecycle and does not increase the refcount of the object that + * it's reading. Callers should ensure that they do not use a + * reader after disposing the underlying object that it reads. + */ +struct git_reader { + int (*read)(git_buf *out, git_oid *out_oid, git_filemode_t *mode, git_reader *reader, const char *filename); +}; + +/** + * Create a `git_reader` that will allow random access to the given + * tree. Paths requested via `git_reader_read` will be rooted at this + * tree, callers are not expected to recurse through tree lookups. Thus, + * you can request to read `/src/foo.c` and the tree provided to this + * function will be searched to find another tree named `src`, which + * will then be opened to find `foo.c`. + * + * @param out The reader for the given tree + * @param tree The tree object to read + * @return 0 on success, or an error code < 0 + */ +extern int git_reader_for_tree( + git_reader **out, + git_tree *tree); + +/** + * Create a `git_reader` that will allow random access to the given + * index, or the repository's index. + * + * @param out The reader for the given index + * @param repo The repository containing the index + * @param index The index to read, or NULL to use the repository's index + * @return 0 on success, or an error code < 0 + */ +extern int git_reader_for_index( + git_reader **out, + git_repository *repo, + git_index *index); + +/** + * Create a `git_reader` that will allow random access to the given + * repository's working directory. Note that the contents are read + * in repository format, meaning any workdir -> odb filters are + * applied. + * + * If `validate_index` is set to true, reads of files will hash the + * on-disk contents and ensure that the resulting object ID matches + * the repository's index. This ensures that the working directory + * is unmodified from the index contents. + * + * @param out The reader for the given working directory + * @param repo The repository containing the working directory + * @param validate_index If true, the working directory contents will + * be compared to the index contents during read to ensure that + * the working directory is unmodified. + * @return 0 on success, or an error code < 0 + */ +extern int git_reader_for_workdir( + git_reader **out, + git_repository *repo, + bool validate_index); + +/** + * Read the given filename from the reader and populate the given buffer + * with the contents and the given oid with the object ID. + * + * @param out The buffer to populate with the file contents + * @param out_id The oid to populate with the object ID + * @param reader The reader to read + * @param filename The filename to read from the reader + */ +extern int git_reader_read( + git_buf *out, + git_oid *out_id, + git_filemode_t *out_filemode, + git_reader *reader, + const char *filename); + +/** + * Free the given reader and any associated objects. + * + * @param reader The reader to free + */ +extern void git_reader_free(git_reader *reader); + +#endif diff --git a/tests/apply/apply_helpers.h b/tests/apply/apply_helpers.h new file mode 100644 index 000000000..81563351e --- /dev/null +++ b/tests/apply/apply_helpers.h @@ -0,0 +1,587 @@ +#include "../merge/merge_helpers.h" + +#define TEST_REPO_PATH "merge-recursive" + +#define DIFF_MODIFY_TWO_FILES \ + "diff --git a/asparagus.txt b/asparagus.txt\n" \ + "index f516580..ffb36e5 100644\n" \ + "--- a/asparagus.txt\n" \ + "+++ b/asparagus.txt\n" \ + "@@ -1 +1 @@\n" \ + "-ASPARAGUS SOUP!\n" \ + "+ASPARAGUS SOUP.\n" \ + "diff --git a/veal.txt b/veal.txt\n" \ + "index 94d2c01..a7b0665 100644\n" \ + "--- a/veal.txt\n" \ + "+++ b/veal.txt\n" \ + "@@ -1 +1 @@\n" \ + "-VEAL SOUP!\n" \ + "+VEAL SOUP.\n" \ + "@@ -7 +7 @@ occasionally, then put into it a shin of veal, let it boil two hours\n" \ + "-longer. take out the slices of ham, and skim off the grease if any\n" \ + "+longer; take out the slices of ham, and skim off the grease if any\n" + +#define DIFF_DELETE_FILE \ + "diff --git a/gravy.txt b/gravy.txt\n" \ + "deleted file mode 100644\n" \ + "index c4e6cca..0000000\n" \ + "--- a/gravy.txt\n" \ + "+++ /dev/null\n" \ + "@@ -1,8 +0,0 @@\n" \ + "-GRAVY SOUP.\n" \ + "-\n" \ + "-Get eight pounds of coarse lean beef--wash it clean and lay it in your\n" \ + "-pot, put in the same ingredients as for the shin soup, with the same\n" \ + "-quantity of water, and follow the process directed for that. Strain the\n" \ + "-soup through a sieve, and serve it up clear, with nothing more than\n" \ + "-toasted bread in it; two table-spoonsful of mushroom catsup will add a\n" \ + "-fine flavour to the soup.\n" + +#define DIFF_ADD_FILE \ + "diff --git a/newfile.txt b/newfile.txt\n" \ + "new file mode 100644\n" \ + "index 0000000..6370543\n" \ + "--- /dev/null\n" \ + "+++ b/newfile.txt\n" \ + "@@ -0,0 +1,2 @@\n" \ + "+This is a new file!\n" \ + "+Added by a patch.\n" + +#define DIFF_EXECUTABLE_FILE \ + "diff --git a/beef.txt b/beef.txt\n" \ + "old mode 100644\n" \ + "new mode 100755\n" + +#define DIFF_MANY_CHANGES_ONE \ + "diff --git a/veal.txt b/veal.txt\n" \ + "index 94d2c01..c9d7d5d 100644\n" \ + "--- a/veal.txt\n" \ + "+++ b/veal.txt\n" \ + "@@ -1,2 +1,2 @@\n" \ + "-VEAL SOUP!\n" \ + "+VEAL SOUP\n" \ + " \n" \ + "@@ -4,3 +4,2 @@\n" \ + " spoonful of black pepper pounded, and two of salt, with two or three\n" \ + "-slices of lean ham; let it boil steadily two hours; skim it\n" \ + " occasionally, then put into it a shin of veal, let it boil two hours\n" \ + "@@ -8,3 +7,3 @@\n" \ + " should rise, take a gill of good cream, mix with it two table-spoonsful\n" \ + "-of flour very nicely, and the yelks of two eggs beaten well, strain this\n" \ + "+OF FLOUR very nicely, and the yelks of two eggs beaten well, strain this\n" \ + " mixture, and add some chopped parsley; pour some soup on by degrees,\n" \ + "@@ -12,2 +11,3 @@\n" \ + " boiled two or three minutes to take off the raw taste of the eggs. If\n" \ + "+Inserted line.\n" \ + " the cream be not perfectly sweet, and the eggs quite new, the thickening\n" \ + "@@ -15,3 +15,3 @@\n" \ + " in, first taking off their skins, by letting them stand a few minutes in\n" \ + "-hot water, when they may be easily peeled. When made in this way you\n" \ + "+Changed line.\n" \ + " must thicken it with the flour only. Any part of the veal may be used,\n" + +#define DIFF_MANY_CHANGES_TWO \ + "diff --git a/veal.txt b/veal.txt\n" \ + "index 94d2c01..6b943d6 100644\n" \ + "--- a/veal.txt\n" \ + "+++ b/veal.txt\n" \ + "@@ -1,2 +1,2 @@\n" \ + "-VEAL SOUP!\n" \ + "+VEAL SOUP!!!\n" \ + " \n" \ + "@@ -4,3 +4,2 @@\n" \ + " spoonful of black pepper pounded, and two of salt, with two or three\n" \ + "-slices of lean ham; let it boil steadily two hours; skim it\n" \ + " occasionally, then put into it a shin of veal, let it boil two hours\n" \ + "@@ -8,3 +7,3 @@\n" \ + " should rise, take a gill of good cream, mix with it two table-spoonsful\n" \ + "-of flour very nicely, and the yelks of two eggs beaten well, strain this\n" \ + "+of flour very nicely, AND the yelks of two eggs beaten well, strain this\n" \ + " mixture, and add some chopped parsley; pour some soup on by degrees,\n" \ + "@@ -12,2 +11,3 @@\n" \ + " boiled two or three minutes to take off the raw taste of the eggs. If\n" \ + "+New line.\n" \ + " the cream be not perfectly sweet, and the eggs quite new, the thickening\n" \ + "@@ -15,4 +15,5 @@\n" \ + " in, first taking off their skins, by letting them stand a few minutes in\n" \ + "-hot water, when they may be easily peeled. When made in this way you\n" \ + "-must thicken it with the flour only. Any part of the veal may be used,\n" \ + "-but the shin or knuckle is the nicest.\n" \ + "+HOT water, when they may be easily peeled. When made in this way you\n" \ + "+must THICKEN it with the flour only. Any part of the veal may be used,\n" \ + "+but the shin OR knuckle is the nicest.\n" \ + "+Another new line.\n" \ + +#define DIFF_RENAME_FILE \ + "diff --git a/beef.txt b/notbeef.txt\n" \ + "similarity index 100%\n" \ + "rename from beef.txt\n" \ + "rename to notbeef.txt\n" + +#define DIFF_RENAME_AND_MODIFY_FILE \ + "diff --git a/beef.txt b/notbeef.txt\n" \ + "similarity index 97%\n" \ + "rename from beef.txt\n" \ + "rename to notbeef.txt\n" \ + "index 68f6182..6fa1014 100644\n" \ + "--- a/beef.txt\n" \ + "+++ b/notbeef.txt\n" \ + "@@ -1,4 +1,4 @@\n" \ + "-BEEF SOUP.\n" \ + "+THIS IS NOT BEEF SOUP, IT HAS A NEW NAME.\n" \ + "\n" \ + " Take the hind shin of beef, cut off all the flesh off the leg-bone,\n" \ + " which must be taken away entirely, or the soup will be greasy. Wash the\n" + +#define DIFF_RENAME_A_TO_B_TO_C \ + "diff --git a/asparagus.txt b/asparagus.txt\n" \ + "deleted file mode 100644\n" \ + "index f516580..0000000\n" \ + "--- a/asparagus.txt\n" \ + "+++ /dev/null\n" \ + "@@ -1,10 +0,0 @@\n" \ + "-ASPARAGUS SOUP!\n" \ + "-\n" \ + "-Take four large bunches of asparagus, scrape it nicely, cut off one inch\n" \ + "-of the tops, and lay them in water, chop the stalks and put them on the\n" \ + "-fire with a piece of bacon, a large onion cut up, and pepper and salt;\n" \ + "-add two quarts of water, boil them till the stalks are quite soft, then\n" \ + "-pulp them through a sieve, and strain the water to it, which must be put\n" \ + "-back in the pot; put into it a chicken cut up, with the tops of\n" \ + "-asparagus which had been laid by, boil it until these last articles are\n" \ + "-sufficiently done, thicken with flour, butter and milk, and serve it up.\n" \ + "diff --git a/beef.txt b/beef.txt\n" \ + "index 68f6182..f516580 100644\n" \ + "--- a/beef.txt\n" \ + "+++ b/beef.txt\n" \ + "@@ -1,22 +1,10 @@\n" \ + "-BEEF SOUP.\n" \ + "+ASPARAGUS SOUP!\n" \ + "\n" \ + "-Take the hind shin of beef, cut off all the flesh off the leg-bone,\n" \ + "-which must be taken away entirely, or the soup will be greasy. Wash the\n" \ + "-meat clean and lay it in a pot, sprinkle over it one small\n" \ + "-table-spoonful of pounded black pepper, and two of salt; three onions\n" \ + "-the size of a hen's egg, cut small, six small carrots scraped and cut\n" \ + "-up, two small turnips pared and cut into dice; pour on three quarts of\n" \ + "-water, cover the pot close, and keep it gently and steadily boiling five\n" \ + "-hours, which will leave about three pints of clear soup; do not let the\n" \ + "-pot boil over, but take off the scum carefully, as it rises. When it has\n" \ + "-boiled four hours, put in a small bundle of thyme and parsley, and a\n" \ + "-pint of celery cut small, or a tea-spoonful of celery seed pounded.\n" \ + "-These latter ingredients would lose their delicate flavour if boiled too\n" \ + "-much. Just before you take it up, brown it in the following manner: put\n" \ + "-a small table-spoonful of nice brown sugar into an iron skillet, set it\n" \ + "-on the fire and stir it till it melts and looks very dark, pour into it\n" \ + "-a ladle full of the soup, a little at a time; stirring it all the while.\n" \ + "-Strain this browning and mix it well with the soup; take out the bundle\n" \ + "-of thyme and parsley, put the nicest pieces of meat in your tureen, and\n" \ + "-pour on the soup and vegetables; put in some toasted bread cut in dice,\n" \ + "-and serve it up.\n" \ + "+Take four large bunches of asparagus, scrape it nicely, cut off one inch\n" \ + "+of the tops, and lay them in water, chop the stalks and put them on the\n" \ + "+fire with a piece of bacon, a large onion cut up, and pepper and salt;\n" \ + "+add two quarts of water, boil them till the stalks are quite soft, then\n" \ + "+pulp them through a sieve, and strain the water to it, which must be put\n" \ + "+back in the pot; put into it a chicken cut up, with the tops of\n" \ + "+asparagus which had been laid by, boil it until these last articles are\n" \ + "+sufficiently done, thicken with flour, butter and milk, and serve it up.\n" \ + "diff --git a/notbeef.txt b/notbeef.txt\n" \ + "new file mode 100644\n" \ + "index 0000000..68f6182\n" \ + "--- /dev/null\n" \ + "+++ b/notbeef.txt\n" \ + "@@ -0,0 +1,22 @@\n" \ + "+BEEF SOUP.\n" \ + "+\n" \ + "+Take the hind shin of beef, cut off all the flesh off the leg-bone,\n" \ + "+which must be taken away entirely, or the soup will be greasy. Wash the\n" \ + "+meat clean and lay it in a pot, sprinkle over it one small\n" \ + "+table-spoonful of pounded black pepper, and two of salt; three onions\n" \ + "+the size of a hen's egg, cut small, six small carrots scraped and cut\n" \ + "+up, two small turnips pared and cut into dice; pour on three quarts of\n" \ + "+water, cover the pot close, and keep it gently and steadily boiling five\n" \ + "+hours, which will leave about three pints of clear soup; do not let the\n" \ + "+pot boil over, but take off the scum carefully, as it rises. When it has\n" \ + "+boiled four hours, put in a small bundle of thyme and parsley, and a\n" \ + "+pint of celery cut small, or a tea-spoonful of celery seed pounded.\n" \ + "+These latter ingredients would lose their delicate flavour if boiled too\n" \ + "+much. Just before you take it up, brown it in the following manner: put\n" \ + "+a small table-spoonful of nice brown sugar into an iron skillet, set it\n" \ + "+on the fire and stir it till it melts and looks very dark, pour into it\n" \ + "+a ladle full of the soup, a little at a time; stirring it all the while.\n" \ + "+Strain this browning and mix it well with the soup; take out the bundle\n" \ + "+of thyme and parsley, put the nicest pieces of meat in your tureen, and\n" \ + "+pour on the soup and vegetables; put in some toasted bread cut in dice,\n" \ + "+and serve it up.\n" + +#define DIFF_RENAME_A_TO_B_TO_C_EXACT \ + "diff --git a/asparagus.txt b/beef.txt\n" \ + "similarity index 100%\n" \ + "rename from asparagus.txt\n" \ + "rename to beef.txt\n" \ + "diff --git a/beef.txt b/notbeef.txt\n" \ + "similarity index 100%\n" \ + "rename from beef.txt\n" \ + "rename to notbeef.txt\n" + +#define DIFF_RENAME_CIRCULAR \ + "diff --git a/asparagus.txt b/beef.txt\n" \ + "similarity index 100%\n" \ + "rename from asparagus.txt\n" \ + "rename to beef.txt\n" \ + "diff --git a/beef.txt b/notbeef.txt\n" \ + "similarity index 100%\n" \ + "rename from beef.txt\n" \ + "rename to asparagus.txt\n" + +#define DIFF_RENAME_2_TO_1 \ + "diff --git a/asparagus.txt b/2.txt\n" \ + "similarity index 100%\n" \ + "rename from asparagus.txt\n" \ + "rename to 2.txt\n" \ + "diff --git a/beef.txt b/2.txt\n" \ + "similarity index 100%\n" \ + "rename from beef.txt\n" \ + "rename to 2.txt\n" + +#define DIFF_RENAME_1_TO_2 \ + "diff --git a/asparagus.txt b/2.txt\n" \ + "similarity index 100%\n" \ + "rename from asparagus.txt\n" \ + "rename to 1.txt\n" \ + "diff --git a/asparagus.txt b/2.txt\n" \ + "similarity index 100%\n" \ + "rename from asparagus.txt\n" \ + "rename to 2.txt\n" + +#define DIFF_TWO_DELTAS_ONE_FILE \ + "diff --git a/beef.txt b/beef.txt\n" \ + "index 68f6182..235069d 100644\n" \ + "--- a/beef.txt\n" \ + "+++ b/beef.txt\n" \ + "@@ -1,4 +1,4 @@\n" \ + "-BEEF SOUP.\n" \ + "+BEEF SOUP!\n" \ + "\n" \ + " Take the hind shin of beef, cut off all the flesh off the leg-bone,\n" \ + " which must be taken away entirely, or the soup will be greasy. Wash the\n" \ + "diff --git a/beef.txt b/beef.txt\n" \ + "index 68f6182..e059eb5 100644\n" \ + "--- a/beef.txt\n" \ + "+++ b/beef.txt\n" \ + "@@ -19,4 +19,4 @@ a ladle full of the soup, a little at a time; stirring it all the while.\n" \ + " Strain this browning and mix it well with the soup; take out the bundle\n" \ + " of thyme and parsley, put the nicest pieces of meat in your tureen, and\n" \ + " pour on the soup and vegetables; put in some toasted bread cut in dice,\n" \ + "-and serve it up.\n" \ + "+and serve it up!\n" + +#define DIFF_TWO_DELTAS_ONE_NEW_FILE \ + "diff --git a/newfile.txt b/newfile.txt\n" \ + "new file mode 100644\n" \ + "index 0000000..6434b13\n" \ + "--- /dev/null\n" \ + "+++ b/newfile.txt\n" \ + "@@ -0,0 +1 @@\n" \ + "+This is a new file.\n" \ + "diff --git a/newfile.txt b/newfile.txt\n" \ + "index 6434b13..08d4c44 100644\n" \ + "--- a/newfile.txt\n" \ + "+++ b/newfile.txt\n" \ + "@@ -1 +1,3 @@\n" \ + " This is a new file.\n" \ + "+\n" \ + "+This is another change to a new file.\n" + +#define DIFF_RENAME_AND_MODIFY_DELTAS \ + "diff --git a/veal.txt b/asdf.txt\n" \ + "similarity index 96%\n" \ + "rename from veal.txt\n" \ + "rename to asdf.txt\n" \ + "index 94d2c01..292cb60 100644\n" \ + "--- a/veal.txt\n" \ + "+++ b/asdf.txt\n" \ + "@@ -15,4 +15,4 @@ will curdle in the soup. For a change you may put a dozen ripe tomatos\n" \ + " in, first taking off their skins, by letting them stand a few minutes in\n" \ + " hot water, when they may be easily peeled. When made in this way you\n" \ + " must thicken it with the flour only. Any part of the veal may be used,\n" \ + "-but the shin or knuckle is the nicest.\n" \ + "+but the shin or knuckle is the nicest!\n" \ + "diff --git a/asdf.txt b/asdf.txt\n" \ + "index 292cb60..61c686b 100644\n" \ + "--- a/asdf.txt\n" \ + "+++ b/asdf.txt\n" \ + "@@ -1,4 +1,4 @@\n" \ + "-VEAL SOUP!\n" \ + "+VEAL SOUP\n" \ + "\n" \ + " Put into a pot three quarts of water, three onions cut small, one\n" \ + " spoonful of black pepper pounded, and two of salt, with two or three\n" + +#define DIFF_RENAME_AFTER_MODIFY \ + "diff --git a/veal.txt b/veal.txt\n" \ + "index 292cb60..61c686b 100644\n" \ + "--- a/veal.txt\n" \ + "+++ b/veal.txt\n" \ + "@@ -1,4 +1,4 @@\n" \ + "-VEAL SOUP!\n" \ + "+VEAL SOUP\n" \ + "\n" \ + " Put into a pot three quarts of water, three onions cut small, one\n" \ + " spoonful of black pepper pounded, and two of salt, with two or three\n" \ + "diff --git a/veal.txt b/other.txt\n" \ + "similarity index 96%\n" \ + "rename from veal.txt\n" \ + "rename to other.txt\n" \ + "index 94d2c01..292cb60 100644\n" \ + "--- a/veal.txt\n" \ + "+++ b/other.txt\n" \ + "@@ -15,4 +15,4 @@ will curdle in the soup. For a change you may put a dozen ripe tomatos\n" \ + " in, first taking off their skins, by letting them stand a few minutes in\n" \ + " hot water, when they may be easily peeled. When made in this way you\n" \ + " must thicken it with the flour only. Any part of the veal may be used,\n" \ + "-but the shin or knuckle is the nicest.\n" \ + "+but the shin or knuckle is the nicest!\n" + +#define DIFF_RENAME_AFTER_MODIFY_TARGET_PATH \ + "diff --git a/beef.txt b/beef.txt\n" \ + "index 292cb60..61c686b 100644\n" \ + "--- a/beef.txt\n" \ + "+++ b/beef.txt\n" \ + "@@ -1,4 +1,4 @@\n" \ + "-VEAL SOUP!\n" \ + "+VEAL SOUP\n" \ + "\n" \ + " Put into a pot three quarts of water, three onions cut small, one\n" \ + " spoonful of black pepper pounded, and two of salt, with two or three\n" \ + "diff --git a/veal.txt b/beef.txt\n" \ + "similarity index 96%\n" \ + "rename from veal.txt\n" \ + "rename to beef.txt\n" \ + "index 94d2c01..292cb60 100644\n" \ + "--- a/veal.txt\n" \ + "+++ b/beef.txt\n" \ + "@@ -15,4 +15,4 @@ will curdle in the soup. For a change you may put a dozen ripe tomatos\n" \ + " in, first taking off their skins, by letting them stand a few minutes in\n" \ + " hot water, when they may be easily peeled. When made in this way you\n" \ + " must thicken it with the flour only. Any part of the veal may be used,\n" \ + "-but the shin or knuckle is the nicest.\n" \ + "+but the shin or knuckle is the nicest!\n" + +#define DIFF_RENAME_AND_MODIFY_SOURCE_PATH \ + "diff --git a/veal.txt b/asdf.txt\n" \ + "similarity index 96%\n" \ + "rename from veal.txt\n" \ + "rename to asdf.txt\n" \ + "index 94d2c01..292cb60 100644\n" \ + "--- a/veal.txt\n" \ + "+++ b/asdf.txt\n" \ + "@@ -15,4 +15,4 @@ will curdle in the soup. For a change you may put a dozen ripe tomatos\n" \ + " in, first taking off their skins, by letting them stand a few minutes in\n" \ + " hot water, when they may be easily peeled. When made in this way you\n" \ + " must thicken it with the flour only. Any part of the veal may be used,\n" \ + "-but the shin or knuckle is the nicest.\n" \ + "+but the shin or knuckle is the nicest!\n" \ + "diff --git a/veal.txt b/veal.txt\n" \ + "index 292cb60..61c686b 100644\n" \ + "--- a/veal.txt\n" \ + "+++ b/veal.txt\n" \ + "@@ -1,4 +1,4 @@\n" \ + "-VEAL SOUP!\n" \ + "+VEAL SOUP\n" \ + "\n" \ + " Put into a pot three quarts of water, three onions cut small, one\n" \ + " spoonful of black pepper pounded, and two of salt, with two or three\n" + +#define DIFF_DELETE_AND_READD_FILE \ + "diff --git a/asparagus.txt b/asparagus.txt\n" \ + "deleted file mode 100644\n" \ + "index f516580..0000000\n" \ + "--- a/asparagus.txt\n" \ + "+++ /dev/null\n" \ + "@@ -1,10 +0,0 @@\n" \ + "-ASPARAGUS SOUP!\n" \ + "-\n" \ + "-Take four large bunches of asparagus, scrape it nicely, cut off one inch\n" \ + "-of the tops, and lay them in water, chop the stalks and put them on the\n" \ + "-fire with a piece of bacon, a large onion cut up, and pepper and salt;\n" \ + "-add two quarts of water, boil them till the stalks are quite soft, then\n" \ + "-pulp them through a sieve, and strain the water to it, which must be put\n" \ + "-back in the pot; put into it a chicken cut up, with the tops of\n" \ + "-asparagus which had been laid by, boil it until these last articles are\n" \ + "-sufficiently done, thicken with flour, butter and milk, and serve it up.\n" \ + "diff --git a/asparagus.txt b/asparagus.txt\n" \ + "new file mode 100644\n" \ + "index 0000000..2dc7f8b\n" \ + "--- /dev/null\n" \ + "+++ b/asparagus.txt\n" \ + "@@ -0,0 +1 @@\n" \ + "+New file.\n" \ + +#define DIFF_REMOVE_FILE_TWICE \ + "diff --git a/asparagus.txt b/asparagus.txt\n" \ + "deleted file mode 100644\n" \ + "index f516580..0000000\n" \ + "--- a/asparagus.txt\n" \ + "+++ /dev/null\n" \ + "@@ -1,10 +0,0 @@\n" \ + "-ASPARAGUS SOUP!\n" \ + "-\n" \ + "-Take four large bunches of asparagus, scrape it nicely, cut off one inch\n" \ + "-of the tops, and lay them in water, chop the stalks and put them on the\n" \ + "-fire with a piece of bacon, a large onion cut up, and pepper and salt;\n" \ + "-add two quarts of water, boil them till the stalks are quite soft, then\n" \ + "-pulp them through a sieve, and strain the water to it, which must be put\n" \ + "-back in the pot; put into it a chicken cut up, with the tops of\n" \ + "-asparagus which had been laid by, boil it until these last articles are\n" \ + "-sufficiently done, thicken with flour, butter and milk, and serve it up.\n" \ + "diff --git a/asparagus.txt b/asparagus.txt\n" \ + "deleted file mode 100644\n" \ + "index f516580..0000000\n" \ + "--- a/asparagus.txt\n" \ + "+++ /dev/null\n" \ + "@@ -1,10 +0,0 @@\n" \ + "-ASPARAGUS SOUP!\n" \ + "-\n" \ + "-Take four large bunches of asparagus, scrape it nicely, cut off one inch\n" \ + "-of the tops, and lay them in water, chop the stalks and put them on the\n" \ + "-fire with a piece of bacon, a large onion cut up, and pepper and salt;\n" \ + "-add two quarts of water, boil them till the stalks are quite soft, then\n" \ + "-pulp them through a sieve, and strain the water to it, which must be put\n" \ + "-back in the pot; put into it a chicken cut up, with the tops of\n" \ + "-asparagus which had been laid by, boil it until these last articles are\n" \ + "-sufficiently done, thicken with flour, butter and milk, and serve it up.\n" + +struct iterator_compare_data { + struct merge_index_entry *expected; + size_t cnt; + size_t idx; +}; + +static int iterator_compare(const git_index_entry *entry, void *_data) +{ + git_oid expected_id; + + struct iterator_compare_data *data = (struct iterator_compare_data *)_data; + + cl_assert_equal_i(GIT_IDXENTRY_STAGE(entry), data->expected[data->idx].stage); + cl_git_pass(git_oid_fromstr(&expected_id, data->expected[data->idx].oid_str)); + cl_assert_equal_oid(&entry->id, &expected_id); + cl_assert_equal_i(entry->mode, data->expected[data->idx].mode); + cl_assert_equal_s(entry->path, data->expected[data->idx].path); + + if (data->idx >= data->cnt) + return -1; + + data->idx++; + + return 0; +} + +static void validate_apply_workdir( + git_repository *repo, + struct merge_index_entry *workdir_entries, + size_t workdir_cnt) +{ + git_index *index; + git_iterator *iterator; + git_iterator_options opts = GIT_ITERATOR_OPTIONS_INIT; + struct iterator_compare_data data = { workdir_entries, workdir_cnt }; + + opts.flags |= GIT_ITERATOR_INCLUDE_HASH; + + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_iterator_for_workdir(&iterator, repo, index, NULL, &opts)); + + cl_git_pass(git_iterator_foreach(iterator, iterator_compare, &data)); + cl_assert_equal_i(data.idx, data.cnt); + + git_iterator_free(iterator); + git_index_free(index); +} + +static void validate_apply_index( + git_repository *repo, + struct merge_index_entry *index_entries, + size_t index_cnt) +{ + git_index *index; + git_iterator *iterator; + struct iterator_compare_data data = { index_entries, index_cnt }; + + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_iterator_for_index(&iterator, repo, index, NULL)); + + cl_git_pass(git_iterator_foreach(iterator, iterator_compare, &data)); + cl_assert_equal_i(data.idx, data.cnt); + + git_iterator_free(iterator); + git_index_free(index); +} + +static int iterator_eq(const git_index_entry **entry, void *_data) +{ + GIT_UNUSED(_data); + + if (!entry[0] || !entry[1]) + return -1; + + cl_assert_equal_i(GIT_IDXENTRY_STAGE(entry[0]), GIT_IDXENTRY_STAGE(entry[1])); + cl_assert_equal_oid(&entry[0]->id, &entry[1]->id); + cl_assert_equal_i(entry[0]->mode, entry[1]->mode); + cl_assert_equal_s(entry[0]->path, entry[1]->path); + + return 0; +} + +static void validate_index_unchanged(git_repository *repo) +{ + git_tree *head; + git_index *index; + git_iterator *head_iterator, *index_iterator, *iterators[2]; + + cl_git_pass(git_repository_head_tree(&head, repo)); + cl_git_pass(git_repository_index(&index, repo)); + + cl_git_pass(git_iterator_for_tree(&head_iterator, head, NULL)); + cl_git_pass(git_iterator_for_index(&index_iterator, repo, index, NULL)); + + iterators[0] = head_iterator; + iterators[1] = index_iterator; + + cl_git_pass(git_iterator_walk(iterators, 2, iterator_eq, NULL)); + + git_iterator_free(head_iterator); + git_iterator_free(index_iterator); + + git_tree_free(head); + git_index_free(index); +} + +static void validate_workdir_unchanged(git_repository *repo) +{ + git_tree *head; + git_index *index; + git_iterator *head_iterator, *workdir_iterator, *iterators[2]; + git_iterator_options workdir_opts = GIT_ITERATOR_OPTIONS_INIT; + + cl_git_pass(git_repository_head_tree(&head, repo)); + cl_git_pass(git_repository_index(&index, repo)); + + workdir_opts.flags |= GIT_ITERATOR_INCLUDE_HASH; + + cl_git_pass(git_iterator_for_tree(&head_iterator, head, NULL)); + cl_git_pass(git_iterator_for_workdir(&workdir_iterator, repo, index, NULL, &workdir_opts)); + + iterators[0] = head_iterator; + iterators[1] = workdir_iterator; + + cl_git_pass(git_iterator_walk(iterators, 2, iterator_eq, NULL)); + + git_iterator_free(head_iterator); + git_iterator_free(workdir_iterator); + + git_tree_free(head); + git_index_free(index); +} diff --git a/tests/apply/both.c b/tests/apply/both.c new file mode 100644 index 000000000..5091b8cfd --- /dev/null +++ b/tests/apply/both.c @@ -0,0 +1,736 @@ +#include "clar_libgit2.h" +#include "apply_helpers.h" + +static git_repository *repo; + +#define TEST_REPO_PATH "merge-recursive" + +void test_apply_both__initialize(void) +{ + git_oid oid; + git_commit *commit; + + repo = cl_git_sandbox_init(TEST_REPO_PATH); + + git_oid_fromstr(&oid, "539bd011c4822c560c1d17cab095006b7a10f707"); + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_reset(repo, (git_object *)commit, GIT_RESET_HARD, NULL)); + git_commit_free(commit); +} + +void test_apply_both__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_apply_both__generated_diff(void) +{ + git_oid a_oid, b_oid; + git_commit *a_commit, *b_commit; + git_tree *a_tree, *b_tree; + git_diff *diff; + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + + struct merge_index_entry both_expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + git_oid_fromstr(&a_oid, "539bd011c4822c560c1d17cab095006b7a10f707"); + git_oid_fromstr(&b_oid, "7c7bf85e978f1d18c0566f702d2cb7766b9c8d4f"); + cl_git_pass(git_commit_lookup(&a_commit, repo, &a_oid)); + cl_git_pass(git_commit_lookup(&b_commit, repo, &b_oid)); + + cl_git_pass(git_commit_tree(&a_tree, a_commit)); + cl_git_pass(git_commit_tree(&b_tree, b_commit)); + + cl_git_pass(git_diff_tree_to_tree(&diff, repo, a_tree, b_tree, &diff_opts)); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); + git_tree_free(a_tree); + git_tree_free(b_tree); + git_commit_free(a_commit); + git_commit_free(b_commit); +} + +void test_apply_both__parsed_diff(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, + DIFF_MODIFY_TWO_FILES, strlen(DIFF_MODIFY_TWO_FILES))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__removes_file(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" }, + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_DELETE_FILE, + strlen(DIFF_DELETE_FILE))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__adds_file(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "6370543fcfedb3e6516ec53b06158f3687dc1447", 0, "newfile.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" }, + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, + DIFF_ADD_FILE, strlen(DIFF_ADD_FILE))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__application_failure_leaves_index_unmodified(void) +{ + git_diff *diff; + git_index *index; + + const char *diff_file = DIFF_MODIFY_TWO_FILES; + + struct merge_index_entry index_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + }; + size_t index_expected_cnt = sizeof(index_expected) / + sizeof(struct merge_index_entry); + + /* mutate the index */ + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_index_remove(index, "veal.txt", 0)); + cl_git_pass(git_index_write(index)); + git_index_free(index); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_fail_with(GIT_EAPPLYFAIL, git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, index_expected, index_expected_cnt); + validate_workdir_unchanged(repo); + + git_diff_free(diff); +} + +void test_apply_both__index_must_match_workdir(void) +{ + git_diff *diff; + git_index *index; + git_index_entry idx_entry; + + const char *diff_file = DIFF_MODIFY_TWO_FILES; + + /* + * Append a line to the end of the file in both the index and the + * working directory. Although the appended line would allow for + * patch application in each, the line appended is different in + * each, so the application should not be allowed. + */ + cl_git_append2file("merge-recursive/asparagus.txt", + "This is a modification.\n"); + + cl_git_pass(git_repository_index(&index, repo)); + + memset(&idx_entry, 0, sizeof(git_index_entry)); + idx_entry.mode = 0100644; + idx_entry.path = "asparagus.txt"; + cl_git_pass(git_oid_fromstr(&idx_entry.id, "06d3fefb8726ab1099acc76e02dfb85e034b2538")); + cl_git_pass(git_index_add(index, &idx_entry)); + + cl_git_pass(git_index_write(index)); + git_index_free(index); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_fail_with(GIT_EAPPLYFAIL, git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + git_diff_free(diff); +} + +void test_apply_both__index_mode_must_match_workdir(void) +{ + git_diff *diff; + + if (!cl_is_chmod_supported()) + clar__skip(); + + /* Set a file in the working directory executable. */ + cl_must_pass(p_chmod("merge-recursive/asparagus.txt", 0755)); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_MODIFY_TWO_FILES, + strlen(DIFF_MODIFY_TWO_FILES))); + cl_git_fail_with(GIT_EAPPLYFAIL, git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + git_diff_free(diff); +} + +void test_apply_both__application_failure_leaves_workdir_unmodified(void) +{ + git_diff *diff; + git_index *index; + + const char *diff_file = DIFF_MODIFY_TWO_FILES; + + struct merge_index_entry workdir_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "8684724651336001c5dbce74bed6736d2443958d", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + /* mutate the workdir */ + cl_git_rewritefile("merge-recursive/veal.txt", + "This is a modification.\n"); + + cl_git_pass(git_repository_index(&index, repo)); + git_index_add_bypath(index, "veal.txt"); + cl_git_pass(git_index_write(index)); + git_index_free(index); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_fail_with(GIT_EAPPLYFAIL, git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__keeps_nonconflicting_changes(void) +{ + git_diff *diff; + git_index *index; + git_index_entry idx_entry; + + const char *diff_file = DIFF_MODIFY_TWO_FILES; + + struct merge_index_entry index_expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "898d12687fb35be271c27c795a6b32c8b51da79e", 0, "beef.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t index_expected_cnt = sizeof(index_expected) / + sizeof(struct merge_index_entry); + + struct merge_index_entry workdir_expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "f75ba05f340c51065cbea2e1fdbfe5fe13144c97", 0, "gravy.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + /* mutate the index */ + cl_git_pass(git_repository_index(&index, repo)); + + memset(&idx_entry, 0, sizeof(git_index_entry)); + idx_entry.mode = 0100644; + idx_entry.path = "beef.txt"; + cl_git_pass(git_oid_fromstr(&idx_entry.id, "898d12687fb35be271c27c795a6b32c8b51da79e")); + git_index_add(index, &idx_entry); + + cl_git_pass(git_index_remove(index, "bouilli.txt", 0)); + cl_git_pass(git_index_write(index)); + git_index_free(index); + + /* and mutate the working directory */ + cl_git_rmfile("merge-recursive/oyster.txt"); + cl_git_rewritefile("merge-recursive/gravy.txt", "Hello, world.\n"); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, index_expected, index_expected_cnt); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__can_apply_nonconflicting_file_changes(void) +{ + git_diff *diff; + git_index *index; + + const char *diff_file = DIFF_MODIFY_TWO_FILES; + + struct merge_index_entry both_expected[] = { + { 0100644, "f8a701c8a1a22c1729ee50faff1111f2d64f96fc", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + /* + * Replace the workdir file with a version that is different than + * HEAD but such that the patch still applies cleanly. This item + * has a new line appended. + */ + cl_git_append2file("merge-recursive/asparagus.txt", + "This line is added in the index and the workdir.\n"); + + cl_git_pass(git_repository_index(&index, repo)); + git_index_add_bypath(index, "asparagus.txt"); + cl_git_pass(git_index_write(index)); + git_index_free(index); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__honors_crlf_attributes(void) +{ + git_diff *diff; + git_oid oid; + git_commit *commit; + + const char *diff_file = DIFF_MODIFY_TWO_FILES; + + struct merge_index_entry index_expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t index_expected_cnt = sizeof(index_expected) / + sizeof(struct merge_index_entry); + + struct merge_index_entry workdir_expected[] = { + { 0100644, "176a458f94e0ea5272ce67c36bf30b6be9caf623", 0, ".gitattributes" }, + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + cl_git_mkfile("merge-recursive/.gitattributes", "* text=auto\n"); + + cl_git_rmfile("merge-recursive/asparagus.txt"); + cl_git_rmfile("merge-recursive/veal.txt"); + + git_oid_fromstr(&oid, "539bd011c4822c560c1d17cab095006b7a10f707"); + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_reset(repo, (git_object *)commit, GIT_RESET_HARD, NULL)); + git_commit_free(commit); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, index_expected, index_expected_cnt); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__rename(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "notbeef.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" }, + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_RENAME_FILE, + strlen(DIFF_RENAME_FILE))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__rename_and_modify(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "6fa10147f00fe1fab1d5e835529a9dad53db8552", 0, "notbeef.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" }, + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_RENAME_AND_MODIFY_FILE, + strlen(DIFF_RENAME_AND_MODIFY_FILE))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__rename_a_to_b_to_c(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "notbeef.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" }, + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_RENAME_A_TO_B_TO_C, + strlen(DIFF_RENAME_A_TO_B_TO_C))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__rename_a_to_b_to_c_exact(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "notbeef.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" }, + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_RENAME_A_TO_B_TO_C_EXACT, + strlen(DIFF_RENAME_A_TO_B_TO_C_EXACT))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__rename_circular(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "asparagus.txt" }, + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" } + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_RENAME_CIRCULAR, + strlen(DIFF_RENAME_CIRCULAR))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__rename_2_to_1(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "2.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" } + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_RENAME_2_TO_1, + strlen(DIFF_RENAME_2_TO_1))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__rename_1_to_2(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "1.txt" }, + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "2.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" } + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_RENAME_1_TO_2, + strlen(DIFF_RENAME_1_TO_2))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__two_deltas_one_file(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "0a9fd4415635e72573f0f6b5e68084cfe18f5075", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" } + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_TWO_DELTAS_ONE_FILE, + strlen(DIFF_TWO_DELTAS_ONE_FILE))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__two_deltas_one_new_file(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "08d4c445cf0078f3d9b604b82f32f4d87e083325", 0, "newfile.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" } + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_TWO_DELTAS_ONE_NEW_FILE, + strlen(DIFF_TWO_DELTAS_ONE_NEW_FILE))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__rename_and_modify_deltas(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "61c686bed39684eee8a2757ceb1291004a21333f", 0, "asdf.txt" }, + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_RENAME_AND_MODIFY_DELTAS, + strlen(DIFF_RENAME_AND_MODIFY_DELTAS))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__rename_delta_after_modify_delta(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "292cb60ce5e25c337c5b6e12957bbbfe1be4bf49", 0, "other.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "c8c120f466591bbe3b8867361d5ec3cdd9fda756", 0, "veal.txt" } + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_RENAME_AFTER_MODIFY, + strlen(DIFF_RENAME_AFTER_MODIFY))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__cant_rename_after_modify_nonexistent_target_path(void) +{ + git_diff *diff; + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_RENAME_AFTER_MODIFY_TARGET_PATH, + strlen(DIFF_RENAME_AFTER_MODIFY_TARGET_PATH))); + cl_git_fail(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + git_diff_free(diff); +} + +void test_apply_both__cant_modify_source_path_after_rename(void) +{ + git_diff *diff; + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_RENAME_AND_MODIFY_SOURCE_PATH, + strlen(DIFF_RENAME_AND_MODIFY_SOURCE_PATH))); + cl_git_fail(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + git_diff_free(diff); +} + +void test_apply_both__readd_deleted_file(void) +{ + git_diff *diff; + + struct merge_index_entry both_expected[] = { + { 0100644, "2dc7f8b24ba27f3888368bd180df03ff4c6c6fab", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" } + }; + size_t both_expected_cnt = sizeof(both_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_DELETE_AND_READD_FILE, + strlen(DIFF_DELETE_AND_READD_FILE))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + validate_apply_index(repo, both_expected, both_expected_cnt); + validate_apply_workdir(repo, both_expected, both_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_both__cant_remove_file_twice(void) +{ + git_diff *diff; + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_REMOVE_FILE_TWICE, + strlen(DIFF_REMOVE_FILE_TWICE))); + cl_git_fail(git_apply(repo, diff, GIT_APPLY_LOCATION_BOTH, NULL)); + + git_diff_free(diff); +} diff --git a/tests/apply/callbacks.c b/tests/apply/callbacks.c new file mode 100644 index 000000000..1b759dc9b --- /dev/null +++ b/tests/apply/callbacks.c @@ -0,0 +1,128 @@ +#include "clar_libgit2.h" +#include "apply_helpers.h" + +static git_repository *repo; + +#define TEST_REPO_PATH "merge-recursive" + +void test_apply_callbacks__initialize(void) +{ + git_oid oid; + git_commit *commit; + + repo = cl_git_sandbox_init(TEST_REPO_PATH); + + git_oid_fromstr(&oid, "539bd011c4822c560c1d17cab095006b7a10f707"); + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_reset(repo, (git_object *)commit, GIT_RESET_HARD, NULL)); + git_commit_free(commit); +} + +void test_apply_callbacks__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +static int delta_abort_cb(const git_diff_delta *delta, void *payload) +{ + GIT_UNUSED(payload); + + if (!strcmp(delta->old_file.path, "veal.txt")) + return -99; + + return 0; +} + +void test_apply_callbacks__delta_aborts(void) +{ + git_diff *diff; + git_apply_options opts = GIT_APPLY_OPTIONS_INIT; + + opts.delta_cb = delta_abort_cb; + + cl_git_pass(git_diff_from_buffer(&diff, + DIFF_MODIFY_TWO_FILES, strlen(DIFF_MODIFY_TWO_FILES))); + cl_git_fail_with(-99, + git_apply(repo, diff, GIT_APPLY_LOCATION_INDEX, &opts)); + + validate_index_unchanged(repo); + validate_workdir_unchanged(repo); + + git_diff_free(diff); +} + +static int delta_skip_cb(const git_diff_delta *delta, void *payload) +{ + GIT_UNUSED(payload); + + if (!strcmp(delta->old_file.path, "asparagus.txt")) + return 1; + + return 0; +} + +void test_apply_callbacks__delta_can_skip(void) +{ + git_diff *diff; + git_apply_options opts = GIT_APPLY_OPTIONS_INIT; + + struct merge_index_entry workdir_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + opts.delta_cb = delta_skip_cb; + + cl_git_pass(git_diff_from_buffer(&diff, + DIFF_MODIFY_TWO_FILES, strlen(DIFF_MODIFY_TWO_FILES))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_WORKDIR, &opts)); + + validate_index_unchanged(repo); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} + +static int hunk_skip_odds_cb(const git_diff_hunk *hunk, void *payload) +{ + int *count = (int *)payload; + GIT_UNUSED(hunk); + + return ((*count)++ % 2 == 1); +} + +void test_apply_callbacks__hunk_can_skip(void) +{ + git_diff *diff; + git_apply_options opts = GIT_APPLY_OPTIONS_INIT; + int count = 0; + + struct merge_index_entry workdir_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "06f751b6ba4f017ddbf4248015768300268e092a", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + opts.hunk_cb = hunk_skip_odds_cb; + opts.payload = &count; + + cl_git_pass(git_diff_from_buffer(&diff, + DIFF_MANY_CHANGES_ONE, strlen(DIFF_MANY_CHANGES_ONE))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_WORKDIR, &opts)); + + validate_index_unchanged(repo); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} diff --git a/tests/apply/fromdiff.c b/tests/apply/fromdiff.c index 611060988..8a6d8fa0a 100644 --- a/tests/apply/fromdiff.c +++ b/tests/apply/fromdiff.c @@ -49,7 +49,7 @@ static int apply_gitbuf( cl_assert_equal_s(patch_expected, patchbuf.ptr); } - error = git_apply__patch(&result, &filename, &mode, old ? old->ptr : NULL, old ? old->size : 0, patch); + error = git_apply__patch(&result, &filename, &mode, old ? old->ptr : NULL, old ? old->size : 0, patch, NULL); if (error == 0 && new == NULL) { cl_assert_equal_i(0, result.size); @@ -150,6 +150,52 @@ void test_apply_fromdiff__prepend_nocontext(void) PATCH_ORIGINAL_TO_PREPEND_NOCONTEXT, &diff_opts)); } +void test_apply_fromdiff__prepend_and_change(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_PREPEND_AND_CHANGE, "file.txt", + PATCH_ORIGINAL_TO_PREPEND_AND_CHANGE, NULL)); +} + +void test_apply_fromdiff__prepend_and_change_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_AND_CHANGE, "file.txt", + PATCH_ORIGINAL_TO_PREPEND_AND_CHANGE_NOCONTEXT, &diff_opts)); +} + +void test_apply_fromdiff__delete_and_change(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_DELETE_AND_CHANGE, "file.txt", + PATCH_ORIGINAL_TO_DELETE_AND_CHANGE, NULL)); +} + +void test_apply_fromdiff__delete_and_change_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_DELETE_AND_CHANGE, "file.txt", + PATCH_ORIGINAL_TO_DELETE_AND_CHANGE_NOCONTEXT, &diff_opts)); +} + +void test_apply_fromdiff__delete_firstline(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_DELETE_FIRSTLINE, "file.txt", + PATCH_ORIGINAL_TO_DELETE_FIRSTLINE, NULL)); +} + void test_apply_fromdiff__append(void) { cl_git_pass(apply_buf( diff --git a/tests/apply/fromfile.c b/tests/apply/fromfile.c index 0ace639fc..6d4379b1b 100644 --- a/tests/apply/fromfile.c +++ b/tests/apply/fromfile.c @@ -39,7 +39,7 @@ static int apply_patchfile( cl_git_pass(git_patch_from_buffer(&patch, patchfile, strlen(patchfile), NULL)); - error = git_apply__patch(&result, &filename, &mode, old, old_len, patch); + error = git_apply__patch(&result, &filename, &mode, old, old_len, patch, NULL); if (error == 0) { cl_assert_equal_i(new_len, result.size); diff --git a/tests/apply/index.c b/tests/apply/index.c new file mode 100644 index 000000000..9c9094cce --- /dev/null +++ b/tests/apply/index.c @@ -0,0 +1,321 @@ +#include "clar_libgit2.h" +#include "apply_helpers.h" + +static git_repository *repo; + +#define TEST_REPO_PATH "merge-recursive" + +void test_apply_index__initialize(void) +{ + git_oid oid; + git_commit *commit; + + repo = cl_git_sandbox_init(TEST_REPO_PATH); + + git_oid_fromstr(&oid, "539bd011c4822c560c1d17cab095006b7a10f707"); + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_reset(repo, (git_object *)commit, GIT_RESET_HARD, NULL)); + git_commit_free(commit); +} + +void test_apply_index__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_apply_index__generate_diff(void) +{ + git_oid a_oid, b_oid; + git_commit *a_commit, *b_commit; + git_tree *a_tree, *b_tree; + git_diff *diff; + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + + struct merge_index_entry index_expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t index_expected_cnt = sizeof(index_expected) / + sizeof(struct merge_index_entry); + + git_oid_fromstr(&a_oid, "539bd011c4822c560c1d17cab095006b7a10f707"); + git_oid_fromstr(&b_oid, "7c7bf85e978f1d18c0566f702d2cb7766b9c8d4f"); + cl_git_pass(git_commit_lookup(&a_commit, repo, &a_oid)); + cl_git_pass(git_commit_lookup(&b_commit, repo, &b_oid)); + + cl_git_pass(git_commit_tree(&a_tree, a_commit)); + cl_git_pass(git_commit_tree(&b_tree, b_commit)); + + cl_git_pass(git_diff_tree_to_tree(&diff, repo, a_tree, b_tree, &diff_opts)); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_INDEX, NULL)); + + validate_apply_index(repo, index_expected, index_expected_cnt); + validate_workdir_unchanged(repo); + + git_diff_free(diff); + git_tree_free(a_tree); + git_tree_free(b_tree); + git_commit_free(a_commit); + git_commit_free(b_commit); +} + +void test_apply_index__parsed_diff(void) +{ + git_diff *diff; + + struct merge_index_entry index_expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t index_expected_cnt = sizeof(index_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, + DIFF_MODIFY_TWO_FILES, strlen(DIFF_MODIFY_TWO_FILES))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_INDEX, NULL)); + + validate_apply_index(repo, index_expected, index_expected_cnt); + validate_workdir_unchanged(repo); + + git_diff_free(diff); +} + +void test_apply_index__removes_file(void) +{ + git_diff *diff; + + struct merge_index_entry index_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" }, + }; + size_t index_expected_cnt = sizeof(index_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_DELETE_FILE, + strlen(DIFF_DELETE_FILE))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_INDEX, NULL)); + + validate_apply_index(repo, index_expected, index_expected_cnt); + validate_workdir_unchanged(repo); + + git_diff_free(diff); +} + +void test_apply_index__adds_file(void) +{ + git_diff *diff; + + struct merge_index_entry index_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "6370543fcfedb3e6516ec53b06158f3687dc1447", 0, "newfile.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" }, + }; + size_t index_expected_cnt = sizeof(index_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, + DIFF_ADD_FILE, strlen(DIFF_ADD_FILE))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_INDEX, NULL)); + + validate_apply_index(repo, index_expected, index_expected_cnt); + validate_workdir_unchanged(repo); + + git_diff_free(diff); +} + +void test_apply_index__modified_workdir_with_unmodified_index_is_ok(void) +{ + git_diff *diff; + + const char *diff_file = DIFF_MODIFY_TWO_FILES; + + struct merge_index_entry index_expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t index_expected_cnt = sizeof(index_expected) / + sizeof(struct merge_index_entry); + + struct merge_index_entry workdir_expected[] = { + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "f75ba05f340c51065cbea2e1fdbfe5fe13144c97", 0, "veal.txt" } + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + /* mutate the workdir and leave the index matching HEAD */ + cl_git_rmfile("merge-recursive/asparagus.txt"); + cl_git_rewritefile("merge-recursive/veal.txt", "Hello, world.\n"); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_INDEX, NULL)); + + validate_apply_index(repo, index_expected, index_expected_cnt); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_index__application_failure_leaves_index_unmodified(void) +{ + git_diff *diff; + git_index *index; + + const char *diff_file = DIFF_MODIFY_TWO_FILES; + + struct merge_index_entry index_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + }; + size_t index_expected_cnt = sizeof(index_expected) / + sizeof(struct merge_index_entry); + + /* mutate the index */ + cl_git_pass(git_repository_index(&index, repo)); + cl_git_pass(git_index_remove(index, "veal.txt", 0)); + cl_git_pass(git_index_write(index)); + git_index_free(index); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_fail_with(GIT_EAPPLYFAIL, git_apply(repo, diff, GIT_APPLY_LOCATION_INDEX, NULL)); + + validate_apply_index(repo, index_expected, index_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_index__keeps_nonconflicting_changes(void) +{ + git_diff *diff; + git_index *index; + git_index_entry idx_entry; + + const char *diff_file = DIFF_MODIFY_TWO_FILES; + + struct merge_index_entry index_expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "898d12687fb35be271c27c795a6b32c8b51da79e", 0, "beef.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t index_expected_cnt = sizeof(index_expected) / + sizeof(struct merge_index_entry); + + /* mutate the index */ + cl_git_pass(git_repository_index(&index, repo)); + + memset(&idx_entry, 0, sizeof(git_index_entry)); + idx_entry.mode = 0100644; + idx_entry.path = "beef.txt"; + cl_git_pass(git_oid_fromstr(&idx_entry.id, "898d12687fb35be271c27c795a6b32c8b51da79e")); + cl_git_pass(git_index_add(index, &idx_entry)); + + cl_git_pass(git_index_remove(index, "bouilli.txt", 0)); + cl_git_pass(git_index_write(index)); + git_index_free(index); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_INDEX, NULL)); + + validate_apply_index(repo, index_expected, index_expected_cnt); + validate_workdir_unchanged(repo); + + git_diff_free(diff); +} + +void test_apply_index__can_apply_nonconflicting_file_changes(void) +{ + git_diff *diff; + git_index *index; + git_index_entry idx_entry; + + const char *diff_file = DIFF_MODIFY_TWO_FILES; + + struct merge_index_entry index_expected[] = { + { 0100644, "4f2d1645dee99ced096877911de540c65ade2ef8", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t index_expected_cnt = sizeof(index_expected) / + sizeof(struct merge_index_entry); + + /* + * Replace the index entry with a version that is different than + * HEAD but such that the patch still applies cleanly. This item + * has a new line appended. + */ + + cl_git_pass(git_repository_index(&index, repo)); + + memset(&idx_entry, 0, sizeof(git_index_entry)); + idx_entry.mode = 0100644; + idx_entry.path = "asparagus.txt"; + cl_git_pass(git_oid_fromstr(&idx_entry.id, "06d3fefb8726ab1099acc76e02dfb85e034b2538")); + cl_git_pass(git_index_add(index, &idx_entry)); + + cl_git_pass(git_index_write(index)); + git_index_free(index); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_INDEX, NULL)); + + validate_apply_index(repo, index_expected, index_expected_cnt); + validate_workdir_unchanged(repo); + + git_diff_free(diff); +} + +void test_apply_index__change_mode(void) +{ + git_diff *diff; + + const char *diff_file = DIFF_EXECUTABLE_FILE; + + struct merge_index_entry index_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100755, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" }, + }; + size_t index_expected_cnt = sizeof(index_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_INDEX, NULL)); + + validate_apply_index(repo, index_expected, index_expected_cnt); + validate_workdir_unchanged(repo); + + git_diff_free(diff); +} diff --git a/tests/apply/partial.c b/tests/apply/partial.c new file mode 100644 index 000000000..243dccf0f --- /dev/null +++ b/tests/apply/partial.c @@ -0,0 +1,232 @@ +#include "clar_libgit2.h" +#include "git2/sys/repository.h" + +#include "apply.h" +#include "repository.h" +#include "buf_text.h" + +#include "../patch/patch_common.h" + +static git_repository *repo = NULL; + +void test_apply_partial__initialize(void) +{ + repo = cl_git_sandbox_init("renames"); +} + +void test_apply_partial__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +static int skip_addition( + const git_diff_hunk *hunk, + void *payload) +{ + GIT_UNUSED(payload); + + return (hunk->new_lines > hunk->old_lines) ? 1 : 0; +} + +static int skip_deletion( + const git_diff_hunk *hunk, + void *payload) +{ + GIT_UNUSED(payload); + + return (hunk->new_lines < hunk->old_lines) ? 1 : 0; +} + +static int skip_change( + const git_diff_hunk *hunk, + void *payload) +{ + GIT_UNUSED(payload); + + return (hunk->new_lines == hunk->old_lines) ? 1 : 0; +} + +static int abort_addition( + const git_diff_hunk *hunk, + void *payload) +{ + GIT_UNUSED(payload); + + return (hunk->new_lines > hunk->old_lines) ? GIT_EUSER : 0; +} + +static int abort_deletion( + const git_diff_hunk *hunk, + void *payload) +{ + GIT_UNUSED(payload); + + return (hunk->new_lines < hunk->old_lines) ? GIT_EUSER : 0; +} + +static int abort_change( + const git_diff_hunk *hunk, + void *payload) +{ + GIT_UNUSED(payload); + + return (hunk->new_lines == hunk->old_lines) ? GIT_EUSER : 0; +} + +static int apply_buf( + const char *old, + const char *oldname, + const char *new, + const char *newname, + const char *expected, + const git_diff_options *diff_opts, + git_apply_hunk_cb hunk_cb, + void *payload) +{ + git_patch *patch; + git_buf result = GIT_BUF_INIT; + git_buf patchbuf = GIT_BUF_INIT; + git_apply_options opts = GIT_APPLY_OPTIONS_INIT; + char *filename; + unsigned int mode; + int error; + size_t oldsize = strlen(old); + size_t newsize = strlen(new); + + opts.hunk_cb = hunk_cb; + opts.payload = payload; + + cl_git_pass(git_patch_from_buffers(&patch, old, oldsize, oldname, new, newsize, newname, diff_opts)); + if ((error = git_apply__patch(&result, &filename, &mode, old, oldsize, patch, &opts)) == 0) { + cl_assert_equal_s(expected, result.ptr); + cl_assert_equal_s(newname, 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_partial__prepend_and_change_skip_addition(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_PREPEND_AND_CHANGE, "file.txt", + FILE_ORIGINAL, NULL, skip_addition, NULL)); +} + +void test_apply_partial__prepend_and_change_nocontext_skip_addition(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_AND_CHANGE, "file.txt", + FILE_CHANGE_MIDDLE, &diff_opts, skip_addition, NULL)); +} + +void test_apply_partial__prepend_and_change_nocontext_abort_addition(void) +{ + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + diff_opts.context_lines = 0; + + cl_git_fail(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_PREPEND_AND_CHANGE, "file.txt", + FILE_ORIGINAL, &diff_opts, abort_addition, NULL)); +} + +void test_apply_partial__prepend_and_change_skip_change(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_PREPEND_AND_CHANGE, "file.txt", + FILE_PREPEND_AND_CHANGE, NULL, skip_change, NULL)); +} + +void test_apply_partial__prepend_and_change_nocontext_skip_change(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_AND_CHANGE, "file.txt", + FILE_PREPEND, &diff_opts, skip_change, NULL)); +} + +void test_apply_partial__prepend_and_change_nocontext_abort_change(void) +{ + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + diff_opts.context_lines = 0; + + cl_git_fail(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_PREPEND_AND_CHANGE, "file.txt", + FILE_PREPEND, &diff_opts, abort_change, NULL)); +} + +void test_apply_partial__delete_and_change_skip_deletion(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_DELETE_AND_CHANGE, "file.txt", + FILE_ORIGINAL, NULL, skip_deletion, NULL)); +} + +void test_apply_partial__delete_and_change_nocontext_skip_deletion(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_DELETE_AND_CHANGE, "file.txt", + FILE_CHANGE_MIDDLE, &diff_opts, skip_deletion, NULL)); +} + +void test_apply_partial__delete_and_change_nocontext_abort_deletion(void) +{ + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + diff_opts.context_lines = 0; + + cl_git_fail(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_DELETE_AND_CHANGE, "file.txt", + FILE_ORIGINAL, &diff_opts, abort_deletion, NULL)); +} + +void test_apply_partial__delete_and_change_skip_change(void) +{ + cl_git_pass(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_DELETE_AND_CHANGE, "file.txt", + FILE_DELETE_AND_CHANGE, NULL, skip_change, NULL)); +} + +void test_apply_partial__delete_and_change_nocontext_skip_change(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_DELETE_AND_CHANGE, "file.txt", + FILE_DELETE_FIRSTLINE, &diff_opts, skip_change, NULL)); +} + +void test_apply_partial__delete_and_change_nocontext_abort_change(void) +{ + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + diff_opts.context_lines = 0; + + cl_git_fail(apply_buf( + FILE_ORIGINAL, "file.txt", + FILE_DELETE_AND_CHANGE, "file.txt", + FILE_DELETE_FIRSTLINE, &diff_opts, abort_change, NULL)); +} diff --git a/tests/apply/tree.c b/tests/apply/tree.c new file mode 100644 index 000000000..f35b13ce0 --- /dev/null +++ b/tests/apply/tree.c @@ -0,0 +1,58 @@ +#include "clar_libgit2.h" +#include "../merge/merge_helpers.h" + +static git_repository *repo; + +#define TEST_REPO_PATH "merge-recursive" + + +void test_apply_tree__initialize(void) +{ + repo = cl_git_sandbox_init(TEST_REPO_PATH); +} + +void test_apply_tree__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_apply_tree__one(void) +{ + git_oid a_oid, b_oid; + git_commit *a_commit, *b_commit; + git_tree *a_tree, *b_tree; + git_diff *diff; + git_index *index = NULL; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + + struct merge_index_entry expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + + git_oid_fromstr(&a_oid, "539bd011c4822c560c1d17cab095006b7a10f707"); + git_oid_fromstr(&b_oid, "7c7bf85e978f1d18c0566f702d2cb7766b9c8d4f"); + + cl_git_pass(git_commit_lookup(&a_commit, repo, &a_oid)); + cl_git_pass(git_commit_lookup(&b_commit, repo, &b_oid)); + + cl_git_pass(git_commit_tree(&a_tree, a_commit)); + cl_git_pass(git_commit_tree(&b_tree, b_commit)); + + cl_git_pass(git_diff_tree_to_tree(&diff, repo, a_tree, b_tree, &opts)); + + cl_git_pass(git_apply_to_tree(&index, repo, a_tree, diff, NULL)); + merge_test_index(index, expected, 6); + + git_index_free(index); + git_diff_free(diff); + git_tree_free(a_tree); + git_tree_free(b_tree); + git_commit_free(a_commit); + git_commit_free(b_commit); +} + diff --git a/tests/apply/workdir.c b/tests/apply/workdir.c new file mode 100644 index 000000000..1a8292508 --- /dev/null +++ b/tests/apply/workdir.c @@ -0,0 +1,358 @@ +#include "clar_libgit2.h" +#include "apply_helpers.h" + +static git_repository *repo; + +#define TEST_REPO_PATH "merge-recursive" + +void test_apply_workdir__initialize(void) +{ + git_oid oid; + git_commit *commit; + + repo = cl_git_sandbox_init(TEST_REPO_PATH); + + git_oid_fromstr(&oid, "539bd011c4822c560c1d17cab095006b7a10f707"); + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_reset(repo, (git_object *)commit, GIT_RESET_HARD, NULL)); + git_commit_free(commit); +} + +void test_apply_workdir__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_apply_workdir__generated_diff(void) +{ + git_oid a_oid, b_oid; + git_commit *a_commit, *b_commit; + git_tree *a_tree, *b_tree; + git_diff *diff; + git_diff_options opts = GIT_DIFF_OPTIONS_INIT; + + struct merge_index_entry workdir_expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + git_oid_fromstr(&a_oid, "539bd011c4822c560c1d17cab095006b7a10f707"); + git_oid_fromstr(&b_oid, "7c7bf85e978f1d18c0566f702d2cb7766b9c8d4f"); cl_git_pass(git_commit_lookup(&a_commit, repo, &a_oid)); + cl_git_pass(git_commit_lookup(&b_commit, repo, &b_oid)); + + cl_git_pass(git_commit_tree(&a_tree, a_commit)); + cl_git_pass(git_commit_tree(&b_tree, b_commit)); + + cl_git_pass(git_diff_tree_to_tree(&diff, repo, a_tree, b_tree, &opts)); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_WORKDIR, NULL)); + + validate_index_unchanged(repo); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); + git_tree_free(a_tree); + git_tree_free(b_tree); + git_commit_free(a_commit); + git_commit_free(b_commit); +} + +void test_apply_workdir__parsed_diff(void) +{ + git_diff *diff; + + struct merge_index_entry workdir_expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, + DIFF_MODIFY_TWO_FILES, strlen(DIFF_MODIFY_TWO_FILES))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_WORKDIR, NULL)); + + validate_index_unchanged(repo); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_workdir__removes_file(void) +{ + git_diff *diff; + + struct merge_index_entry workdir_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, DIFF_DELETE_FILE, + strlen(DIFF_DELETE_FILE))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_WORKDIR, NULL)); + + validate_index_unchanged(repo); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_workdir__adds_file(void) +{ + git_diff *diff; + + struct merge_index_entry workdir_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "6370543fcfedb3e6516ec53b06158f3687dc1447", 0, "newfile.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, + DIFF_ADD_FILE, strlen(DIFF_ADD_FILE))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_WORKDIR, NULL)); + + validate_index_unchanged(repo); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_workdir__modified_index_with_unmodified_workdir_is_ok(void) +{ + git_index *index; + git_index_entry idx_entry = {{0}}; + git_diff *diff; + + const char *diff_file = DIFF_MODIFY_TWO_FILES; + + struct merge_index_entry index_expected[] = { + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "veal.txt" } + }; + size_t index_expected_cnt = sizeof(index_expected) / + sizeof(struct merge_index_entry); + + struct merge_index_entry workdir_expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + /* mutate the index and leave the workdir matching HEAD */ + git_repository_index(&index, repo); + + idx_entry.mode = 0100644; + idx_entry.path = "veal.txt"; + cl_git_pass(git_oid_fromstr(&idx_entry.id, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d")); + + cl_git_pass(git_index_add(index, &idx_entry)); + cl_git_pass(git_index_remove(index, "asparagus.txt", 0)); + cl_git_pass(git_index_write(index)); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_WORKDIR, NULL)); + + validate_apply_index(repo, index_expected, index_expected_cnt); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_index_free(index); + git_diff_free(diff); +} + +void test_apply_workdir__application_failure_leaves_workdir_unmodified(void) +{ + git_diff *diff; + + const char *diff_file = DIFF_MODIFY_TWO_FILES; + + struct merge_index_entry workdir_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "8684724651336001c5dbce74bed6736d2443958d", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + /* mutate the workdir */ + cl_git_rewritefile("merge-recursive/veal.txt", + "This is a modification.\n"); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_fail_with(GIT_EAPPLYFAIL, git_apply(repo, diff, GIT_APPLY_LOCATION_WORKDIR, NULL)); + + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_workdir__keeps_nonconflicting_changes(void) +{ + git_diff *diff; + + struct merge_index_entry workdir_expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "f75ba05f340c51065cbea2e1fdbfe5fe13144c97", 0, "gravy.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + cl_git_rmfile("merge-recursive/oyster.txt"); + cl_git_rewritefile("merge-recursive/gravy.txt", "Hello, world.\n"); + + cl_git_pass(git_diff_from_buffer(&diff, + DIFF_MODIFY_TWO_FILES, strlen(DIFF_MODIFY_TWO_FILES))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_WORKDIR, NULL)); + + validate_index_unchanged(repo); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_workdir__can_apply_nonconflicting_file_changes(void) +{ + git_diff *diff; + + const char *diff_file = DIFF_MODIFY_TWO_FILES; + + struct merge_index_entry workdir_expected[] = { + { 0100644, "5db1a0fef164cb66cc0c00d35cc5af979ddc1a64", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + /* + * Replace the workdir file with a version that is different than + * HEAD but such that the patch still applies cleanly. This item + * has a new line appended. + */ + cl_git_append2file("merge-recursive/asparagus.txt", + "This line is added in the workdir.\n"); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_WORKDIR, NULL)); + + validate_index_unchanged(repo); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_workdir__change_mode(void) +{ +#ifndef GIT_WIN32 + git_diff *diff; + + const char *diff_file = DIFF_EXECUTABLE_FILE; + + struct merge_index_entry workdir_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100755, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, diff_file, strlen(diff_file))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_WORKDIR, NULL)); + + validate_index_unchanged(repo); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +#endif +} + +void test_apply_workdir__apply_many_changes_one(void) +{ + git_diff *diff; + git_apply_options opts = GIT_APPLY_OPTIONS_INIT; + + struct merge_index_entry workdir_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "c9d7d5d58088bc91f6e06f17ca3a205091568d3a", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, + DIFF_MANY_CHANGES_ONE, strlen(DIFF_MANY_CHANGES_ONE))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_WORKDIR, &opts)); + + validate_index_unchanged(repo); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} + +void test_apply_workdir__apply_many_changes_two(void) +{ + git_diff *diff; + git_apply_options opts = GIT_APPLY_OPTIONS_INIT; + + struct merge_index_entry workdir_expected[] = { + { 0100644, "f51658077d85f2264fa179b4d0848268cb3475c3", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "6b943d65af6d8db74d747284fa4ca7d716ad5bbb", 0, "veal.txt" }, + }; + size_t workdir_expected_cnt = sizeof(workdir_expected) / + sizeof(struct merge_index_entry); + + cl_git_pass(git_diff_from_buffer(&diff, + DIFF_MANY_CHANGES_TWO, strlen(DIFF_MANY_CHANGES_TWO))); + cl_git_pass(git_apply(repo, diff, GIT_APPLY_LOCATION_WORKDIR, &opts)); + + validate_index_unchanged(repo); + validate_apply_workdir(repo, workdir_expected, workdir_expected_cnt); + + git_diff_free(diff); +} diff --git a/tests/iterator/workdir.c b/tests/iterator/workdir.c index a16acd722..889fcd6c0 100644 --- a/tests/iterator/workdir.c +++ b/tests/iterator/workdir.c @@ -3,6 +3,7 @@ #include "repository.h" #include "fileops.h" #include "../submodule/submodule_helpers.h" +#include "../merge/merge_helpers.h" #include "iterator_helpers.h" #include <stdarg.h> @@ -1474,3 +1475,48 @@ void test_iterator_workdir__pathlist_with_directory_include_trees(void) git_vector_free(&filelist); } +void test_iterator_workdir__hash_when_requested(void) +{ + git_iterator *iter; + const git_index_entry *entry; + git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT; + git_oid expected_id = {{0}}; + size_t i; + + struct merge_index_entry expected[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" }, + { 0100644, "7c7e08f9559d9e1551b91e1cf68f1d0066109add", 0, "oyster.txt" }, + { 0100644, "898d12687fb35be271c27c795a6b32c8b51da79e", 0, "veal.txt" }, + }; + + g_repo = cl_git_sandbox_init("merge-recursive"); + + /* do the iteration normally, ensure there are no hashes */ + cl_git_pass(git_iterator_for_workdir(&iter, g_repo, NULL, NULL, &iter_opts)); + + for (i = 0; i < sizeof(expected) / sizeof(struct merge_index_entry); i++) { + cl_git_pass(git_iterator_advance(&entry, iter)); + + cl_assert_equal_oid(&expected_id, &entry->id); + cl_assert_equal_s(expected[i].path, entry->path); + } + cl_assert_equal_i(GIT_ITEROVER, git_iterator_advance(&entry, iter)); + git_iterator_free(iter); + + /* do the iteration requesting hashes */ + iter_opts.flags |= GIT_ITERATOR_INCLUDE_HASH; + cl_git_pass(git_iterator_for_workdir(&iter, g_repo, NULL, NULL, &iter_opts)); + + for (i = 0; i < sizeof(expected) / sizeof(struct merge_index_entry); i++) { + cl_git_pass(git_iterator_advance(&entry, iter)); + + cl_git_pass(git_oid_fromstr(&expected_id, expected[i].oid_str)); + cl_assert_equal_oid(&expected_id, &entry->id); + cl_assert_equal_s(expected[i].path, entry->path); + } + cl_assert_equal_i(GIT_ITEROVER, git_iterator_advance(&entry, iter)); + git_iterator_free(iter); +} diff --git a/tests/patch/patch_common.h b/tests/patch/patch_common.h index e838e6089..3f2668ddc 100644 --- a/tests/patch/patch_common.h +++ b/tests/patch/patch_common.h @@ -220,6 +220,112 @@ "@@ -0,0 +1 @@\n" \ "+insert at front\n" +/* An insertion at the beginning of the file and change in the middle */ + +#define FILE_PREPEND_AND_CHANGE \ + "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_AND_CHANGE \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..f73c8bb 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -1,9 +1,10 @@\n" \ + "+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" \ + "+(THIS line is changed!)\n" \ + " and this\n" \ + " is additional context\n" \ + " below it!\n" + +#define PATCH_ORIGINAL_TO_PREPEND_AND_CHANGE_NOCONTEXT \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..f73c8bb 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -0,0 +1 @@\n" \ + "+insert at front\n" \ + "@@ -6 +7 @@ yes it is!\n" \ + "-(this line is changed)\n" \ + "+(THIS line is changed!)\n" + +/* A deletion at the beginning of the file and a change in the middle */ + +#define FILE_DELETE_AND_CHANGE \ + "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_DELETE_AND_CHANGE \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..1e2dfa6 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -1,9 +1,8 @@\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" \ + "+(THIS line is changed!)\n" \ + " and this\n" \ + " is additional context\n" \ + " below it!\n" + +#define PATCH_ORIGINAL_TO_DELETE_AND_CHANGE_NOCONTEXT \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..1e2dfa6 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -1 +0,0 @@\n" \ + "-hey!\n" \ + "@@ -6 +5 @@ yes it is!\n" \ + "-(this line is changed)\n" \ + "+(THIS line is changed!)\n" + +/* A deletion at the beginning of the file */ + +#define FILE_DELETE_FIRSTLINE \ + "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_DELETE_FIRSTLINE \ + "diff --git a/file.txt b/file.txt\n" \ + "index 9432026..f31fa13 100644\n" \ + "--- a/file.txt\n" \ + "+++ b/file.txt\n" \ + "@@ -1,4 +1,3 @@\n" \ + "-hey!\n" \ + " this is some context!\n" \ + " around some lines\n" \ + " that will change\n" + /* An insertion at the end of the file (and the resultant patch) */ #define FILE_APPEND \ diff --git a/tests/resources/merge-recursive/.gitted/objects/06/d3fefb8726ab1099acc76e02dfb85e034b2538 b/tests/resources/merge-recursive/.gitted/objects/06/d3fefb8726ab1099acc76e02dfb85e034b2538 Binary files differnew file mode 100644 index 000000000..b3919aab6 --- /dev/null +++ b/tests/resources/merge-recursive/.gitted/objects/06/d3fefb8726ab1099acc76e02dfb85e034b2538 |