Merge pull request #4243 from pks-t/pks/submodule-workdir

Submodule working directory
diff --git a/src/attrcache.c b/src/attrcache.c
index 4df14ee..5416189 100644
--- a/src/attrcache.c
+++ b/src/attrcache.c
@@ -290,14 +290,16 @@
 		const char *cfgval = entry->value;
 
 		/* expand leading ~/ as needed */
-		if (cfgval && cfgval[0] == '~' && cfgval[1] == '/' &&
-			!git_sysdir_find_global_file(&buf, &cfgval[2]))
-			*out = git_buf_detach(&buf);
-		else if (cfgval)
+		if (cfgval && cfgval[0] == '~' && cfgval[1] == '/') {
+			if (! (error = git_sysdir_expand_global_file(&buf, &cfgval[2])))
+				*out = git_buf_detach(&buf);
+		} else if (cfgval) {
 			*out = git__strdup(cfgval);
+		}
 	}
-	else if (!git_sysdir_find_xdg_file(&buf, fallback))
+	else if (!git_sysdir_find_xdg_file(&buf, fallback)) {
 		*out = git_buf_detach(&buf);
+	}
 
 	git_config_entry_free(entry);
 	git_buf_free(&buf);
diff --git a/src/config.c b/src/config.c
index cbcea2e..169a628 100644
--- a/src/config.c
+++ b/src/config.c
@@ -1358,9 +1358,6 @@
 
 int git_config_parse_path(git_buf *out, const char *value)
 {
-	int error = 0;
-	const git_buf *home;
-
 	assert(out && value);
 
 	git_buf_sanitize(out);
@@ -1371,16 +1368,7 @@
 			return -1;
 		}
 
-		if ((error = git_sysdir_get(&home, GIT_SYSDIR_GLOBAL)) < 0)
-			return error;
-
-		git_buf_sets(out, home->ptr);
-		git_buf_puts(out, value + 1);
-
-		if (git_buf_oom(out))
-			return -1;
-
-		return 0;
+		return git_sysdir_expand_global_file(out, value[1] ? &value[2] : NULL);
 	}
 
 	return git_buf_sets(out, value);
diff --git a/src/config_file.c b/src/config_file.c
index 7df43c8..2302d33 100644
--- a/src/config_file.c
+++ b/src/config_file.c
@@ -1256,7 +1256,7 @@
 {
 	/* From the user's home */
 	if (path[0] == '~' && path[1] == '/')
-		return git_sysdir_find_global_file(out, &path[1]);
+		return git_sysdir_expand_global_file(out, &path[1]);
 
 	return git_path_join_unrooted(out, path, dir, NULL);
 }
diff --git a/src/repository.c b/src/repository.c
index 18c6eed..d0a38cc 100644
--- a/src/repository.c
+++ b/src/repository.c
@@ -2553,7 +2553,9 @@
 
 	git_buf_puts(out, " to ");
 
-	if (git_reference__is_branch(new))
+	if (git_reference__is_branch(new) ||
+		git_reference__is_tag(new) ||
+		git_reference__is_remote(new))
 		git_buf_puts(out, git_reference__shorthand(new));
 	else
 		git_buf_puts(out, new);
@@ -2564,6 +2566,41 @@
 	return 0;
 }
 
+static int detach(git_repository *repo, const git_oid *id, const char *new)
+{
+	int error;
+	git_buf log_message = GIT_BUF_INIT;
+	git_object *object = NULL, *peeled = NULL;
+	git_reference *new_head = NULL, *current = NULL;
+
+	assert(repo && id);
+
+	if ((error = git_reference_lookup(&current, repo, GIT_HEAD_FILE)) < 0)
+		return error;
+
+	if ((error = git_object_lookup(&object, repo, id, GIT_OBJ_ANY)) < 0)
+		goto cleanup;
+
+	if ((error = git_object_peel(&peeled, object, GIT_OBJ_COMMIT)) < 0)
+		goto cleanup;
+
+	if (new == NULL)
+		new = git_oid_tostr_s(git_object_id(peeled));
+
+	if ((error = checkout_message(&log_message, current, new)) < 0)
+		goto cleanup;
+
+	error = git_reference_create(&new_head, repo, GIT_HEAD_FILE, git_object_id(peeled), true, git_buf_cstr(&log_message));
+
+cleanup:
+	git_buf_free(&log_message);
+	git_object_free(object);
+	git_object_free(peeled);
+	git_reference_free(current);
+	git_reference_free(new_head);
+	return error;
+}
+
 int git_repository_set_head(
 	git_repository* repo,
 	const char* refname)
