summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorEdward Thomson <ethomson@edwardthomson.com>2018-11-11 16:40:56 +0000
committerGitHub <noreply@github.com>2018-11-11 16:40:56 +0000
commit11fbead80b425eacf483fe16beaf8891f582f905 (patch)
tree347128ac534ff09809772ac5e87f6ccbbb742793 /src
parent2f5f3cfdcd7b15fc74fa1ed4b1695150ed071504 (diff)
parent4e746d80d229a77b08c5689a64d880bde5fd960f (diff)
downloadlibgit2-11fbead80b425eacf483fe16beaf8891f582f905.tar.gz
Merge pull request #4705 from libgit2/ethomson/apply
Patch (diff) application
Diffstat (limited to 'src')
-rw-r--r--src/apply.c490
-rw-r--r--src/apply.h4
-rw-r--r--src/iterator.c90
-rw-r--r--src/iterator.h15
-rw-r--r--src/reader.c259
-rw-r--r--src/reader.h107
6 files changed, 946 insertions, 19 deletions
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