(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.

 - "git checkout --to" is supposed to create $R/locked if the new repo
   is on a different partition than the shared one. The main use case
   is when the checkout is on a portable device and may not be
   available at prune time.

   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 <pclo...@gmail.com>
---
 Documentation/git-prune.txt            |  3 ++
 Documentation/gitrepository-layout.txt | 19 +++++++++
 builtin/checkout.c                     | 36 ++++++++++++++++-
 builtin/prune.c                        | 74 ++++++++++++++++++++++++++++++++++
 compat/mingw.h                         |  1 +
 git-compat-util.h                      |  4 ++
 setup.c                                | 13 ++++++
 7 files changed, 149 insertions(+), 1 deletion(-)

diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt
index 058ac0d..7babf11 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 418e5c8..2dc6901 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -252,6 +252,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 1675808..1fc85d3 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -854,6 +854,17 @@ static void remove_junk_on_signal(int signo)
        raise(signo);
 }
 
+static dev_t get_device_or_die(const char *path)
+{
+       struct stat buf;
+       if (stat(path, &buf))
+               die_errno("failed to stat '%s'", path);
+       /* Ah Windows! Make different drives different "partitions" */
+       if (is_windows())
+               buf.st_dev = toupper(real_path(path)[0]);
+       return buf.st_dev;
+}
+
 static int prepare_linked_checkout(const struct checkout_opts *opts,
                                   struct branch_info *new)
 {
@@ -862,7 +873,7 @@ static int prepare_linked_checkout(const struct 
checkout_opts *opts,
        const char *path = opts->new_worktree, *name;
        struct stat st;
        struct child_process cp;
-       int counter = 0, len, ret;
+       int counter = 0, len, keep_locked = 0, ret;
 
        if (!new->commit)
                die(_("no branch specified"));
@@ -898,12 +909,18 @@ static int prepare_linked_checkout(const struct 
checkout_opts *opts,
        junk_git_dir = sb_repo.buf;
        is_junk = 1;
 
+       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,12 +929,24 @@ 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);
        strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
        write_file(sb.buf, 1, "../..\n");
 
+       if (get_device_or_die(path) != get_device_or_die(get_git_dir())) {
+               strbuf_reset(&sb);
+               strbuf_addf(&sb, "%s/locked", sb_repo.buf);
+               write_file(sb.buf, 1, "located on a different file system\n");
+               keep_locked = 1;
+       } else {
+               strbuf_reset(&sb);
+               strbuf_addf(&sb, "%s/link", sb_repo.buf);
+               (void)link(sb_git.buf, sb.buf);
+       }
+
        if (!opts->quiet)
                fprintf_ln(stderr, _("Enter %s (identifier %s)"), path, name);
 
@@ -930,6 +959,11 @@ static int prepare_linked_checkout(const struct 
checkout_opts *opts,
        ret = run_command(&cp);
        if (!ret)
                is_junk = 0;
+       if (!keep_locked) {
+               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 de43b26..733cb3b 100644
--- a/builtin/prune.c
+++ b/builtin/prune.c
@@ -112,6 +112,70 @@ static void prune_object_dir(const char *path)
        }
 }
 
+static const char *prune_repo_dir(const char *id, struct stat *st)
+{
+       char *path;
+       int fd, len;
+       if (file_exists(git_path("repos/%s/locked", id)))
+               return NULL;
+       if (stat(git_path("repos/%s/gitdir", id), st)) {
+               st->st_mtime = expire;
+               return _("gitdir does not exist");
+       }
+       fd = open(git_path("repos/%s/gitdir", id), O_RDONLY);
+       len = st->st_size;
+       path = xmalloc(len + 1);
+       read_in_full(fd, path, len);
+       close(fd);
+       while (path[len - 1] == '\n' || path[len - 1] == '\r')
+               len--;
+       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 NULL;
+               return _("gitdir points to non-existing file");
+       }
+       free(path);
+       return NULL;
+}
+
+static void prune_repos_dir(void)
+{
+       const char *reason;
+       DIR *dir = opendir(git_path("repos"));
+       struct dirent *d;
+       int removed = 0;
+       struct stat st;
+       if (!dir)
+               return;
+       while ((d = readdir(dir)) != NULL) {
+               if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+                       continue;
+               if ((reason = prune_repo_dir(d->d_name, &st)) != NULL &&
+                   st.st_mtime <= expire) {
+                       struct strbuf sb = STRBUF_INIT;
+                       if (show_only || verbose)
+                               printf(_("Removing repos/%s: %s\n"), d->d_name, 
reason);
+                       if (show_only)
+                               continue;
+                       strbuf_addstr(&sb, git_path("repos/%s", d->d_name));
+                       remove_dir_recursively(&sb, 0);
+                       strbuf_release(&sb);
+                       removed = 1;
+               }
+       }
+       closedir(dir);
+       if (removed)
+               rmdir(git_path("repos"));
+}
+
 /*
  * Write errors (particularly out of space) can result in
  * failed temporary packs (and more rarely indexes and other
@@ -138,10 +202,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 +220,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/compat/mingw.h b/compat/mingw.h
index e033e72..18323c1 100644
--- a/compat/mingw.h
+++ b/compat/mingw.h
@@ -328,6 +328,7 @@ int winansi_fprintf(FILE *stream, const char *format, ...) 
__attribute__((format
  * git specific compatibility
  */
 
+#define is_windows() 1
 #define has_dos_drive_prefix(path) (isalpha(*(path)) && (path)[1] == ':')
 #define is_dir_sep(c) ((c) == '/' || (c) == '\\')
 static inline char *mingw_find_last_dir_sep(const char *path)
diff --git a/git-compat-util.h b/git-compat-util.h
index cbd86c3..41f1b74 100644
--- a/git-compat-util.h
+++ b/git-compat-util.h
@@ -266,6 +266,10 @@ extern char *gitbasename(char *);
 #define STRIP_EXTENSION ""
 #endif
 
+#ifndef is_windows
+#define is_windows() 0
+#endif
+
 #ifndef has_dos_drive_prefix
 #define has_dos_drive_prefix(path) 0
 #endif
diff --git a/setup.c b/setup.c
index 40ce191..5529b26 100644
--- a/setup.c
+++ b/setup.c
@@ -336,6 +336,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.
@@ -384,6 +395,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);
-- 
1.9.0.40.gaa8c3ea

--
To unsubscribe from this list: send the line "unsubscribe git" in
the body of a message to majord...@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html

Reply via email to