@@ -2597,7 +2634,8 @@
 			error = git_reference_symbolic_create(&new_head, repo, GIT_HEAD_FILE,
 					git_reference_name(ref), true, git_buf_cstr(&log_message));
 		} else {
-			error = git_repository_set_head_detached(repo, git_reference_target(ref));
+			error = detach(repo, git_reference_target(ref),
+				git_reference_is_tag(ref) || git_reference_is_remote(ref) ? refname : NULL);
 		}
 	} else if (git_reference__is_branch(refname)) {
 		error = git_reference_symbolic_create(&new_head, repo, GIT_HEAD_FILE, refname,
@@ -2612,41 +2650,6 @@
 	return error;
 }
 
-static int detach(git_repository *repo, const git_oid *id, const char *from)
-{
-	int error;
-	git_buf log_message = GIT_BUF_INIT;
-	git_object *object = NULL, *peeled = NULL;
-	git_reference *new_head = NULL, *current = NULL;
-
-	assert(repo && id);
-
-	if ((error = git_reference_lookup(&current, repo, GIT_HEAD_FILE)) < 0)
-		return error;
-
-	if ((error = git_object_lookup(&object, repo, id, GIT_OBJ_ANY)) < 0)
-		goto cleanup;
-
-	if ((error = git_object_peel(&peeled, object, GIT_OBJ_COMMIT)) < 0)
-		goto cleanup;
-
-	if (from == NULL)
-		from = git_oid_tostr_s(git_object_id(peeled));
-
-	if ((error = checkout_message(&log_message, current, from)) < 0)
-		goto cleanup;
-
-	error = git_reference_create(&new_head, repo, GIT_HEAD_FILE, git_object_id(peeled), true, git_buf_cstr(&log_message));
-
-cleanup:
-	git_buf_free(&log_message);
-	git_object_free(object);
-	git_object_free(peeled);
-	git_reference_free(current);
-	git_reference_free(new_head);
-	return error;
-}
-
 int git_repository_set_head_detached(
 	git_repository* repo,
 	const git_oid* commitish)
diff --git a/src/revparse.c b/src/revparse.c
index d5511b4..fd6bd1e 100644
--- a/src/revparse.c
+++ b/src/revparse.c
@@ -892,6 +892,17 @@
 		const char *rstr;
 		revspec->flags = GIT_REVPARSE_RANGE;
 
+		/*
+		 * Following git.git, don't allow '..' because it makes command line
+		 * arguments which can be either paths or revisions ambiguous when the
+		 * path is almost certainly intended. The empty range '...' is still
+		 * allowed.
+		 */
+		if (!git__strcmp(spec, "..")) {
+			giterr_set(GITERR_INVALID, "Invalid pattern '..'");
+			return GIT_EINVALIDSPEC;
+		}
+
 		lstr = git__substrdup(spec, dotdot - spec);
 		rstr = dotdot + 2;
 		if (dotdot[2] == '.') {
@@ -899,9 +910,17 @@
 			rstr++;
 		}
 
-		error = git_revparse_single(&revspec->from, repo, lstr);
-		if (!error)
-			error = git_revparse_single(&revspec->to, repo, rstr);
+		error = git_revparse_single(
+			&revspec->from,
+			repo,
+			*lstr == '\0' ? "HEAD" : lstr);
+
+		if (!error) {
+			error = git_revparse_single(
+				&revspec->to,
+				repo,
+				*rstr == '\0' ? "HEAD" : rstr);
+		}
 
 		git__free((void*)lstr);
 	} else {
diff --git a/src/sysdir.c b/src/sysdir.c
index ed11221..9312a7e 100644
--- a/src/sysdir.c
+++ b/src/sysdir.c
@@ -275,3 +275,14 @@
 		path, NULL, GIT_SYSDIR_TEMPLATE, "template");
 }
 
