(alias R=$GIT_COMMON_DIR/repos/<id>) - linked checkouts are supposed to keep its location in $R/gitdir up to date. The use case is auto fixup after a manual checkout move. - linked checkouts are supposed to update mtime of $R/gitdir. If $R/gitdir's mtime is older than a limit, and it points to nowhere, repos/<id> is to be pruned. - If $R/locked exists, repos/<id> is not supposed to be pruned. If $R/locked exists and $R/gitdir's mtime is older than a really long limit, warn about old unused repo. - "git checkout --to" is supposed to make a hard link named $R/link pointing to the .git file on supported file systems to help detect the user manually deleting the checkout. If $R/link exists and its link count is greated than 1, the repo is kept. Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@xxxxxxxxx> --- Documentation/git-prune.txt | 3 + Documentation/gitrepository-layout.txt | 19 ++++++ builtin/checkout.c | 14 +++++ builtin/prune.c | 99 ++++++++++++++++++++++++++++++ setup.c | 13 ++++ t/t2026-prune-linked-checkouts.sh (new +x) | 84 +++++++++++++++++++++++++ 6 files changed, 232 insertions(+) create mode 100755 t/t2026-prune-linked-checkouts.sh diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt index 7a493c8..50e39ec 100644 --- a/Documentation/git-prune.txt +++ b/Documentation/git-prune.txt @@ -48,6 +48,9 @@ OPTIONS --expire <time>:: Only expire loose objects older than <time>. +--repos:: + Prune directories in $GIT_DIR/repos. + <head>...:: In addition to objects reachable from any of our references, keep objects diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt index 543d874..bed4f1a 100644 --- a/Documentation/gitrepository-layout.txt +++ b/Documentation/gitrepository-layout.txt @@ -256,6 +256,25 @@ repos:: $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/repos" will be used instead. +repos/<id>/gitdir:: + A text file containing the absolute path back to the .git file + that points to here. This is used to check if the linked + repository has been manually removed and there is no need to + keep this directory any more. mtime of this file should be + updated every time the linked repository is accessed. + +repos/<id>/locked:: + If this file exists, the linked repository may be on a + portable device and not available. It does not mean that the + linked repository is gone and `repos/<id>` could be + removed. The file's content contains a reason string on why + the repository is locked. + +repos/<id>/link:: + If this file exists, it is a hard link to the linked .git + file. It is used to detect if the linked repository is + manually removed. + SEE ALSO -------- linkgit:git-init[1], diff --git a/builtin/checkout.c b/builtin/checkout.c index fe24766..5b93f49 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -898,12 +898,22 @@ static int prepare_linked_checkout(const struct checkout_opts *opts, junk_git_dir = sb_repo.buf; is_junk = 1; + /* + * lock the incomplete repo so prune won't delete it, unlock + * after the preparation is over. + */ + strbuf_addf(&sb, "%s/locked", sb_repo.buf); + write_file(sb.buf, 1, "initializing\n"); + strbuf_addf(&sb_git, "%s/.git", path); if (safe_create_leading_directories_const(sb_git.buf)) die_errno(_("could not create leading directories of '%s'"), sb_git.buf); junk_work_tree = path; + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/gitdir", sb_repo.buf); + write_file(sb.buf, 1, "%s\n", real_path(sb_git.buf)); write_file(sb_git.buf, 1, "gitdir: %s/repos/%s\n", real_path(get_git_common_dir()), name); /* @@ -912,6 +922,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts, * value would do because this value will be ignored and * replaced at the next (real) checkout. */ + strbuf_reset(&sb); strbuf_addf(&sb, "%s/HEAD", sb_repo.buf); write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1)); strbuf_reset(&sb); @@ -930,6 +941,9 @@ static int prepare_linked_checkout(const struct checkout_opts *opts, ret = run_command(&cp); if (!ret) is_junk = 0; + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/locked", sb_repo.buf); + unlink_or_warn(sb.buf); strbuf_release(&sb); strbuf_release(&sb_repo); strbuf_release(&sb_git); diff --git a/builtin/prune.c b/builtin/prune.c index 144a3bd..28b7adf 100644 --- a/builtin/prune.c +++ b/builtin/prune.c @@ -112,6 +112,95 @@ static void prune_object_dir(const char *path) } } +static int prune_repo_dir(const char *id, struct stat *st, struct strbuf *reason) +{ + char *path; + int fd, len; + + if (!is_directory(git_path("repos/%s", id))) { + strbuf_addf(reason, _("Removing repos/%s: not a valid directory"), id); + return 1; + } + if (file_exists(git_path("repos/%s/locked", id))) + return 0; + if (stat(git_path("repos/%s/gitdir", id), st)) { + st->st_mtime = expire; + strbuf_addf(reason, _("Removing repos/%s: gitdir file does not exist"), id); + return 1; + } + fd = open(git_path("repos/%s/gitdir", id), O_RDONLY); + if (fd < 0) { + st->st_mtime = expire; + strbuf_addf(reason, _("Removing repos/%s: unable to read gitdir file (%s)"), + id, strerror(errno)); + return 1; + } + len = st->st_size; + path = xmalloc(len + 1); + read_in_full(fd, path, len); + close(fd); + while (len && (path[len - 1] == '\n' || path[len - 1] == '\r')) + len--; + if (!len) { + st->st_mtime = expire; + strbuf_addf(reason, _("Removing repos/%s: invalid gitdir file"), id); + free(path); + return 1; + } + path[len] = '\0'; + if (!file_exists(path)) { + struct stat st_link; + free(path); + /* + * the repo is moved manually and has not been + * accessed since? + */ + if (!stat(git_path("repos/%s/link", id), &st_link) && + st_link.st_nlink > 1) + return 0; + strbuf_addf(reason, _("Removing repos/%s: gitdir file points to non-existent location"), id); + return 1; + } + free(path); + return 0; +} + +static void prune_repos_dir(void) +{ + struct strbuf reason = STRBUF_INIT; + struct strbuf path = STRBUF_INIT; + DIR *dir = opendir(git_path("repos")); + struct dirent *d; + int ret; + struct stat st; + if (!dir) + return; + while ((d = readdir(dir)) != NULL) { + if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) + continue; + strbuf_reset(&reason); + if (!prune_repo_dir(d->d_name, &st, &reason) || + st.st_mtime > expire) + continue; + if (show_only || verbose) + printf("%s\n", reason.buf); + if (show_only) + continue; + strbuf_reset(&path); + strbuf_addstr(&path, git_path("repos/%s", d->d_name)); + ret = remove_dir_recursively(&path, 0); + if (ret < 0 && errno == ENOTDIR) + ret = unlink(path.buf); + if (ret) + error(_("failed to remove: %s"), strerror(errno)); + } + closedir(dir); + if (!show_only) + rmdir(git_path("repos")); + strbuf_release(&reason); + strbuf_release(&path); +} + /* * Write errors (particularly out of space) can result in * failed temporary packs (and more rarely indexes and other @@ -138,10 +227,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix) { struct rev_info revs; struct progress *progress = NULL; + int prune_repos = 0; const struct option options[] = { OPT__DRY_RUN(&show_only, N_("do not remove, show only")), OPT__VERBOSE(&verbose, N_("report pruned objects")), OPT_BOOL(0, "progress", &show_progress, N_("show progress")), + OPT_BOOL(0, "repos", &prune_repos, N_("prune .git/repos/")), OPT_EXPIRY_DATE(0, "expire", &expire, N_("expire objects older than <time>")), OPT_END() @@ -154,6 +245,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix) init_revisions(&revs, prefix); argc = parse_options(argc, argv, prefix, options, prune_usage, 0); + + if (prune_repos) { + if (argc) + die(_("--repos does not take extra arguments")); + prune_repos_dir(); + return 0; + } + while (argc--) { unsigned char sha1[20]; const char *name = *argv++; diff --git a/setup.c b/setup.c index 8f90bc3..da2d669 100644 --- a/setup.c +++ b/setup.c @@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok) return ret; } +static void update_linked_gitdir(const char *gitfile, const char *gitdir) +{ + struct strbuf path = STRBUF_INIT; + struct stat st; + + strbuf_addf(&path, "%s/gitfile", gitdir); + if (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL)) + write_file(path.buf, 0, "%s\n", gitfile); + strbuf_release(&path); +} + /* * Try to read the location of the git directory from the .git file, * return path to git directory if found. @@ -438,6 +449,8 @@ const char *read_gitfile(const char *path) if (!is_git_directory(dir)) die("Not a git repository: %s", dir); + + update_linked_gitdir(path, dir); path = real_path(dir); free(buf); diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh new file mode 100755 index 0000000..4ccfa4e --- /dev/null +++ b/t/t2026-prune-linked-checkouts.sh @@ -0,0 +1,84 @@ +#!/bin/sh + +test_description='prune $GIT_DIR/repos' + +. ./test-lib.sh + +test_expect_success 'prune --repos on normal repo' ' + git prune --repos && + test_must_fail git prune --repos abc +' + +test_expect_success 'prune files inside $GIT_DIR/repos' ' + mkdir .git/repos && + : >.git/repos/abc && + git prune --repos --verbose >actual && + cat >expect <<EOF && +Removing repos/abc: not a valid directory +EOF + test_i18ncmp expect actual && + ! test -f .git/repos/abc && + ! test -d .git/repos +' + +test_expect_success 'prune directories without gitdir' ' + mkdir -p .git/repos/def/abc && + : >.git/repos/def/def && + cat >expect <<EOF && +Removing repos/def: gitdir file does not exist +EOF + git prune --repos --verbose >actual && + test_i18ncmp expect actual && + ! test -d .git/repos/def && + ! test -d .git/repos +' + +test_expect_success POSIXPERM 'prune directories with unreadable gitdir' ' + mkdir -p .git/repos/def/abc && + : >.git/repos/def/def && + : >.git/repos/def/gitdir && + chmod u-r .git/repos/def/gitdir && + git prune --repos --verbose >actual && + test_i18ngrep "Removing repos/def: unable to read gitdir file" actual && + ! test -d .git/repos/def && + ! test -d .git/repos +' + +test_expect_success 'prune directories with invalid gitdir' ' + mkdir -p .git/repos/def/abc && + : >.git/repos/def/def && + : >.git/repos/def/gitdir && + git prune --repos --verbose >actual && + test_i18ngrep "Removing repos/def: invalid gitdir file" actual && + ! test -d .git/repos/def && + ! test -d .git/repos +' + +test_expect_success 'prune directories with gitdir pointing to nowhere' ' + mkdir -p .git/repos/def/abc && + : >.git/repos/def/def && + echo "$TRASH_DIRECTORY"/nowhere >.git/repos/def/gitdir && + git prune --repos --verbose >actual && + test_i18ngrep "Removing repos/def: gitdir file points to non-existent location" actual && + ! test -d .git/repos/def && + ! test -d .git/repos +' + +test_expect_success 'not prune locked checkout' ' + test_when_finished rm -r .git/repos + mkdir -p .git/repos/ghi && + : >.git/repos/ghi/locked && + git prune --repos && + test -d .git/repos/ghi +' + +test_expect_success 'not prune recent checkouts' ' + test_when_finished rm -r .git/repos + mkdir zz && + mkdir -p .git/repos/jlm && + echo "$TRASH_DIRECTORY"/zz >.git/repos/jlm/gitdir && + git prune --repos --verbose && + test -d .git/repos/jlm +' + +test_done -- 1.9.1.346.ga2b5940 -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html