Commit 4386d80be108102548d4ff52c875aedfa94e7412

Carlos Martín Nieto 2013-12-21T17:18:21

clone: perform a "local clone" when given a local path When git is given such a path, it will perform a "local clone", bypassing the git-aware protocol and simply copying over all objects that exist in the source. Copy this behaviour when given a local path.

diff --git a/include/git2/clone.h b/include/git2/clone.h
index 985c04b..ceb1a53 100644
--- a/include/git2/clone.h
+++ b/include/git2/clone.h
@@ -123,6 +123,31 @@ GIT_EXTERN(int) git_clone_into(
 	const char *branch,
 	const git_signature *signature);
 
+/**
+ * Perform a local clone into a repository
+ *
+ * A "local clone" bypasses any git-aware protocols and simply copies
+ * over the object database from the source repository. It is often
+ * faster than a git-aware clone, but no verification of the data is
+ * performed, and can copy over too much data.
+ *
+ * @param repo the repository to use
+ * @param remote the remote repository to clone from
+ * @param co_opts options to use during checkout
+ * @param branch the branch to checkout after the clone, pass NULL for the
+ *        remote's default branch
+ * @param signature the identity used when updating the reflog
+ * @return 0 on success, any non-zero return value from a callback
+ *         function, or a negative value to indicate an error (use
+ *         `giterr_last` for a detailed error message)
+ */
+GIT_EXTERN(int) git_clone_local_into(
+	git_repository *repo,
+	git_remote *remote,
+	const git_checkout_options *co_opts,
+	const char *branch,
+	const git_signature *signature);
+
 /** @} */
 GIT_END_DECL
 #endif
diff --git a/src/clone.c b/src/clone.c
index 8381ec6..b66ba6b 100644
--- a/src/clone.c
+++ b/src/clone.c
@@ -22,6 +22,7 @@
 #include "refs.h"
 #include "path.h"
 #include "repository.h"
+#include "odb.h"
 
 static int create_branch(
 	git_reference **branch,
@@ -280,6 +281,23 @@ static bool should_checkout(
 	return !git_repository_head_unborn(repo);
 }
 
+static int checkout_branch(git_repository *repo, git_remote *remote, const git_checkout_options *co_opts, const char *branch, const git_signature *signature, const char *reflog_message)
+{
+	int error;
+
+	if (branch)
+		error = update_head_to_branch(repo, git_remote_name(remote), branch,
+				signature, reflog_message);
+	/* Point HEAD to the same ref as the remote's head */
+	else
+		error = update_head_to_remote(repo, remote, signature, reflog_message);
+
+	if (!error && should_checkout(repo, git_repository_is_bare(repo), co_opts))
+		error = git_checkout_head(repo, co_opts);
+
+	return error;
+}
+
 int git_clone_into(git_repository *repo, git_remote *_remote, const git_checkout_options *co_opts, const char *branch, const git_signature *signature)
 {
 	int error;
@@ -311,15 +329,7 @@ int git_clone_into(git_repository *repo, git_remote *_remote, const git_checkout
 	if ((error = git_remote_fetch(remote, signature, git_buf_cstr(&reflog_message))) != 0)
 		goto cleanup;
 
-	if (branch)
-		error = update_head_to_branch(repo, git_remote_name(remote), branch,
-				signature, git_buf_cstr(&reflog_message));
-	/* Point HEAD to the same ref as the remote's head */
-	else
-		error = update_head_to_remote(repo, remote, signature, git_buf_cstr(&reflog_message));
-
-	if (!error && should_checkout(repo, git_repository_is_bare(repo), co_opts))
-		error = git_checkout_head(repo, co_opts);
+	error = checkout_branch(repo, remote, co_opts, branch, signature, git_buf_cstr(&reflog_message));
 
 cleanup:
 	git_remote_free(remote);
@@ -362,8 +372,15 @@ int git_clone(
 		return error;
 
 	if (!(error = create_and_configure_origin(&origin, repo, url, &options))) {
-		error = git_clone_into(
-			repo, origin, &options.checkout_opts, options.checkout_branch, options.signature);
+		if (git__prefixcmp(url, "file://")) {
+			error = git_clone_local_into(
+				repo, origin, &options.checkout_opts,
+				options.checkout_branch, options.signature);
+		} else {
+			error = git_clone_into(
+				repo, origin, &options.checkout_opts,
+				options.checkout_branch, options.signature);
+		}
 
 		git_remote_free(origin);
 	}
@@ -390,3 +407,78 @@ int git_clone_init_options(git_clone_options *opts, unsigned int version)
 		opts, version, git_clone_options, GIT_CLONE_OPTIONS_INIT);
 	return 0;
 }
+
+static const char *repository_base(git_repository *repo)
+{
+	if (git_repository_is_bare(repo))
+		return git_repository_path(repo);
+
+	return git_repository_workdir(repo);
+}
+
+int git_clone_local_into(git_repository *repo, git_remote *remote, const git_checkout_options *co_opts, const char *branch, const git_signature *signature)
+{
+	int error, root;
+	git_repository *src;
+	git_buf src_odb = GIT_BUF_INIT, dst_odb = GIT_BUF_INIT, src_path = GIT_BUF_INIT;
+	git_buf reflog_message = GIT_BUF_INIT;
+	const char *url;
+
+	assert(repo && remote && co_opts);
+
+	if (!git_repository_is_empty(repo)) {
+		giterr_set(GITERR_INVALID, "the repository is not empty");
+		return -1;
+	}
+
+	/*
+	 * Let's figure out what path we should use for the source
+	 * repo, if it's not rooted, the path should be relative to
+	 * the repository's worktree/gitdir.
+	 */
+	url = git_remote_url(remote);
+	if (!git__prefixcmp(url, "file://"))
+		root = strlen("file://");
+	else
+		root = git_path_root(url);
+
+	if (root >= 0)
+		git_buf_puts(&src_path, url + root);
+	else
+		git_buf_joinpath(&src_path, repository_base(repo), url);
+
+	if (git_buf_oom(&src_path))
+		return -1;
+
+	/* Copy .git/objects/ from the source to the target */
+	if ((error = git_repository_open(&src, git_buf_cstr(&src_path))) < 0) {
+		git_buf_free(&src_path);
+		return error;
+	}
+
+	git_buf_joinpath(&src_odb, git_repository_path(src), GIT_OBJECTS_DIR);
+	git_buf_joinpath(&dst_odb, git_repository_path(repo), GIT_OBJECTS_DIR);
+	if (git_buf_oom(&src_odb) || git_buf_oom(&dst_odb)) {
+		error = -1;
+		goto cleanup;
+	}
+
+	if ((error = git_futils_cp_r(git_buf_cstr(&src_odb), git_buf_cstr(&dst_odb),
+				     0, GIT_OBJECT_DIR_MODE)) < 0)
+		goto cleanup;
+
+	git_buf_printf(&reflog_message, "clone: from %s", git_remote_url(remote));
+
+	if ((error = git_remote_fetch(remote, signature, git_buf_cstr(&reflog_message))) != 0)
+		goto cleanup;
+
+	error = checkout_branch(repo, remote, co_opts, branch, signature, git_buf_cstr(&reflog_message));
+
+cleanup:
+	git_buf_free(&reflog_message);
+	git_buf_free(&src_path);
+	git_buf_free(&src_odb);
+	git_buf_free(&dst_odb);
+	git_repository_free(src);
+	return error;
+}