+int git_sysdir_expand_global_file(git_buf *path, const char *filename)
+{
+	int error;
+
+	if ((error = git_sysdir_find_global_file(path, NULL)) == 0) {
+		if (filename)
+			error = git_buf_joinpath(path, path->ptr, filename);
+	}
+
+	return error;
+}
diff --git a/src/sysdir.h b/src/sysdir.h
index 1187898..79f2381 100644
--- a/src/sysdir.h
+++ b/src/sysdir.h
@@ -55,6 +55,18 @@
  */
 extern int git_sysdir_find_template_dir(git_buf *path);
 
+/**
+ * Expand the name of a "global" file (i.e. one in a user's home
+ * directory).  Unlike `find_global_file` (above), this makes no
+ * attempt to check for the existence of the file, and is useful if
+ * you want the full path regardless of existence.
+ *
+ * @param path buffer to write the full path into
+ * @param filename name of file in the home directory
+ * @return 0 on success or -1 on error
+ */
+extern int git_sysdir_expand_global_file(git_buf *path, const char *filename);
+
 typedef enum {
 	GIT_SYSDIR_SYSTEM = 0,
 	GIT_SYSDIR_GLOBAL = 1,
diff --git a/src/transports/http.c b/src/transports/http.c
index 949e857..cb4a6d0 100644
--- a/src/transports/http.c
+++ b/src/transports/http.c
@@ -575,6 +575,9 @@
 		if ((error = git_remote__get_http_proxy(t->owner->owner, !!t->connection_data.use_ssl, &url)) < 0)
 			return error;
 
+		opts.credentials = t->owner->proxy.credentials;
+		opts.certificate_check = t->owner->proxy.certificate_check;
+		opts.payload = t->owner->proxy.payload;
 		opts.type = GIT_PROXY_SPECIFIED;
 		opts.url = url;
 		error = git_stream_set_proxy(t->io, &opts);
diff --git a/tests/config/include.c b/tests/config/include.c
index 882b89b..0a07c9b 100644
--- a/tests/config/include.c
+++ b/tests/config/include.c
@@ -108,6 +108,26 @@
 	git_config_free(cfg);
 }
 
+void test_config_include__missing_homedir(void)
+{
+	git_config *cfg;
+	git_buf buf = GIT_BUF_INIT;
+
+	cl_git_pass(git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, cl_fixture("config")));
+	cl_git_mkfile("including", "[include]\npath = ~/.nonexistentfile\n[foo]\nbar = baz");
+
+	giterr_clear();
+	cl_git_pass(git_config_open_ondisk(&cfg, "including"));
+	cl_assert(giterr_last() == NULL);
+	cl_git_pass(git_config_get_string_buf(&buf, cfg, "foo.bar"));
+	cl_assert_equal_s("baz", git_buf_cstr(&buf));
+
+	git_buf_free(&buf);
+	git_config_free(cfg);
+
+	cl_sandbox_set_search_path_defaults();
+}
+
 #define replicate10(s) s s s s s s s s s s
 void test_config_include__depth2(void)
 {
diff --git a/tests/refs/revparse.c b/tests/refs/revparse.c
index c22c304..459188c 100644
--- a/tests/refs/revparse.c
+++ b/tests/refs/revparse.c
@@ -122,6 +122,14 @@
 	test_id_inrepo(spec, expected_left, expected_right, expected_flags, g_repo);
 }
 
+static void test_invalid_revspec(const char* invalid_spec)
+{
+	git_revspec revspec;
+
+	cl_assert_equal_i(
+		GIT_EINVALIDSPEC, git_revparse(&revspec, g_repo, invalid_spec));
+}
+
 void test_refs_revparse__initialize(void)
 {
 	cl_git_pass(git_repository_open(&g_repo, cl_fixture("testrepo.git")));
@@ -749,6 +757,33 @@
 		"4a202b346bb0fb0db7eff3cffeb3c70babbd2045",
 		"a65fedf39aefe402d3bb6e24df4d4f5fe4547750",
 		GIT_REVPARSE_RANGE | GIT_REVPARSE_MERGE_BASE);
