Commit ac587e7596ced3616d682b7c57815f20dc0edaa8

Carlos Martín Nieto 2015-05-31T15:45:56

Merge pull request #3048 from pks-t/insteadof Implementation of url.*.insteadOf

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f42682..8d4b733 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -61,6 +61,10 @@ support for HTTPS connections insead of OpenSSL.
   `git_off_t` instead of `size_t` for the size of the blob, which
   allows putting large files into the odb on 32-bit systems.
 
+* The remote's push and pull URLs now honor the url.$URL.insteadOf
+  configuration. This allows modifying URL prefixes to a custom
+  value via gitconfig.
+
 ### API additions
 
 * The `git_merge_options` gained a `file_flags` member.
diff --git a/include/git2/remote.h b/include/git2/remote.h
index 02d73a0..ccd0b43 100644
--- a/include/git2/remote.h
+++ b/include/git2/remote.h
@@ -120,6 +120,9 @@ GIT_EXTERN(const char *) git_remote_name(const git_remote *remote);
 /**
  * Get the remote's url
  *
+ * If url.*.insteadOf has been configured for this URL, it will
+ * return the modified URL.
+ *
  * @param remote the remote
  * @return a pointer to the url
  */
@@ -128,6 +131,9 @@ GIT_EXTERN(const char *) git_remote_url(const git_remote *remote);
 /**
  * Get the remote's url for pushing
  *
+ * If url.*.pushInsteadOf has been configured for this URL, it
+ * will return the modified URL.
+ *
  * @param remote the remote
  * @return a pointer to the url or NULL if no special url for pushing is set
  */
diff --git a/src/remote.c b/src/remote.c
index 43b3456..b7acbb9 100644
--- a/src/remote.c
+++ b/src/remote.c
@@ -28,6 +28,7 @@
 
 static int dwim_refspecs(git_vector *out, git_vector *refspecs, git_vector *refs);
 static int lookup_remote_prune_config(git_remote *remote, git_config *config, const char *name);
