mirror of
https://github.com/git/git.git
synced 2024-11-20 18:34:07 +01:00
3e8aded203
In repos with many refs, it is unlikely that most refs will ever change. This fact is already exploited by "git gc" by executing "git pack-refs" to consolidate all refs into a single file. When cloning a repo with many refs, it does not make sense to create the loose refs in the first place, just to have the next "git gc" consolidate them into one file. Instead, make "git clone" create the packed refs file immediately, and forego the loose refs completely. Signed-off-by: Johan Herland <johan@herland.net> Acked-by: Daniel Barkalow <barkalow@iabervon.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>
558 lines
14 KiB
C
558 lines
14 KiB
C
/*
|
|
* Builtin "git clone"
|
|
*
|
|
* Copyright (c) 2007 Kristian Høgsberg <krh@redhat.com>,
|
|
* 2008 Daniel Barkalow <barkalow@iabervon.org>
|
|
* Based on git-commit.sh by Junio C Hamano and Linus Torvalds
|
|
*
|
|
* Clone a repository into a different directory that does not yet exist.
|
|
*/
|
|
|
|
#include "cache.h"
|
|
#include "parse-options.h"
|
|
#include "fetch-pack.h"
|
|
#include "refs.h"
|
|
#include "tree.h"
|
|
#include "tree-walk.h"
|
|
#include "unpack-trees.h"
|
|
#include "transport.h"
|
|
#include "strbuf.h"
|
|
#include "dir.h"
|
|
#include "pack-refs.h"
|
|
|
|
/*
|
|
* Overall FIXMEs:
|
|
* - respect DB_ENVIRONMENT for .git/objects.
|
|
*
|
|
* Implementation notes:
|
|
* - dropping use-separate-remote and no-separate-remote compatibility
|
|
*
|
|
*/
|
|
static const char * const builtin_clone_usage[] = {
|
|
"git-clone [options] [--] <repo> [<dir>]",
|
|
NULL
|
|
};
|
|
|
|
static int option_quiet, option_no_checkout, option_bare;
|
|
static int option_local, option_no_hardlinks, option_shared;
|
|
static char *option_template, *option_reference, *option_depth;
|
|
static char *option_origin = NULL;
|
|
static char *option_upload_pack = "git-upload-pack";
|
|
|
|
static struct option builtin_clone_options[] = {
|
|
OPT__QUIET(&option_quiet),
|
|
OPT_BOOLEAN('n', "no-checkout", &option_no_checkout,
|
|
"don't create a checkout"),
|
|
OPT_BOOLEAN(0, "bare", &option_bare, "create a bare repository"),
|
|
OPT_BOOLEAN(0, "naked", &option_bare, "create a bare repository"),
|
|
OPT_BOOLEAN('l', "local", &option_local,
|
|
"to clone from a local repository"),
|
|
OPT_BOOLEAN(0, "no-hardlinks", &option_no_hardlinks,
|
|
"don't use local hardlinks, always copy"),
|
|
OPT_BOOLEAN('s', "shared", &option_shared,
|
|
"setup as shared repository"),
|
|
OPT_STRING(0, "template", &option_template, "path",
|
|
"path the template repository"),
|
|
OPT_STRING(0, "reference", &option_reference, "repo",
|
|
"reference repository"),
|
|
OPT_STRING('o', "origin", &option_origin, "branch",
|
|
"use <branch> instead or 'origin' to track upstream"),
|
|
OPT_STRING('u', "upload-pack", &option_upload_pack, "path",
|
|
"path to git-upload-pack on the remote"),
|
|
OPT_STRING(0, "depth", &option_depth, "depth",
|
|
"create a shallow clone of that depth"),
|
|
|
|
OPT_END()
|
|
};
|
|
|
|
static char *get_repo_path(const char *repo, int *is_bundle)
|
|
{
|
|
static char *suffix[] = { "/.git", ".git", "" };
|
|
static char *bundle_suffix[] = { ".bundle", "" };
|
|
struct stat st;
|
|
int i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(suffix); i++) {
|
|
const char *path;
|
|
path = mkpath("%s%s", repo, suffix[i]);
|
|
if (!stat(path, &st) && S_ISDIR(st.st_mode)) {
|
|
*is_bundle = 0;
|
|
return xstrdup(make_nonrelative_path(path));
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < ARRAY_SIZE(bundle_suffix); i++) {
|
|
const char *path;
|
|
path = mkpath("%s%s", repo, bundle_suffix[i]);
|
|
if (!stat(path, &st) && S_ISREG(st.st_mode)) {
|
|
*is_bundle = 1;
|
|
return xstrdup(make_nonrelative_path(path));
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static char *guess_dir_name(const char *repo, int is_bundle)
|
|
{
|
|
const char *p, *start, *end, *limit;
|
|
int after_slash_or_colon;
|
|
|
|
/* Guess dir name from repository: strip trailing '/',
|
|
* strip trailing '[:/]*.{git,bundle}', strip leading '.*[/:]'. */
|
|
|
|
after_slash_or_colon = 1;
|
|
limit = repo + strlen(repo);
|
|
start = repo;
|
|
end = limit;
|
|
for (p = repo; p < limit; p++) {
|
|
const char *prefix = is_bundle ? ".bundle" : ".git";
|
|
if (!prefixcmp(p, prefix)) {
|
|
if (!after_slash_or_colon)
|
|
end = p;
|
|
p += strlen(prefix) - 1;
|
|
} else if (!prefixcmp(p, ".bundle")) {
|
|
if (!after_slash_or_colon)
|
|
end = p;
|
|
p += 7;
|
|
} else if (*p == '/' || *p == ':') {
|
|
if (end == limit)
|
|
end = p;
|
|
after_slash_or_colon = 1;
|
|
} else if (after_slash_or_colon) {
|
|
start = p;
|
|
end = limit;
|
|
after_slash_or_colon = 0;
|
|
}
|
|
}
|
|
|
|
return xstrndup(start, end - start);
|
|
}
|
|
|
|
static int is_directory(const char *path)
|
|
{
|
|
struct stat buf;
|
|
|
|
return !stat(path, &buf) && S_ISDIR(buf.st_mode);
|
|
}
|
|
|
|
static void setup_reference(const char *repo)
|
|
{
|
|
const char *ref_git;
|
|
char *ref_git_copy;
|
|
|
|
struct remote *remote;
|
|
struct transport *transport;
|
|
const struct ref *extra;
|
|
|
|
ref_git = make_absolute_path(option_reference);
|
|
|
|
if (is_directory(mkpath("%s/.git/objects", ref_git)))
|
|
ref_git = mkpath("%s/.git", ref_git);
|
|
else if (!is_directory(mkpath("%s/objects", ref_git)))
|
|
die("reference repository '%s' is not a local directory.",
|
|
option_reference);
|
|
|
|
ref_git_copy = xstrdup(ref_git);
|
|
|
|
add_to_alternates_file(ref_git_copy);
|
|
|
|
remote = remote_get(ref_git_copy);
|
|
transport = transport_get(remote, ref_git_copy);
|
|
for (extra = transport_get_remote_refs(transport); extra;
|
|
extra = extra->next)
|
|
add_extra_ref(extra->name, extra->old_sha1, 0);
|
|
|
|
transport_disconnect(transport);
|
|
|
|
free(ref_git_copy);
|
|
}
|
|
|
|
static void copy_or_link_directory(char *src, char *dest)
|
|
{
|
|
struct dirent *de;
|
|
struct stat buf;
|
|
int src_len, dest_len;
|
|
DIR *dir;
|
|
|
|
dir = opendir(src);
|
|
if (!dir)
|
|
die("failed to open %s\n", src);
|
|
|
|
if (mkdir(dest, 0777)) {
|
|
if (errno != EEXIST)
|
|
die("failed to create directory %s\n", dest);
|
|
else if (stat(dest, &buf))
|
|
die("failed to stat %s\n", dest);
|
|
else if (!S_ISDIR(buf.st_mode))
|
|
die("%s exists and is not a directory\n", dest);
|
|
}
|
|
|
|
src_len = strlen(src);
|
|
src[src_len] = '/';
|
|
dest_len = strlen(dest);
|
|
dest[dest_len] = '/';
|
|
|
|
while ((de = readdir(dir)) != NULL) {
|
|
strcpy(src + src_len + 1, de->d_name);
|
|
strcpy(dest + dest_len + 1, de->d_name);
|
|
if (stat(src, &buf)) {
|
|
warning ("failed to stat %s\n", src);
|
|
continue;
|
|
}
|
|
if (S_ISDIR(buf.st_mode)) {
|
|
if (de->d_name[0] != '.')
|
|
copy_or_link_directory(src, dest);
|
|
continue;
|
|
}
|
|
|
|
if (unlink(dest) && errno != ENOENT)
|
|
die("failed to unlink %s\n", dest);
|
|
if (!option_no_hardlinks) {
|
|
if (!link(src, dest))
|
|
continue;
|
|
if (option_local)
|
|
die("failed to create link %s\n", dest);
|
|
option_no_hardlinks = 1;
|
|
}
|
|
if (copy_file(dest, src, 0666))
|
|
die("failed to copy file to %s\n", dest);
|
|
}
|
|
closedir(dir);
|
|
}
|
|
|
|
static const struct ref *clone_local(const char *src_repo,
|
|
const char *dest_repo)
|
|
{
|
|
const struct ref *ret;
|
|
char src[PATH_MAX];
|
|
char dest[PATH_MAX];
|
|
struct remote *remote;
|
|
struct transport *transport;
|
|
|
|
if (option_shared)
|
|
add_to_alternates_file(src_repo);
|
|
else {
|
|
snprintf(src, PATH_MAX, "%s/objects", src_repo);
|
|
snprintf(dest, PATH_MAX, "%s/objects", dest_repo);
|
|
copy_or_link_directory(src, dest);
|
|
}
|
|
|
|
remote = remote_get(src_repo);
|
|
transport = transport_get(remote, src_repo);
|
|
ret = transport_get_remote_refs(transport);
|
|
transport_disconnect(transport);
|
|
return ret;
|
|
}
|
|
|
|
static const char *junk_work_tree;
|
|
static const char *junk_git_dir;
|
|
pid_t junk_pid;
|
|
|
|
static void remove_junk(void)
|
|
{
|
|
struct strbuf sb;
|
|
if (getpid() != junk_pid)
|
|
return;
|
|
strbuf_init(&sb, 0);
|
|
if (junk_git_dir) {
|
|
strbuf_addstr(&sb, junk_git_dir);
|
|
remove_dir_recursively(&sb, 0);
|
|
strbuf_reset(&sb);
|
|
}
|
|
if (junk_work_tree) {
|
|
strbuf_addstr(&sb, junk_work_tree);
|
|
remove_dir_recursively(&sb, 0);
|
|
strbuf_reset(&sb);
|
|
}
|
|
}
|
|
|
|
static void remove_junk_on_signal(int signo)
|
|
{
|
|
remove_junk();
|
|
signal(SIGINT, SIG_DFL);
|
|
raise(signo);
|
|
}
|
|
|
|
static const struct ref *locate_head(const struct ref *refs,
|
|
const struct ref *mapped_refs,
|
|
const struct ref **remote_head_p)
|
|
{
|
|
const struct ref *remote_head = NULL;
|
|
const struct ref *remote_master = NULL;
|
|
const struct ref *r;
|
|
for (r = refs; r; r = r->next)
|
|
if (!strcmp(r->name, "HEAD"))
|
|
remote_head = r;
|
|
|
|
for (r = mapped_refs; r; r = r->next)
|
|
if (!strcmp(r->name, "refs/heads/master"))
|
|
remote_master = r;
|
|
|
|
if (remote_head_p)
|
|
*remote_head_p = remote_head;
|
|
|
|
/* If there's no HEAD value at all, never mind. */
|
|
if (!remote_head)
|
|
return NULL;
|
|
|
|
/* If refs/heads/master could be right, it is. */
|
|
if (remote_master && !hashcmp(remote_master->old_sha1,
|
|
remote_head->old_sha1))
|
|
return remote_master;
|
|
|
|
/* Look for another ref that points there */
|
|
for (r = mapped_refs; r; r = r->next)
|
|
if (r != remote_head &&
|
|
!hashcmp(r->old_sha1, remote_head->old_sha1))
|
|
return r;
|
|
|
|
/* Nothing is the same */
|
|
return NULL;
|
|
}
|
|
|
|
static struct ref *write_remote_refs(const struct ref *refs,
|
|
struct refspec *refspec, const char *reflog)
|
|
{
|
|
struct ref *local_refs = NULL;
|
|
struct ref **tail = &local_refs;
|
|
struct ref *r;
|
|
|
|
get_fetch_map(refs, refspec, &tail, 0);
|
|
get_fetch_map(refs, tag_refspec, &tail, 0);
|
|
|
|
for (r = local_refs; r; r = r->next)
|
|
add_extra_ref(r->peer_ref->name, r->old_sha1, 0);
|
|
|
|
pack_refs(PACK_REFS_ALL);
|
|
clear_extra_refs();
|
|
|
|
return local_refs;
|
|
}
|
|
|
|
int cmd_clone(int argc, const char **argv, const char *prefix)
|
|
{
|
|
int use_local_hardlinks = 1;
|
|
int use_separate_remote = 1;
|
|
int is_bundle = 0;
|
|
struct stat buf;
|
|
const char *repo_name, *repo, *work_tree, *git_dir;
|
|
char *path, *dir;
|
|
const struct ref *refs, *head_points_at, *remote_head, *mapped_refs;
|
|
char branch_top[256], key[256], value[256];
|
|
struct strbuf reflog_msg;
|
|
|
|
struct refspec refspec;
|
|
|
|
junk_pid = getpid();
|
|
|
|
argc = parse_options(argc, argv, builtin_clone_options,
|
|
builtin_clone_usage, 0);
|
|
|
|
if (argc == 0)
|
|
die("You must specify a repository to clone.");
|
|
|
|
if (option_no_hardlinks)
|
|
use_local_hardlinks = 0;
|
|
|
|
if (option_bare) {
|
|
if (option_origin)
|
|
die("--bare and --origin %s options are incompatible.",
|
|
option_origin);
|
|
option_no_checkout = 1;
|
|
use_separate_remote = 0;
|
|
}
|
|
|
|
if (!option_origin)
|
|
option_origin = "origin";
|
|
|
|
repo_name = argv[0];
|
|
|
|
path = get_repo_path(repo_name, &is_bundle);
|
|
if (path)
|
|
repo = path;
|
|
else if (!strchr(repo_name, ':'))
|
|
repo = xstrdup(make_absolute_path(repo_name));
|
|
else
|
|
repo = repo_name;
|
|
|
|
if (argc == 2)
|
|
dir = xstrdup(argv[1]);
|
|
else
|
|
dir = guess_dir_name(repo_name, is_bundle);
|
|
|
|
if (!stat(dir, &buf))
|
|
die("destination directory '%s' already exists.", dir);
|
|
|
|
strbuf_init(&reflog_msg, 0);
|
|
strbuf_addf(&reflog_msg, "clone: from %s", repo);
|
|
|
|
if (option_bare)
|
|
work_tree = NULL;
|
|
else {
|
|
work_tree = getenv("GIT_WORK_TREE");
|
|
if (work_tree && !stat(work_tree, &buf))
|
|
die("working tree '%s' already exists.", work_tree);
|
|
}
|
|
|
|
if (option_bare || work_tree)
|
|
git_dir = xstrdup(dir);
|
|
else {
|
|
work_tree = dir;
|
|
git_dir = xstrdup(mkpath("%s/.git", dir));
|
|
}
|
|
|
|
if (!option_bare) {
|
|
junk_work_tree = work_tree;
|
|
if (mkdir(work_tree, 0755))
|
|
die("could not create work tree dir '%s'.", work_tree);
|
|
set_git_work_tree(work_tree);
|
|
}
|
|
junk_git_dir = git_dir;
|
|
atexit(remove_junk);
|
|
signal(SIGINT, remove_junk_on_signal);
|
|
|
|
setenv(CONFIG_ENVIRONMENT, xstrdup(mkpath("%s/config", git_dir)), 1);
|
|
|
|
set_git_dir(make_absolute_path(git_dir));
|
|
|
|
fprintf(stderr, "Initialize %s\n", git_dir);
|
|
init_db(option_template, option_quiet ? INIT_DB_QUIET : 0);
|
|
|
|
if (option_reference)
|
|
setup_reference(git_dir);
|
|
|
|
git_config(git_default_config, NULL);
|
|
|
|
if (option_bare) {
|
|
strcpy(branch_top, "refs/heads/");
|
|
|
|
git_config_set("core.bare", "true");
|
|
} else {
|
|
snprintf(branch_top, sizeof(branch_top),
|
|
"refs/remotes/%s/", option_origin);
|
|
|
|
/* Configure the remote */
|
|
snprintf(key, sizeof(key), "remote.%s.url", option_origin);
|
|
git_config_set(key, repo);
|
|
|
|
snprintf(key, sizeof(key), "remote.%s.fetch", option_origin);
|
|
snprintf(value, sizeof(value),
|
|
"+refs/heads/*:%s*", branch_top);
|
|
git_config_set_multivar(key, value, "^$", 0);
|
|
}
|
|
|
|
refspec.force = 0;
|
|
refspec.pattern = 1;
|
|
refspec.src = "refs/heads/";
|
|
refspec.dst = branch_top;
|
|
|
|
if (path && !is_bundle)
|
|
refs = clone_local(path, git_dir);
|
|
else {
|
|
struct remote *remote = remote_get(argv[0]);
|
|
struct transport *transport = transport_get(remote, argv[0]);
|
|
|
|
if (!transport->get_refs_list || !transport->fetch)
|
|
die("Don't know how to clone %s", transport->url);
|
|
|
|
transport_set_option(transport, TRANS_OPT_KEEP, "yes");
|
|
|
|
if (option_depth)
|
|
transport_set_option(transport, TRANS_OPT_DEPTH,
|
|
option_depth);
|
|
|
|
if (option_quiet)
|
|
transport->verbose = -1;
|
|
|
|
refs = transport_get_remote_refs(transport);
|
|
transport_fetch_refs(transport, refs);
|
|
}
|
|
|
|
clear_extra_refs();
|
|
|
|
mapped_refs = write_remote_refs(refs, &refspec, reflog_msg.buf);
|
|
|
|
head_points_at = locate_head(refs, mapped_refs, &remote_head);
|
|
|
|
if (head_points_at) {
|
|
/* Local default branch link */
|
|
create_symref("HEAD", head_points_at->name, NULL);
|
|
|
|
if (!option_bare) {
|
|
struct strbuf head_ref;
|
|
const char *head = head_points_at->name;
|
|
|
|
if (!prefixcmp(head, "refs/heads/"))
|
|
head += 11;
|
|
|
|
/* Set up the initial local branch */
|
|
|
|
/* Local branch initial value */
|
|
update_ref(reflog_msg.buf, "HEAD",
|
|
head_points_at->old_sha1,
|
|
NULL, 0, DIE_ON_ERR);
|
|
|
|
strbuf_init(&head_ref, 0);
|
|
strbuf_addstr(&head_ref, branch_top);
|
|
strbuf_addstr(&head_ref, "HEAD");
|
|
|
|
/* Remote branch link */
|
|
create_symref(head_ref.buf,
|
|
head_points_at->peer_ref->name,
|
|
reflog_msg.buf);
|
|
|
|
snprintf(key, sizeof(key), "branch.%s.remote", head);
|
|
git_config_set(key, option_origin);
|
|
snprintf(key, sizeof(key), "branch.%s.merge", head);
|
|
git_config_set(key, head_points_at->name);
|
|
}
|
|
} else if (remote_head) {
|
|
/* Source had detached HEAD pointing somewhere. */
|
|
if (!option_bare)
|
|
update_ref(reflog_msg.buf, "HEAD",
|
|
remote_head->old_sha1,
|
|
NULL, REF_NODEREF, DIE_ON_ERR);
|
|
} else {
|
|
/* Nothing to checkout out */
|
|
if (!option_no_checkout)
|
|
warning("remote HEAD refers to nonexistent ref, "
|
|
"unable to checkout.\n");
|
|
option_no_checkout = 1;
|
|
}
|
|
|
|
if (!option_no_checkout) {
|
|
struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file));
|
|
struct unpack_trees_options opts;
|
|
struct tree *tree;
|
|
struct tree_desc t;
|
|
int fd;
|
|
|
|
/* We need to be in the new work tree for the checkout */
|
|
setup_work_tree();
|
|
|
|
fd = hold_locked_index(lock_file, 1);
|
|
|
|
memset(&opts, 0, sizeof opts);
|
|
opts.update = 1;
|
|
opts.merge = 1;
|
|
opts.fn = oneway_merge;
|
|
opts.verbose_update = !option_quiet;
|
|
opts.src_index = &the_index;
|
|
opts.dst_index = &the_index;
|
|
|
|
tree = parse_tree_indirect(remote_head->old_sha1);
|
|
parse_tree(tree);
|
|
init_tree_desc(&t, tree->buffer, tree->size);
|
|
unpack_trees(1, &t, &opts);
|
|
|
|
if (write_cache(fd, active_cache, active_nr) ||
|
|
commit_locked_index(lock_file))
|
|
die("unable to write new index file");
|
|
}
|
|
|
|
strbuf_release(&reflog_msg);
|
|
junk_pid = 0;
|
|
return 0;
|
|
}
|