+
+	test_id("HEAD~3..",
+		"4a202b346bb0fb0db7eff3cffeb3c70babbd2045",
+		"a65fedf39aefe402d3bb6e24df4d4f5fe4547750",
+		GIT_REVPARSE_RANGE);
+
+	test_id("HEAD~3...",
+		"4a202b346bb0fb0db7eff3cffeb3c70babbd2045",
+		"a65fedf39aefe402d3bb6e24df4d4f5fe4547750",
+		GIT_REVPARSE_RANGE | GIT_REVPARSE_MERGE_BASE);
+
+	test_id("..HEAD~3",
+		"a65fedf39aefe402d3bb6e24df4d4f5fe4547750",
+		"4a202b346bb0fb0db7eff3cffeb3c70babbd2045",
+		GIT_REVPARSE_RANGE);
+
+	test_id("...HEAD~3",
+		"a65fedf39aefe402d3bb6e24df4d4f5fe4547750",
+		"4a202b346bb0fb0db7eff3cffeb3c70babbd2045",
+		GIT_REVPARSE_RANGE | GIT_REVPARSE_MERGE_BASE);
+
+	test_id("...",
+		"a65fedf39aefe402d3bb6e24df4d4f5fe4547750",
+		"a65fedf39aefe402d3bb6e24df4d4f5fe4547750",
+		GIT_REVPARSE_RANGE | GIT_REVPARSE_MERGE_BASE);
+
+	test_invalid_revspec("..");
 }
 
 void test_refs_revparse__ext_retrieves_both_the_reference_and_its_target(void)
diff --git a/tests/repo/head.c b/tests/repo/head.c
index 31c2287..d021160 100644
--- a/tests/repo/head.c
+++ b/tests/repo/head.c
@@ -261,15 +261,19 @@
 	cl_git_pass(git_revparse_single(&tag, repo, "tags/test"));
 	cl_git_pass(git_repository_set_head_detached(repo, git_object_id(tag)));
 	cl_git_pass(git_repository_set_head(repo, "refs/heads/haacked"));
+	cl_git_pass(git_repository_set_head(repo, "refs/tags/test"));
+	cl_git_pass(git_repository_set_head(repo, "refs/remotes/test/master"));
 
-	test_reflog(repo, 2, NULL, "refs/heads/haacked", "foo@example.com", "checkout: moving from master to haacked");
-	test_reflog(repo, 1, NULL, "tags/test^{commit}", "foo@example.com", "checkout: moving from unborn to e90810b8df3e80c413d903f631643c716887138d");
-	test_reflog(repo, 0, "tags/test^{commit}", "refs/heads/haacked", "foo@example.com", "checkout: moving from e90810b8df3e80c413d903f631643c716887138d to haacked");
+	test_reflog(repo, 4, NULL, "refs/heads/haacked", "foo@example.com", "checkout: moving from master to haacked");
+	test_reflog(repo, 3, NULL, "tags/test^{commit}", "foo@example.com", "checkout: moving from unborn to e90810b8df3e80c413d903f631643c716887138d");
+	test_reflog(repo, 2, "tags/test^{commit}", "refs/heads/haacked", "foo@example.com", "checkout: moving from e90810b8df3e80c413d903f631643c716887138d to haacked");
+	test_reflog(repo, 1, "refs/heads/haacked", "tags/test^{commit}", "foo@example.com", "checkout: moving from haacked to test");
+	test_reflog(repo, 0, "tags/test^{commit}", "refs/remotes/test/master", "foo@example.com", "checkout: moving from e90810b8df3e80c413d903f631643c716887138d to test/master");
 
 	cl_git_pass(git_annotated_commit_from_revspec(&annotated, repo, "haacked~0"));
 	cl_git_pass(git_repository_set_head_detached_from_annotated(repo, annotated));
 
-	test_reflog(repo, 0, NULL, "refs/heads/haacked", "foo@example.com", "checkout: moving from haacked to haacked~0");
+	test_reflog(repo, 0, NULL, "refs/heads/haacked", "foo@example.com", "checkout: moving from be3563ae3f795b2b4353bcce3a527ad0a4f7f644 to haacked~0");
 
 	git_annotated_commit_free(annotated);
 	git_object_free(tag);