Commit f83bbe0a882cb141c636ebf7657462b3be85dea8

Edward Thomson 2018-03-19T19:50:45

apply: introduce `git_apply` Introduce `git_apply`, which will take a `git_diff` and apply it to the working directory (akin to `git apply`), the index (akin to `git apply --cached`), or both (akin to `git apply --index`).

diff --git a/include/git2/apply.h b/include/git2/apply.h
index a4ec4fc..3bf6aad 100644
--- a/include/git2/apply.h
+++ b/include/git2/apply.h
@@ -36,6 +36,48 @@ GIT_EXTERN(int) git_apply_to_tree(
 	git_tree *preimage,
 	git_diff *diff);
 
+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;
+
+typedef struct {
+	unsigned int version;
+
+	git_apply_location_t location;
+} git_apply_options;
+
+#define GIT_APPLY_OPTIONS_VERSION 1
+#define GIT_APPLY_OPTIONS_INIT {GIT_APPLY_OPTIONS_VERSION}
+
+/**
+ * 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 options the options for the apply (or null for defaults)
+ */
+GIT_EXTERN(int) git_apply(
+	git_repository *repo,
+	git_diff *diff,
+	git_apply_options *options);
+
 /** @} */
 GIT_END_DECL
 #endif
diff --git a/src/apply.c b/src/apply.c
index 134a98f..e761b27 100644
--- a/src/apply.c
+++ b/src/apply.c
@@ -9,10 +9,13 @@
 
 #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"
@@ -481,3 +484,110 @@ done:
 
 	return error;
 }
+
+/* normal: apply to workdir: ignore index
+ * --cached: apply to index: ignore workdir
+ * --index: apply to both: validate index == workdir
+ */
+
+int git_apply(
+	git_repository *repo,
+	git_diff *diff,
+	git_apply_options *given_opts)
+{
+	git_index *contents = NULL, *repo_index = NULL;
+	git_reader *pre_reader = NULL;
+	const git_diff_delta *delta;
+	git_vector paths = GIT_VECTOR_INIT;
+	git_apply_options opts = GIT_APPLY_OPTIONS_INIT;
+	git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
+	bool do_checkout;
+	size_t i;
+	int error;
+
+	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));
+
+	do_checkout = (opts.location != GIT_APPLY_LOCATION_INDEX);
+
+	/*
+	 * 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.
+	 */
+	if (opts.location == GIT_APPLY_LOCATION_WORKDIR)
+		error = git_reader_for_workdir(&pre_reader, repo);
+	else
+		error = git_reader_for_index(&pre_reader, repo, NULL);
+
+	if (error < 0)
+		goto done;
+
+	/* if we're not checking out, we're writing to the repo's index */
+	if (do_checkout)
+		error = git_vector_init(&paths, git_diff_num_deltas(diff), NULL);
+	else
+		error = git_repository_index(&repo_index, repo);
+
+	if (error < 0)
+		goto done;
+
+	/*
+	 * note: this is not the full postimage, this only contains the
+	 * new files created during the diffing process.  we will limit
+	 * checkout to only write the files affected by this diff.
+	 */
+	if ((error = git_index_new(&contents)) < 0)
+		goto done;
+
+	for (i = 0; i < git_diff_num_deltas(diff); i++) {
+		delta = git_diff_get_delta(diff, i);
+
+		if ((error = apply_one(repo, pre_reader, contents, diff, i)) < 0)
+			goto done;
+
+		if (do_checkout) {
+			git_vector_insert(&paths, (void *)delta->old_file.path);
+
+			if (strcmp(delta->old_file.path, delta->new_file.path))
+				git_vector_insert(&paths, (void *)delta->new_file.path);
+		}
+	}
+
+	if (do_checkout) {
+		checkout_opts.checkout_strategy |= GIT_CHECKOUT_SAFE;
+		checkout_opts.checkout_strategy |= GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH;
+
+		if (opts.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;
+
+		error = git_checkout_index(repo, contents, &checkout_opts);
+	} else {
+		const git_index_entry *entry;
+
+		for (i = 0; i < git_index_entrycount(contents); i++) {
+			entry = git_index_get_byindex(contents, i);
+
+			if ((error = git_index_add(repo_index, entry)) < 0)
+				goto done;
+		}
+
+		error = git_index_write(repo_index);
+	}
+
+done:
+	git_vector_free(&paths);
+	git_index_free(contents);
+	git_reader_free(pre_reader);
+	git_index_free(repo_index);
+
+	return error;
+}