+char *apply_insteadof(git_config *config, const char *url, int direction);
 
 static int add_refspec_to(git_vector *vector, const char *string, bool is_fetch)
 {
@@ -207,7 +208,7 @@ static int create_internal(git_remote **out, git_repository *repo, const char *n
 		canonicalize_url(&canonical_url, url) < 0)
 		goto on_error;
 
-	remote->url = git_buf_detach(&canonical_url);
+	remote->url = apply_insteadof(repo->_config, canonical_url.ptr, GIT_DIRECTION_FETCH);
 
 	if (name != NULL) {
 		remote->name = git__strdup(name);
@@ -216,7 +217,7 @@ static int create_internal(git_remote **out, git_repository *repo, const char *n
 		if ((error = git_buf_printf(&var, CONFIG_URL_FMT, name)) < 0)
 			goto on_error;
 
-		if ((error = git_config_set_string(config, var.ptr, remote->url)) < 0)
+		if ((error = git_config_set_string(config, var.ptr, canonical_url.ptr)) < 0)
 			goto on_error;
 	}
 
@@ -341,7 +342,7 @@ int git_remote_dup(git_remote **dest, git_remote *source)
 
 	if (source->url != NULL) {
 		remote->url = git__strdup(source->url);
-		GITERR_CHECK_ALLOC(remote->url);		
+		GITERR_CHECK_ALLOC(remote->url);
 	}
 
 	if (source->pushurl != NULL) {
@@ -456,7 +457,7 @@ int git_remote_lookup(git_remote **out, git_repository *repo, const char *name)
 	remote->download_tags = GIT_REMOTE_DOWNLOAD_TAGS_AUTO;
 
 	if (found && strlen(val) > 0) {
-		remote->url = git__strdup(val);
+		remote->url = apply_insteadof(config, val, GIT_DIRECTION_FETCH);
 		GITERR_CHECK_ALLOC(remote->url);
 	}
 
@@ -476,7 +477,7 @@ int git_remote_lookup(git_remote **out, git_repository *repo, const char *name)
 	}
 
 	if (found && strlen(val) > 0) {
-		remote->pushurl = git__strdup(val);
+		remote->pushurl = apply_insteadof(config, val, GIT_DIRECTION_PUSH);
 		GITERR_CHECK_ALLOC(remote->pushurl);
 	}
 
@@ -2421,3 +2422,67 @@ int git_remote_push(git_remote *remote, const git_strarray *refspecs, const git_
 	git_remote_disconnect(remote);
 	return error;
 }
+
+#define PREFIX "url"
+#define SUFFIX_FETCH "insteadof"
+#define SUFFIX_PUSH "pushinsteadof"
+
+char *apply_insteadof(git_config *config, const char *url, int direction)
+{
+	size_t match_length, prefix_length, suffix_length;
+	char *replacement = NULL;
+	const char *regexp;
+
+	git_buf result = GIT_BUF_INIT;
+	git_config_entry *entry;
+	git_config_iterator *iter;
+
+	assert(config);
+	assert(url);
+	assert(direction == GIT_DIRECTION_FETCH || direction == GIT_DIRECTION_PUSH);
+
+	/* Add 1 to prefix/suffix length due to the additional escaped dot */
+	prefix_length = strlen(PREFIX) + 1;
+	if (direction == GIT_DIRECTION_FETCH) {
+		regexp = PREFIX "\\..*\\." SUFFIX_FETCH;
+		suffix_length = strlen(SUFFIX_FETCH) + 1;
+	} else {
+		regexp = PREFIX "\\..*\\." SUFFIX_PUSH;
+		suffix_length = strlen(SUFFIX_PUSH) + 1;
+	}
+
+	git_config_iterator_glob_new(&iter, config, regexp);
+
+	match_length = 0;
+	while (git_config_next(&entry, iter) == 0) {
+		size_t n, replacement_length;
+
+		/* Check if entry value is a prefix of URL */
+		if (git__prefixcmp(url, entry->value))
+			continue;
+		/* Check if entry value is longer than previous
+		 * prefixes */
+		if ((n = strlen(entry->value)) <= match_length)
+			continue;
+
+		git__free(replacement);
+		match_length = n;
+
+		/* Cut off prefix and suffix of the value */
+		replacement_length =
+		    strlen(entry->name) - (prefix_length + suffix_length);
+		replacement = git__strndup(entry->name + prefix_length,
+				replacement_length);
+	}
+
+	git_config_iterator_free(iter);
+
+	if (match_length == 0)
+		return git__strdup(url);
+
+	git_buf_printf(&result, "%s%s", replacement, url + match_length);
+
+	git__free(replacement);
+
+	return result.ptr;
+}
diff --git a/tests/remote/insteadof.c b/tests/remote/insteadof.c
new file mode 100644
index 0000000..05d4757
--- /dev/null
+++ b/tests/remote/insteadof.c
@@ -0,0 +1,72 @@
+#include "clar_libgit2.h"
+#include "remote.h"
+#include "repository.h"
+
+#define REPO_PATH "testrepo2/.gitted"
+#define REMOTE_ORIGIN "origin"
+#define REMOTE_INSTEADOF "insteadof-test"
+
+static git_repository *g_repo;
+static git_remote *g_remote;
+
+void test_remote_insteadof__initialize(void)
+{
+	g_repo = NULL;
+	g_remote = NULL;
+}
+
+void test_remote_insteadof__cleanup(void)
+{
+	git_repository_free(g_repo);
+	git_remote_free(g_remote);
+}
+
+void test_remote_insteadof__url_insteadof_not_applicable(void)
+{
+	cl_git_pass(git_repository_open(&g_repo, cl_fixture(REPO_PATH)));
+	cl_git_pass(git_remote_lookup(&g_remote, g_repo, REMOTE_ORIGIN));
+
+	cl_assert_equal_s(
+		git_remote_url(g_remote),
+		"https://github.com/libgit2/false.git");
+}
+
+void test_remote_insteadof__url_insteadof_applicable(void)
+{
+	cl_git_pass(git_repository_open(&g_repo, cl_fixture(REPO_PATH)));
+	cl_git_pass(git_remote_lookup(&g_remote, g_repo, REMOTE_INSTEADOF));
+
+	cl_assert_equal_s(
+	    git_remote_url(g_remote),
+	    "http://github.com/libgit2/libgit2");
+}
+
+void test_remote_insteadof__pushurl_insteadof_not_applicable(void)
+{
+	cl_git_pass(git_repository_open(&g_repo, cl_fixture(REPO_PATH)));
+	cl_git_pass(git_remote_lookup(&g_remote, g_repo, REMOTE_ORIGIN));
+
+	cl_assert_equal_p(git_remote_pushurl(g_remote), NULL);
+}
+
+void test_remote_insteadof__pushurl_insteadof_applicable(void)
+{
+	cl_git_pass(git_repository_open(&g_repo, cl_fixture(REPO_PATH)));
+	cl_git_pass(git_remote_lookup(&g_remote, g_repo, REMOTE_INSTEADOF));
+
+	cl_assert_equal_s(
+	    git_remote_pushurl(g_remote),
+	    "git@github.com:libgit2/libgit2");
+}
+
+void test_remote_insteadof__anonymous_remote(void)
+{
+	cl_git_pass(git_repository_open(&g_repo, cl_fixture(REPO_PATH)));
+	cl_git_pass(git_remote_create_anonymous(&g_remote, g_repo,
+	    "http://example.com/libgit2/libgit2"));
+
+	cl_assert_equal_s(
+	    git_remote_url(g_remote),
+	    "http://github.com/libgit2/libgit2");
+	cl_assert_equal_p(git_remote_pushurl(g_remote), NULL);
+}
diff --git a/tests/resources/testrepo2/.gitted/config b/tests/resources/testrepo2/.gitted/config
index fc2433c..4af067f 100644
--- a/tests/resources/testrepo2/.gitted/config
+++ b/tests/resources/testrepo2/.gitted/config
@@ -8,7 +8,19 @@
 [remote "origin"]
 	url = https://github.com/libgit2/false.git
 	fetch = +refs/heads/*:refs/remotes/origin/*
+[remote "insteadof-test"]
+	url = http://example.com/libgit2/libgit2
+	pushurl = http://github.com/libgit2/libgit2
+	fetch = +refs/heads/*:refs/remotes/test/*
 [branch "master"]
 	remote = origin
 	merge = refs/heads/master
 	rebase = true
+[url "longer-non-prefix-match"]
+	insteadOf = ttp://example.com/li
+[url "shorter-prefix"]
+	insteadOf = http://example.co
+[url "http://github.com"]
+	insteadOf = http://example.com
+[url "git@github.com:"]
+	pushInsteadOf = http://github.com/