{"thread":{"id":"42711","subject":"[PATCH 00/10] git worktree (re)move","startedAt":"2016-06-25T08:01:14Z","lastAt":"2017-08-01T18:24:38Z","messageCount":16,"participants":["Nguyễn Thái Ngọc Duy","Duy Nguyen","Eric Sunshine"],"isPatch":true,"patchVersion":1,"patchTotal":10},"messages":[{"id":"290118","messageId":"20160625075433.4608-1-pclouds@gmail.com","threadId":"42711","inReplyTo":null,"subject":"[PATCH 00/10] git worktree (re)move","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-06-25T07:54:23Z","receivedAt":"2016-06-25T08:01:14Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Now that nd/worktree-lock seems to be on its way to 'next', let's\ncontinue to review this one. The first 5 patches bring busybox's copy\nfunctionality to git. The last 5 add support functions and the two new\ncommands.\n\nThis series needs both nd/worktree-lock (for find_worktree) and\n'master' (for error_errno).\n\nNguyễn Thái Ngọc Duy (10):\n  copy.c: import copy_file() from busybox\n  copy.c: delete unused code in copy_file()\n  copy.c: convert bb_(p)error_msg to error(_errno)\n  copy.c: style fix\n  copy.c: convert copy_file() to copy_dir_recursively()\n  worktree.c: add validate_worktree()\n  worktree.c: add update_worktree_location()\n  worktree: add \"move\" commmand\n  worktree move: accept destination as directory\n  worktree: add \"remove\" command\n\n Documentation/git-worktree.txt         |  28 ++-\n builtin/worktree.c                     | 156 ++++++++++++++\n cache.h                                |   1 +\n contrib/completion/git-completion.bash |   5 +-\n copy.c                                 | 369 +++++++++++++++++++++++++++++++++\n t/t2028-worktree-move.sh               |  56 +++++\n worktree.c                             |  88 ++++++++\n worktree.h                             |  11 +\n 8 files changed, 703 insertions(+), 11 deletions(-)\n\n-- \n2.8.2.526.g02eed6d\n\n"},{"id":"290119","messageId":"20160625075433.4608-5-pclouds@gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-1-pclouds@gmail.com","subject":"[PATCH 04/10] copy.c: style fix","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-06-25T07:54:27Z","receivedAt":"2016-06-25T08:06:53Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n copy.c | 50 +++++++++++++++++++++++++++++---------------------\n 1 file changed, 29 insertions(+), 21 deletions(-)\n\ndiff --git a/copy.c b/copy.c\nindex 074b609..60c7d8a 100644\n--- a/copy.c\n+++ b/copy.c\n@@ -111,8 +111,10 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\tif (dest_exists) {\n \t\t\tif (!S_ISDIR(dest_stat.st_mode))\n \t\t\t\treturn error(_(\"target '%s' is not a directory\"), dest);\n-\t\t\t/* race here: user can substitute a symlink between\n-\t\t\t * this check and actual creation of files inside dest */\n+\t\t\t/*\n+\t\t\t * race here: user can substitute a symlink between\n+\t\t\t * this check and actual creation of files inside dest\n+\t\t\t */\n \t\t} else {\n \t\t\t/* Create DEST */\n \t\t\tmode_t mode;\n@@ -130,22 +132,24 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\t\tif (lstat(dest, &dest_stat) < 0)\n \t\t\t\treturn error_errno(_(\"can't stat '%s'\"), dest);\n \t\t}\n-\t\t/* remember (dev,inode) of each created dir.\n-\t\t * NULL: name is not remembered */\n+\t\t/*\n+\t\t * remember (dev,inode) of each created dir. name is\n+\t\t * not remembered\n+\t\t */\n \t\tadd_to_ino_dev_hashtable(&dest_stat, NULL);\n \n \t\t/* Recursively copy files in SOURCE */\n \t\tdp = opendir(source);\n-\t\tif (dp == NULL) {\n+\t\tif (!dp) {\n \t\t\tretval = -1;\n \t\t\tgoto preserve_mode_ugid_time;\n \t\t}\n \n-\t\twhile ((d = readdir(dp)) != NULL) {\n+\t\twhile ((d = readdir(dp))) {\n \t\t\tchar *new_source, *new_dest;\n \n \t\t\tnew_source = concat_subpath_file(source, d->d_name);\n-\t\t\tif (new_source == NULL)\n+\t\t\tif (!new_source)\n \t\t\t\tcontinue;\n \t\t\tnew_dest = concat_path_file(dest, d->d_name);\n \t\t\tif (copy_file(new_source, new_dest, flags & ~FILEUTILS_DEREFERENCE_L0) < 0)\n@@ -155,16 +159,15 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\t}\n \t\tclosedir(dp);\n \n-\t\tif (!dest_exists\n-\t\t && chmod(dest, source_stat.st_mode & ~saved_umask) < 0\n-\t\t) {\n+\t\tif (!dest_exists &&\n+\t\t    chmod(dest, source_stat.st_mode & ~saved_umask) < 0) {\n \t\t\terror_errno(_(\"can't preserve permissions of '%s'\"), dest);\n \t\t\t/* retval = -1; - WRONG! copy *WAS* made */\n \t\t}\n \t\tgoto preserve_mode_ugid_time;\n \t}\n \n-\tif (S_ISREG(source_stat.st_mode) ) { /* \"cp [-opts] regular_file thing2\" */\n+\tif (S_ISREG(source_stat.st_mode)) { /* \"cp [-opts] regular_file thing2\" */\n \t\tint src_fd;\n \t\tint dst_fd;\n \t\tmode_t new_mode;\n@@ -199,7 +202,7 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\tif (!S_ISREG(source_stat.st_mode))\n \t\t\tnew_mode = 0666;\n \n-\t\t// POSIX way is a security problem versus (sym)link attacks\n+\t\t/* POSIX way is a security problem versus (sym)link attacks */\n \t\tif (!ENABLE_FEATURE_NON_POSIX_CP) {\n \t\t\tdst_fd = open(dest, O_WRONLY|O_CREAT|O_TRUNC, new_mode);\n \t\t} else { /* safe way: */\n@@ -226,13 +229,15 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\t\tretval = error_errno(_(\"error writing to '%s'\"), dest);\n \t\t/* ...but read size is already checked by bb_copyfd_eof */\n \t\tclose(src_fd);\n-\t\t/* \"cp /dev/something new_file\" should not\n-\t\t * copy mode of /dev/something */\n+\t\t/*\n+\t\t * \"cp /dev/something new_file\" should not\n+\t\t * copy mode of /dev/something\n+\t\t */\n \t\tif (!S_ISREG(source_stat.st_mode))\n \t\t\treturn retval;\n \t\tgoto preserve_mode_ugid_time;\n \t}\n- dont_cat:\n+dont_cat:\n \n \t/* Source is a symlink or a special file */\n \t/* We are lazy here, a bit lax with races... */\n@@ -252,20 +257,23 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\t\tif (lchown(dest, source_stat.st_uid, source_stat.st_gid) < 0)\n \t\t\t\terror_errno(_(\"can't preserve %s of '%s'\"), \"ownership\", dest);\n \t\t}\n-\t\t/* _Not_ jumping to preserve_mode_ugid_time:\n-\t\t * symlinks don't have those */\n+\t\t/*\n+\t\t * _Not_ jumping to preserve_mode_ugid_time: symlinks\n+\t\t * don't have those\n+\t\t */\n \t\treturn 0;\n \t}\n-\tif (S_ISBLK(source_stat.st_mode) || S_ISCHR(source_stat.st_mode)\n-\t || S_ISSOCK(source_stat.st_mode) || S_ISFIFO(source_stat.st_mode)\n-\t) {\n+\tif (S_ISBLK(source_stat.st_mode) ||\n+\t    S_ISCHR(source_stat.st_mode) ||\n+\t    S_ISSOCK(source_stat.st_mode) ||\n+\t    S_ISFIFO(source_stat.st_mode)) {\n \t\tif (mknod(dest, source_stat.st_mode, source_stat.st_rdev) < 0)\n \t\t\treturn error_errno(_(\"can't create '%s'\"), dest);\n \t} else\n \t\treturn error(_(\"unrecognized file '%s' with mode %x\"),\n \t\t\t     source, source_stat.st_mode);\n \n- preserve_mode_ugid_time:\n+preserve_mode_ugid_time:\n \n \tif (1 /*FILEUTILS_PRESERVE_STATUS*/) {\n \t\tstruct timeval times[2];\n-- \n2.8.2.526.g02eed6d\n\n"},{"id":"290120","messageId":"20160625075433.4608-3-pclouds@gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-1-pclouds@gmail.com","subject":"[PATCH 02/10] copy.c: delete unused code in copy_file()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-06-25T07:54:25Z","receivedAt":"2016-06-25T08:08:37Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":" - selinux preservation code\n - make-link code\n - delete link dereference code\n - non-recursive copy code\n - stat no preservation code\n - verbose printing code\n\nSome of these are \"cp\" features that we don't need (for \"git worktree\nmove\"). Some do not make sense in source-control context (SELinux).\nSome probably need a reimplementation to better fit in (verbose\nprinting code).\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n copy.c | 101 +++++------------------------------------------------------------\n 1 file changed, 7 insertions(+), 94 deletions(-)\n\ndiff --git a/copy.c b/copy.c\nindex 79623ac..b7a87f1 100644\n--- a/copy.c\n+++ b/copy.c\n@@ -82,14 +82,7 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \tsmallint dest_exists = 0;\n \tsmallint ovr;\n \n-/* Inverse of cp -d (\"cp without -d\") */\n-#define FLAGS_DEREF (flags & (FILEUTILS_DEREFERENCE + FILEUTILS_DEREFERENCE_L0))\n-\n-\tif ((FLAGS_DEREF ? stat : lstat)(source, &source_stat) < 0) {\n-\t\t/* This may be a dangling symlink.\n-\t\t * Making [sym]links to dangling symlinks works, so... */\n-\t\tif (flags & (FILEUTILS_MAKE_SOFTLINK|FILEUTILS_MAKE_HARDLINK))\n-\t\t\tgoto make_links;\n+\tif (lstat(source, &source_stat) < 0) {\n \t\tbb_perror_msg(\"can't stat '%s'\", source);\n \t\treturn -1;\n \t}\n@@ -109,35 +102,12 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\tdest_exists = 1;\n \t}\n \n-#if ENABLE_SELINUX\n-\tif ((flags & FILEUTILS_PRESERVE_SECURITY_CONTEXT) && is_selinux_enabled() > 0) {\n-\t\tsecurity_context_t con;\n-\t\tif (lgetfilecon(source, &con) >= 0) {\n-\t\t\tif (setfscreatecon(con) < 0) {\n-\t\t\t\tbb_perror_msg(\"can't set setfscreatecon %s\", con);\n-\t\t\t\tfreecon(con);\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t} else if (errno == ENOTSUP || errno == ENODATA) {\n-\t\t\tsetfscreatecon_or_die(NULL);\n-\t\t} else {\n-\t\t\tbb_perror_msg(\"can't lgetfilecon %s\", source);\n-\t\t\treturn -1;\n-\t\t}\n-\t}\n-#endif\n-\n \tif (S_ISDIR(source_stat.st_mode)) {\n \t\tDIR *dp;\n \t\tconst char *tp;\n \t\tstruct dirent *d;\n \t\tmode_t saved_umask = 0;\n \n-\t\tif (!(flags & FILEUTILS_RECUR)) {\n-\t\t\tbb_error_msg(\"omitting directory '%s'\", source);\n-\t\t\treturn -1;\n-\t\t}\n-\n \t\t/* Did we ever create source ourself before? */\n \t\ttp = is_in_ino_dev_hashtable(&source_stat);\n \t\tif (tp) {\n@@ -160,8 +130,6 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\t\tsaved_umask = umask(0);\n \n \t\t\tmode = source_stat.st_mode;\n-\t\t\tif (!(flags & FILEUTILS_PRESERVE_STATUS))\n-\t\t\t\tmode = source_stat.st_mode & ~saved_umask;\n \t\t\t/* Allow owner to access new dir (at least for now) */\n \t\t\tmode |= S_IRWXU;\n \t\t\tif (mkdir(dest, mode) < 0) {\n@@ -210,45 +178,17 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\tgoto preserve_mode_ugid_time;\n \t}\n \n-\tif (flags & (FILEUTILS_MAKE_SOFTLINK|FILEUTILS_MAKE_HARDLINK)) {\n-\t\tint (*lf)(const char *oldpath, const char *newpath);\n- make_links:\n-\t\t/* Hmm... maybe\n-\t\t * if (DEREF && MAKE_SOFTLINK) source = realpath(source) ?\n-\t\t * (but realpath returns NULL on dangling symlinks...) */\n-\t\tlf = (flags & FILEUTILS_MAKE_SOFTLINK) ? symlink : link;\n-\t\tif (lf(source, dest) < 0) {\n-\t\t\tovr = ask_and_unlink(dest, flags);\n-\t\t\tif (ovr <= 0)\n-\t\t\t\treturn ovr;\n-\t\t\tif (lf(source, dest) < 0) {\n-\t\t\t\tbb_perror_msg(\"can't create link '%s'\", dest);\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t}\n-\t\t/* _Not_ jumping to preserve_mode_ugid_time:\n-\t\t * (sym)links don't have those */\n-\t\treturn 0;\n-\t}\n-\n-\tif (/* \"cp thing1 thing2\" without -R: just open and read() from thing1 */\n-\t    !(flags & FILEUTILS_RECUR)\n-\t    /* \"cp [-opts] regular_file thing2\" */\n-\t || S_ISREG(source_stat.st_mode)\n-\t /* DEREF uses stat, which never returns S_ISLNK() == true.\n-\t  * So the below is never true: */\n-\t /* || (FLAGS_DEREF && S_ISLNK(source_stat.st_mode)) */\n-\t) {\n+\tif (S_ISREG(source_stat.st_mode) ) { /* \"cp [-opts] regular_file thing2\" */\n \t\tint src_fd;\n \t\tint dst_fd;\n \t\tmode_t new_mode;\n \n-\t\tif (!FLAGS_DEREF && S_ISLNK(source_stat.st_mode)) {\n+\t\tif (S_ISLNK(source_stat.st_mode)) {\n \t\t\t/* \"cp -d symlink dst\": create a link */\n \t\t\tgoto dont_cat;\n \t\t}\n \n-\t\tif (ENABLE_FEATURE_PRESERVE_HARDLINKS && !FLAGS_DEREF) {\n+\t\tif (ENABLE_FEATURE_PRESERVE_HARDLINKS) {\n \t\t\tconst char *link_target;\n \t\t\tlink_target = is_in_ino_dev_hashtable(&source_stat);\n \t\t\tif (link_target) {\n@@ -295,25 +235,6 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\t\t}\n \t\t}\n \n-#if ENABLE_SELINUX\n-\t\tif ((flags & (FILEUTILS_PRESERVE_SECURITY_CONTEXT|FILEUTILS_SET_SECURITY_CONTEXT))\n-\t\t && is_selinux_enabled() > 0\n-\t\t) {\n-\t\t\tsecurity_context_t con;\n-\t\t\tif (getfscreatecon(&con) == -1) {\n-\t\t\t\tbb_perror_msg(\"getfscreatecon\");\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tif (con) {\n-\t\t\t\tif (setfilecon(dest, con) == -1) {\n-\t\t\t\t\tbb_perror_msg(\"setfilecon:%s,%s\", dest, con);\n-\t\t\t\t\tfreecon(con);\n-\t\t\t\t\treturn -1;\n-\t\t\t\t}\n-\t\t\t\tfreecon(con);\n-\t\t\t}\n-\t\t}\n-#endif\n \t\tif (bb_copyfd_eof(src_fd, dst_fd) == -1)\n \t\t\tretval = -1;\n \t\t/* Careful with writing... */\n@@ -348,9 +269,8 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\t\t\tbb_perror_msg(\"can't create symlink '%s'\", dest);\n \t\t\t\treturn -1;\n \t\t\t}\n-\t\t\tif (flags & FILEUTILS_PRESERVE_STATUS)\n-\t\t\t\tif (lchown(dest, source_stat.st_uid, source_stat.st_gid) < 0)\n-\t\t\t\t\tbb_perror_msg(\"can't preserve %s of '%s'\", \"ownership\", dest);\n+\t\t\tif (lchown(dest, source_stat.st_uid, source_stat.st_gid) < 0)\n+\t\t\t\tbb_perror_msg(\"can't preserve %s of '%s'\", \"ownership\", dest);\n \t\t}\n \t\t/* _Not_ jumping to preserve_mode_ugid_time:\n \t\t * symlinks don't have those */\n@@ -370,10 +290,7 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \n  preserve_mode_ugid_time:\n \n-\tif (flags & FILEUTILS_PRESERVE_STATUS\n-\t/* Cannot happen: */\n-\t/* && !(flags & (FILEUTILS_MAKE_SOFTLINK|FILEUTILS_MAKE_HARDLINK)) */\n-\t) {\n+\tif (1 /*FILEUTILS_PRESERVE_STATUS*/) {\n \t\tstruct timeval times[2];\n \n \t\ttimes[1].tv_sec = times[0].tv_sec = source_stat.st_mtime;\n@@ -389,10 +306,6 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\t\tbb_perror_msg(\"can't preserve %s of '%s'\", \"permissions\", dest);\n \t}\n \n-\tif (flags & FILEUTILS_VERBOSE) {\n-\t\tprintf(\"'%s' -> '%s'\\n\", source, dest);\n-\t}\n-\n \treturn retval;\n }\n #endif\n-- \n2.8.2.526.g02eed6d\n\n"},{"id":"290121","messageId":"20160625075433.4608-10-pclouds@gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-1-pclouds@gmail.com","subject":"[PATCH 09/10] worktree move: accept destination as directory","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-06-25T07:54:32Z","receivedAt":"2016-06-25T08:08:43Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Similar to \"mv a b/\", which is actually \"mv a b/a\", we extract basename\nof source worktree and create a directory of the same name at\ndestination if dst path is a directory.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/worktree.c | 19 ++++++++++++++++++-\n 1 file changed, 18 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 5d7ca27..443a5ae 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -541,7 +541,13 @@ static int move_worktree(int ac, const char **av, const char *prefix)\n \tstrbuf_addstr(&dst, prefix_filename(prefix,\n \t\t\t\t\t    strlen(prefix),\n \t\t\t\t\t    av[1]));\n-\tif (file_exists(dst.buf))\n+\tif (is_directory(dst.buf))\n+\t\t/*\n+\t\t * keep going, dst will be appended after we get the\n+\t\t * source's absolute path\n+\t\t */\n+\t\t;\n+\telse if (file_exists(dst.buf))\n \t\tdie(_(\"target '%s' already exists\"), av[1]);\n \n \tworktrees = get_worktrees();\n@@ -558,6 +564,17 @@ static int move_worktree(int ac, const char **av, const char *prefix)\n \tif (validate_worktree(wt, 0))\n \t\treturn -1;\n \n+\tif (is_directory(dst.buf)) {\n+\t\tconst char *sep = find_last_dir_sep(wt->path);\n+\n+\t\tif (!sep)\n+\t\t\tdie(_(\"could not figure out destination name from '%s'\"),\n+\t\t\t    wt->path);\n+\t\tstrbuf_addstr(&dst, sep);\n+\t\tif (file_exists(dst.buf))\n+\t\t\tdie(_(\"target '%s' already exists\"), dst.buf);\n+\t}\n+\n \t/*\n \t * First try. Atomically move, and probably cheaper, if both\n \t * source and target are on the same file system.\n-- \n2.8.2.526.g02eed6d\n\n"},{"id":"290122","messageId":"20160625075433.4608-6-pclouds@gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-1-pclouds@gmail.com","subject":"[PATCH 05/10] copy.c: convert copy_file() to copy_dir_recursively()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-06-25T07:54:28Z","receivedAt":"2016-06-25T08:08:53Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This finally enables busybox's copy_file() code under a new name\n(because \"copy_file\" is already taken in Git code base). Because this\ncomes from busybox, POSIXy (or even Linuxy) behavior is expected. More\nchanges may be needed for Windows support.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n cache.h |   1 +\n copy.c  | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++------------\n 2 files changed, 179 insertions(+), 38 deletions(-)\n\ndiff --git a/cache.h b/cache.h\nindex 6049f86..34062f7 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1719,6 +1719,7 @@ extern void fprintf_or_die(FILE *, const char *fmt, ...);\n extern int copy_fd(int ifd, int ofd);\n extern int copy_file(const char *dst, const char *src, int mode);\n extern int copy_file_with_time(const char *dst, const char *src, int mode);\n+extern int copy_dir_recursively(const char *source, const char *dest);\n \n extern void write_or_die(int fd, const void *buf, size_t count);\n extern int write_or_whine(int fd, const void *buf, size_t count, const char *msg);\ndiff --git a/copy.c b/copy.c\nindex 60c7d8a..8471f7f 100644\n--- a/copy.c\n+++ b/copy.c\n@@ -1,4 +1,6 @@\n #include \"cache.h\"\n+#include \"dir.h\"\n+#include \"hashmap.h\"\n \n int copy_fd(int ifd, int ofd)\n {\n@@ -66,21 +68,126 @@ int copy_file_with_time(const char *dst, const char *src, int mode)\n \treturn status;\n }\n \n-#if 0\n-/* Return:\n- * -1 error, copy not made\n- *  0 copy is made or user answered \"no\" in interactive mode\n- *    (failures to preserve mode/owner/times are not reported in exit code)\n+struct inode_key {\n+\tstruct hashmap_entry entry;\n+\tino_t ino;\n+\tdev_t dev;\n+\t/*\n+\t * Reportedly, on cramfs a file and a dir can have same ino.\n+\t * Need to also remember \"file/dir\" bit:\n+\t */\n+\tchar isdir; /* bool */\n+};\n+\n+struct inode_value {\n+\tstruct inode_key key;\n+\tchar name[FLEX_ARRAY];\n+};\n+\n+#define HASH_SIZE      311u   /* Should be prime */\n+static inline unsigned hash_inode(ino_t i)\n+{\n+\treturn i % HASH_SIZE;\n+}\n+\n+static int inode_cmp(const void *entry, const void *entry_or_key,\n+\t\t     const void *keydata)\n+{\n+\tconst struct inode_value *inode = entry;\n+\tconst struct inode_key   *key   = entry_or_key;\n+\n+\treturn !(inode->key.ino   == key->ino &&\n+\t\t inode->key.dev   == key->dev &&\n+\t\t inode->key.isdir == key->isdir);\n+}\n+\n+static const char *is_in_ino_dev_hashtable(const struct hashmap *map,\n+\t\t\t\t\t   const struct stat *st)\n+{\n+\tstruct inode_key key;\n+\tstruct inode_value *value;\n+\n+\tkey.entry.hash = hash_inode(st->st_ino);\n+\tkey.ino\t       = st->st_ino;\n+\tkey.dev\t       = st->st_dev;\n+\tkey.isdir      = !!S_ISDIR(st->st_mode);\n+\tvalue\t       = hashmap_get(map, &key, NULL);\n+\treturn value ? value->name : NULL;\n+}\n+\n+static void add_to_ino_dev_hashtable(struct hashmap *map,\n+\t\t\t\t     const struct stat *st,\n+\t\t\t\t     const char *path)\n+{\n+\tstruct inode_value *v;\n+\tint len = strlen(path);\n+\n+\tv = xmalloc(offsetof(struct inode_value, name) + len + 1);\n+\tv->key.entry.hash = hash_inode(st->st_ino);\n+\tv->key.ino\t  = st->st_ino;\n+\tv->key.dev\t  = st->st_dev;\n+\tv->key.isdir      = !!S_ISDIR(st->st_mode);\n+\tmemcpy(v->name, path, len + 1);\n+\thashmap_add(map, v);\n+}\n+\n+/*\n+ * Find out if the last character of a string matches the one given.\n+ * Don't underrun the buffer if the string length is 0.\n  */\n-int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n+static inline char *last_char_is(const char *s, int c)\n+{\n+\tif (s && *s) {\n+\t\tsize_t sz = strlen(s) - 1;\n+\t\ts += sz;\n+\t\tif ( (unsigned char)*s == c)\n+\t\t\treturn (char*)s;\n+\t}\n+\treturn NULL;\n+}\n+\n+static inline char *concat_path_file(const char *path, const char *filename)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tchar *lc;\n+\n+\tif (!path)\n+\t\tpath = \"\";\n+\tlc = last_char_is(path, '/');\n+\twhile (*filename == '/')\n+\t\tfilename++;\n+\tstrbuf_addf(&sb, \"%s%s%s\", path, (lc==NULL ? \"/\" : \"\"), filename);\n+\treturn strbuf_detach(&sb, NULL);\n+}\n+\n+static char *concat_subpath_file(const char *path, const char *f)\n+{\n+\tif (f && is_dot_or_dotdot(f))\n+\t\treturn NULL;\n+\treturn concat_path_file(path, f);\n+}\n+\n+static int do_unlink(const char *dest)\n+{\n+\tint e = errno;\n+\n+\tif (unlink(dest) < 0) {\n+\t\terrno = e; /* do not use errno from unlink */\n+\t\treturn error_errno(_(\"can't create '%s'\"), dest);\n+\t}\n+\treturn 0;\n+}\n+\n+static int copy_dir_1(struct hashmap *inode_map,\n+\t\t      const char *source,\n+\t\t      const char *dest)\n {\n \t/* This is a recursive function, try to minimize stack usage */\n-\t/* NB: each struct stat is ~100 bytes */\n \tstruct stat source_stat;\n \tstruct stat dest_stat;\n-\tsmallint retval = 0;\n-\tsmallint dest_exists = 0;\n-\tsmallint ovr;\n+\tint retval = 0;\n+\tint dest_exists = 0;\n+\tint ovr;\n \n \tif (lstat(source, &source_stat) < 0)\n \t\treturn error_errno(_(\"can't stat '%s'\"), source);\n@@ -102,7 +209,7 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\tmode_t saved_umask = 0;\n \n \t\t/* Did we ever create source ourself before? */\n-\t\ttp = is_in_ino_dev_hashtable(&source_stat);\n+\t\ttp = is_in_ino_dev_hashtable(inode_map, &source_stat);\n \t\tif (tp)\n \t\t\t/* We did! it's a recursion! man the lifeboats... */\n \t\t\treturn error(_(\"recursion detected, omitting directory '%s'\"),\n@@ -132,11 +239,12 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\t\tif (lstat(dest, &dest_stat) < 0)\n \t\t\t\treturn error_errno(_(\"can't stat '%s'\"), dest);\n \t\t}\n+\n \t\t/*\n \t\t * remember (dev,inode) of each created dir. name is\n \t\t * not remembered\n \t\t */\n-\t\tadd_to_ino_dev_hashtable(&dest_stat, NULL);\n+\t\tadd_to_ino_dev_hashtable(inode_map, &dest_stat, \"\");\n \n \t\t/* Recursively copy files in SOURCE */\n \t\tdp = opendir(source);\n@@ -152,7 +260,7 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\t\tif (!new_source)\n \t\t\t\tcontinue;\n \t\t\tnew_dest = concat_path_file(dest, d->d_name);\n-\t\t\tif (copy_file(new_source, new_dest, flags & ~FILEUTILS_DEREFERENCE_L0) < 0)\n+\t\t\tif (copy_dir_1(inode_map, new_source, new_dest) < 0)\n \t\t\t\tretval = -1;\n \t\t\tfree(new_source);\n \t\t\tfree(new_dest);\n@@ -177,53 +285,57 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\t\tgoto dont_cat;\n \t\t}\n \n-\t\tif (ENABLE_FEATURE_PRESERVE_HARDLINKS) {\n+\t\tif (1 /*ENABLE_FEATURE_PRESERVE_HARDLINKS*/) {\n \t\t\tconst char *link_target;\n-\t\t\tlink_target = is_in_ino_dev_hashtable(&source_stat);\n+\t\t\tlink_target = is_in_ino_dev_hashtable(inode_map, &source_stat);\n \t\t\tif (link_target) {\n \t\t\t\tif (link(link_target, dest) < 0) {\n-\t\t\t\t\tovr = ask_and_unlink(dest, flags);\n-\t\t\t\t\tif (ovr <= 0)\n+\t\t\t\t\tovr = do_unlink(dest);\n+\t\t\t\t\tif (ovr < 0)\n \t\t\t\t\t\treturn ovr;\n \t\t\t\t\tif (link(link_target, dest) < 0)\n \t\t\t\t\t\treturn error_errno(_(\"can't create link '%s'\"), dest);\n \t\t\t\t}\n \t\t\t\treturn 0;\n \t\t\t}\n-\t\t\tadd_to_ino_dev_hashtable(&source_stat, dest);\n+\t\t\tadd_to_ino_dev_hashtable(inode_map, &source_stat, dest);\n \t\t}\n \n-\t\tsrc_fd = open_or_warn(source, O_RDONLY);\n+\t\tsrc_fd = open(source, O_RDONLY);\n \t\tif (src_fd < 0)\n-\t\t\treturn -1;\n+\t\t\treturn error_errno(_(\"can't open '%s'\"), source);\n \n \t\t/* Do not try to open with weird mode fields */\n \t\tnew_mode = source_stat.st_mode;\n \t\tif (!S_ISREG(source_stat.st_mode))\n \t\t\tnew_mode = 0666;\n \n-\t\t/* POSIX way is a security problem versus (sym)link attacks */\n-\t\tif (!ENABLE_FEATURE_NON_POSIX_CP) {\n-\t\t\tdst_fd = open(dest, O_WRONLY|O_CREAT|O_TRUNC, new_mode);\n-\t\t} else { /* safe way: */\n-\t\t\tdst_fd = open(dest, O_WRONLY|O_CREAT|O_EXCL, new_mode);\n-\t\t}\n+\t\tdst_fd = open(dest, O_WRONLY|O_CREAT|O_EXCL, new_mode);\n \t\tif (dst_fd == -1) {\n-\t\t\tovr = ask_and_unlink(dest, flags);\n-\t\t\tif (ovr <= 0) {\n+\t\t\tovr = do_unlink(dest);\n+\t\t\tif (ovr < 0) {\n \t\t\t\tclose(src_fd);\n \t\t\t\treturn ovr;\n \t\t\t}\n \t\t\t/* It shouldn't exist. If it exists, do not open (symlink attack?) */\n-\t\t\tdst_fd = open3_or_warn(dest, O_WRONLY|O_CREAT|O_EXCL, new_mode);\n+\t\t\tdst_fd = open(dest, O_WRONLY|O_CREAT|O_EXCL, new_mode);\n \t\t\tif (dst_fd < 0) {\n \t\t\t\tclose(src_fd);\n-\t\t\t\treturn -1;\n+\t\t\t\treturn error_errno(_(\"can't open '%s'\"), dest);\n \t\t\t}\n \t\t}\n \n-\t\tif (bb_copyfd_eof(src_fd, dst_fd) == -1)\n+\t\tswitch (copy_fd(src_fd, dst_fd)) {\n+\t\tcase COPY_READ_ERROR:\n+\t\t\terror(_(\"copy-fd: read returned %s\"), strerror(errno));\n \t\t\tretval = -1;\n+\t\t\tbreak;\n+\t\tcase COPY_WRITE_ERROR:\n+\t\t\terror(_(\"copy-fd: write returned %s\"), strerror(errno));\n+\t\t\tretval = -1;\n+\t\t\tbreak;\n+\t\t}\n+\n \t\t/* Careful with writing... */\n \t\tif (close(dst_fd) < 0)\n \t\t\tretval = error_errno(_(\"error writing to '%s'\"), dest);\n@@ -243,19 +355,28 @@ dont_cat:\n \t/* We are lazy here, a bit lax with races... */\n \tif (dest_exists) {\n \t\terrno = EEXIST;\n-\t\tovr = ask_and_unlink(dest, flags);\n-\t\tif (ovr <= 0)\n+\t\tovr = do_unlink(dest);\n+\t\tif (ovr < 0)\n \t\t\treturn ovr;\n \t}\n \tif (S_ISLNK(source_stat.st_mode)) {\n-\t\tchar *lpath = xmalloc_readlink_or_warn(source);\n-\t\tif (lpath) {\n-\t\t\tint r = symlink(lpath, dest);\n-\t\t\tfree(lpath);\n+\t\tstruct strbuf lpath = STRBUF_INIT;\n+\t\tif (!strbuf_readlink(&lpath, source, 0)) {\n+\t\t\tint r = symlink(lpath.buf, dest);\n+\t\t\tstrbuf_release(&lpath);\n \t\t\tif (r < 0)\n \t\t\t\treturn error_errno(_(\"can't create symlink '%s'\"), dest);\n \t\t\tif (lchown(dest, source_stat.st_uid, source_stat.st_gid) < 0)\n \t\t\t\terror_errno(_(\"can't preserve %s of '%s'\"), \"ownership\", dest);\n+\t\t} else {\n+\t\t\t/* EINVAL => \"file: Invalid argument\" => puzzled user */\n+\t\t\tconst char *errmsg = _(\"not a symlink\");\n+\t\t\tint err = errno;\n+\n+\t\t\tif (err != EINVAL)\n+\t\t\t\terrmsg = strerror(err);\n+\t\t\terror(_(\"%s: cannot read link: %s\"), source, errmsg);\n+\t\t\tstrbuf_release(&lpath);\n \t\t}\n \t\t/*\n \t\t * _Not_ jumping to preserve_mode_ugid_time: symlinks\n@@ -293,4 +414,23 @@ preserve_mode_ugid_time:\n \n \treturn retval;\n }\n-#endif\n+\n+/*\n+ * Return:\n+ * -1 error, copy not made\n+ *  0 copy is made\n+ *\n+ * Failures to preserve mode/owner/times are not reported in exit\n+ * code. No support for preserving SELinux security context. Symlinks\n+ * and hardlinks are preserved.\n+ */\n+int copy_dir_recursively(const char *source, const char *dest)\n+{\n+\tint ret;\n+\tstruct hashmap inode_map;\n+\n+\thashmap_init(&inode_map, inode_cmp, 1024);\n+\tret = copy_dir_1(&inode_map, source, dest);\n+\thashmap_free(&inode_map, 1);\n+\treturn ret;\n+}\n-- \n2.8.2.526.g02eed6d\n\n"},{"id":"290123","messageId":"20160625075433.4608-2-pclouds@gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-1-pclouds@gmail.com","subject":"[PATCH 01/10] copy.c: import copy_file() from busybox","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-06-25T07:54:24Z","receivedAt":"2016-06-25T08:10:00Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This is busybox's unmodified copy_file() in libbb/copy_file.c from the\nGPL2+ commit f2c043acfcf9dad9fd3d65821b81f89986bbe54e (busybox: fix\nuninitialized memory when displaying IPv6 addresses -\n2016-01-18). This is a no-op commit. More changes are needed before\nthis new code can compile.\n\nThis will be needed for implementing \"git worktree move\" where we have\nto move a directory recursively. We can implement it from scratch, but\nthen we will have to deal with corner cases (failure to move, circular\nsymlinks...). And delegating the task to \"/bin/mv\" takes a way the\nability to clean things up properly when things fail and we may have\nto deal with \"mv\" differences between platforms.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n copy.c | 331 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 331 insertions(+)\n\ndiff --git a/copy.c b/copy.c\nindex 4de6a11..79623ac 100644\n--- a/copy.c\n+++ b/copy.c\n@@ -65,3 +65,334 @@ int copy_file_with_time(const char *dst, const char *src, int mode)\n \t\treturn copy_times(dst, src);\n \treturn status;\n }\n+\n+#if 0\n+/* Return:\n+ * -1 error, copy not made\n+ *  0 copy is made or user answered \"no\" in interactive mode\n+ *    (failures to preserve mode/owner/times are not reported in exit code)\n+ */\n+int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n+{\n+\t/* This is a recursive function, try to minimize stack usage */\n+\t/* NB: each struct stat is ~100 bytes */\n+\tstruct stat source_stat;\n+\tstruct stat dest_stat;\n+\tsmallint retval = 0;\n+\tsmallint dest_exists = 0;\n+\tsmallint ovr;\n+\n+/* Inverse of cp -d (\"cp without -d\") */\n+#define FLAGS_DEREF (flags & (FILEUTILS_DEREFERENCE + FILEUTILS_DEREFERENCE_L0))\n+\n+\tif ((FLAGS_DEREF ? stat : lstat)(source, &source_stat) < 0) {\n+\t\t/* This may be a dangling symlink.\n+\t\t * Making [sym]links to dangling symlinks works, so... */\n+\t\tif (flags & (FILEUTILS_MAKE_SOFTLINK|FILEUTILS_MAKE_HARDLINK))\n+\t\t\tgoto make_links;\n+\t\tbb_perror_msg(\"can't stat '%s'\", source);\n+\t\treturn -1;\n+\t}\n+\n+\tif (lstat(dest, &dest_stat) < 0) {\n+\t\tif (errno != ENOENT) {\n+\t\t\tbb_perror_msg(\"can't stat '%s'\", dest);\n+\t\t\treturn -1;\n+\t\t}\n+\t} else {\n+\t\tif (source_stat.st_dev == dest_stat.st_dev\n+\t\t && source_stat.st_ino == dest_stat.st_ino\n+\t\t) {\n+\t\t\tbb_error_msg(\"'%s' and '%s' are the same file\", source, dest);\n+\t\t\treturn -1;\n+\t\t}\n+\t\tdest_exists = 1;\n+\t}\n+\n+#if ENABLE_SELINUX\n+\tif ((flags & FILEUTILS_PRESERVE_SECURITY_CONTEXT) && is_selinux_enabled() > 0) {\n+\t\tsecurity_context_t con;\n+\t\tif (lgetfilecon(source, &con) >= 0) {\n+\t\t\tif (setfscreatecon(con) < 0) {\n+\t\t\t\tbb_perror_msg(\"can't set setfscreatecon %s\", con);\n+\t\t\t\tfreecon(con);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\t\t} else if (errno == ENOTSUP || errno == ENODATA) {\n+\t\t\tsetfscreatecon_or_die(NULL);\n+\t\t} else {\n+\t\t\tbb_perror_msg(\"can't lgetfilecon %s\", source);\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+#endif\n+\n+\tif (S_ISDIR(source_stat.st_mode)) {\n+\t\tDIR *dp;\n+\t\tconst char *tp;\n+\t\tstruct dirent *d;\n+\t\tmode_t saved_umask = 0;\n+\n+\t\tif (!(flags & FILEUTILS_RECUR)) {\n+\t\t\tbb_error_msg(\"omitting directory '%s'\", source);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\t/* Did we ever create source ourself before? */\n+\t\ttp = is_in_ino_dev_hashtable(&source_stat);\n+\t\tif (tp) {\n+\t\t\t/* We did! it's a recursion! man the lifeboats... */\n+\t\t\tbb_error_msg(\"recursion detected, omitting directory '%s'\",\n+\t\t\t\t\tsource);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tif (dest_exists) {\n+\t\t\tif (!S_ISDIR(dest_stat.st_mode)) {\n+\t\t\t\tbb_error_msg(\"target '%s' is not a directory\", dest);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\t\t\t/* race here: user can substitute a symlink between\n+\t\t\t * this check and actual creation of files inside dest */\n+\t\t} else {\n+\t\t\t/* Create DEST */\n+\t\t\tmode_t mode;\n+\t\t\tsaved_umask = umask(0);\n+\n+\t\t\tmode = source_stat.st_mode;\n+\t\t\tif (!(flags & FILEUTILS_PRESERVE_STATUS))\n+\t\t\t\tmode = source_stat.st_mode & ~saved_umask;\n+\t\t\t/* Allow owner to access new dir (at least for now) */\n+\t\t\tmode |= S_IRWXU;\n+\t\t\tif (mkdir(dest, mode) < 0) {\n+\t\t\t\tumask(saved_umask);\n+\t\t\t\tbb_perror_msg(\"can't create directory '%s'\", dest);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\t\t\tumask(saved_umask);\n+\t\t\t/* need stat info for add_to_ino_dev_hashtable */\n+\t\t\tif (lstat(dest, &dest_stat) < 0) {\n+\t\t\t\tbb_perror_msg(\"can't stat '%s'\", dest);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\t\t}\n+\t\t/* remember (dev,inode) of each created dir.\n+\t\t * NULL: name is not remembered */\n+\t\tadd_to_ino_dev_hashtable(&dest_stat, NULL);\n+\n+\t\t/* Recursively copy files in SOURCE */\n+\t\tdp = opendir(source);\n+\t\tif (dp == NULL) {\n+\t\t\tretval = -1;\n+\t\t\tgoto preserve_mode_ugid_time;\n+\t\t}\n+\n+\t\twhile ((d = readdir(dp)) != NULL) {\n+\t\t\tchar *new_source, *new_dest;\n+\n+\t\t\tnew_source = concat_subpath_file(source, d->d_name);\n+\t\t\tif (new_source == NULL)\n+\t\t\t\tcontinue;\n+\t\t\tnew_dest = concat_path_file(dest, d->d_name);\n+\t\t\tif (copy_file(new_source, new_dest, flags & ~FILEUTILS_DEREFERENCE_L0) < 0)\n+\t\t\t\tretval = -1;\n+\t\t\tfree(new_source);\n+\t\t\tfree(new_dest);\n+\t\t}\n+\t\tclosedir(dp);\n+\n+\t\tif (!dest_exists\n+\t\t && chmod(dest, source_stat.st_mode & ~saved_umask) < 0\n+\t\t) {\n+\t\t\tbb_perror_msg(\"can't preserve %s of '%s'\", \"permissions\", dest);\n+\t\t\t/* retval = -1; - WRONG! copy *WAS* made */\n+\t\t}\n+\t\tgoto preserve_mode_ugid_time;\n+\t}\n+\n+\tif (flags & (FILEUTILS_MAKE_SOFTLINK|FILEUTILS_MAKE_HARDLINK)) {\n+\t\tint (*lf)(const char *oldpath, const char *newpath);\n+ make_links:\n+\t\t/* Hmm... maybe\n+\t\t * if (DEREF && MAKE_SOFTLINK) source = realpath(source) ?\n+\t\t * (but realpath returns NULL on dangling symlinks...) */\n+\t\tlf = (flags & FILEUTILS_MAKE_SOFTLINK) ? symlink : link;\n+\t\tif (lf(source, dest) < 0) {\n+\t\t\tovr = ask_and_unlink(dest, flags);\n+\t\t\tif (ovr <= 0)\n+\t\t\t\treturn ovr;\n+\t\t\tif (lf(source, dest) < 0) {\n+\t\t\t\tbb_perror_msg(\"can't create link '%s'\", dest);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\t\t}\n+\t\t/* _Not_ jumping to preserve_mode_ugid_time:\n+\t\t * (sym)links don't have those */\n+\t\treturn 0;\n+\t}\n+\n+\tif (/* \"cp thing1 thing2\" without -R: just open and read() from thing1 */\n+\t    !(flags & FILEUTILS_RECUR)\n+\t    /* \"cp [-opts] regular_file thing2\" */\n+\t || S_ISREG(source_stat.st_mode)\n+\t /* DEREF uses stat, which never returns S_ISLNK() == true.\n+\t  * So the below is never true: */\n+\t /* || (FLAGS_DEREF && S_ISLNK(source_stat.st_mode)) */\n+\t) {\n+\t\tint src_fd;\n+\t\tint dst_fd;\n+\t\tmode_t new_mode;\n+\n+\t\tif (!FLAGS_DEREF && S_ISLNK(source_stat.st_mode)) {\n+\t\t\t/* \"cp -d symlink dst\": create a link */\n+\t\t\tgoto dont_cat;\n+\t\t}\n+\n+\t\tif (ENABLE_FEATURE_PRESERVE_HARDLINKS && !FLAGS_DEREF) {\n+\t\t\tconst char *link_target;\n+\t\t\tlink_target = is_in_ino_dev_hashtable(&source_stat);\n+\t\t\tif (link_target) {\n+\t\t\t\tif (link(link_target, dest) < 0) {\n+\t\t\t\t\tovr = ask_and_unlink(dest, flags);\n+\t\t\t\t\tif (ovr <= 0)\n+\t\t\t\t\t\treturn ovr;\n+\t\t\t\t\tif (link(link_target, dest) < 0) {\n+\t\t\t\t\t\tbb_perror_msg(\"can't create link '%s'\", dest);\n+\t\t\t\t\t\treturn -1;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\t\t\tadd_to_ino_dev_hashtable(&source_stat, dest);\n+\t\t}\n+\n+\t\tsrc_fd = open_or_warn(source, O_RDONLY);\n+\t\tif (src_fd < 0)\n+\t\t\treturn -1;\n+\n+\t\t/* Do not try to open with weird mode fields */\n+\t\tnew_mode = source_stat.st_mode;\n+\t\tif (!S_ISREG(source_stat.st_mode))\n+\t\t\tnew_mode = 0666;\n+\n+\t\t// POSIX way is a security problem versus (sym)link attacks\n+\t\tif (!ENABLE_FEATURE_NON_POSIX_CP) {\n+\t\t\tdst_fd = open(dest, O_WRONLY|O_CREAT|O_TRUNC, new_mode);\n+\t\t} else { /* safe way: */\n+\t\t\tdst_fd = open(dest, O_WRONLY|O_CREAT|O_EXCL, new_mode);\n+\t\t}\n+\t\tif (dst_fd == -1) {\n+\t\t\tovr = ask_and_unlink(dest, flags);\n+\t\t\tif (ovr <= 0) {\n+\t\t\t\tclose(src_fd);\n+\t\t\t\treturn ovr;\n+\t\t\t}\n+\t\t\t/* It shouldn't exist. If it exists, do not open (symlink attack?) */\n+\t\t\tdst_fd = open3_or_warn(dest, O_WRONLY|O_CREAT|O_EXCL, new_mode);\n+\t\t\tif (dst_fd < 0) {\n+\t\t\t\tclose(src_fd);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\t\t}\n+\n+#if ENABLE_SELINUX\n+\t\tif ((flags & (FILEUTILS_PRESERVE_SECURITY_CONTEXT|FILEUTILS_SET_SECURITY_CONTEXT))\n+\t\t && is_selinux_enabled() > 0\n+\t\t) {\n+\t\t\tsecurity_context_t con;\n+\t\t\tif (getfscreatecon(&con) == -1) {\n+\t\t\t\tbb_perror_msg(\"getfscreatecon\");\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\t\t\tif (con) {\n+\t\t\t\tif (setfilecon(dest, con) == -1) {\n+\t\t\t\t\tbb_perror_msg(\"setfilecon:%s,%s\", dest, con);\n+\t\t\t\t\tfreecon(con);\n+\t\t\t\t\treturn -1;\n+\t\t\t\t}\n+\t\t\t\tfreecon(con);\n+\t\t\t}\n+\t\t}\n+#endif\n+\t\tif (bb_copyfd_eof(src_fd, dst_fd) == -1)\n+\t\t\tretval = -1;\n+\t\t/* Careful with writing... */\n+\t\tif (close(dst_fd) < 0) {\n+\t\t\tbb_perror_msg(\"error writing to '%s'\", dest);\n+\t\t\tretval = -1;\n+\t\t}\n+\t\t/* ...but read size is already checked by bb_copyfd_eof */\n+\t\tclose(src_fd);\n+\t\t/* \"cp /dev/something new_file\" should not\n+\t\t * copy mode of /dev/something */\n+\t\tif (!S_ISREG(source_stat.st_mode))\n+\t\t\treturn retval;\n+\t\tgoto preserve_mode_ugid_time;\n+\t}\n+ dont_cat:\n+\n+\t/* Source is a symlink or a special file */\n+\t/* We are lazy here, a bit lax with races... */\n+\tif (dest_exists) {\n+\t\terrno = EEXIST;\n+\t\tovr = ask_and_unlink(dest, flags);\n+\t\tif (ovr <= 0)\n+\t\t\treturn ovr;\n+\t}\n+\tif (S_ISLNK(source_stat.st_mode)) {\n+\t\tchar *lpath = xmalloc_readlink_or_warn(source);\n+\t\tif (lpath) {\n+\t\t\tint r = symlink(lpath, dest);\n+\t\t\tfree(lpath);\n+\t\t\tif (r < 0) {\n+\t\t\t\tbb_perror_msg(\"can't create symlink '%s'\", dest);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\t\t\tif (flags & FILEUTILS_PRESERVE_STATUS)\n+\t\t\t\tif (lchown(dest, source_stat.st_uid, source_stat.st_gid) < 0)\n+\t\t\t\t\tbb_perror_msg(\"can't preserve %s of '%s'\", \"ownership\", dest);\n+\t\t}\n+\t\t/* _Not_ jumping to preserve_mode_ugid_time:\n+\t\t * symlinks don't have those */\n+\t\treturn 0;\n+\t}\n+\tif (S_ISBLK(source_stat.st_mode) || S_ISCHR(source_stat.st_mode)\n+\t || S_ISSOCK(source_stat.st_mode) || S_ISFIFO(source_stat.st_mode)\n+\t) {\n+\t\tif (mknod(dest, source_stat.st_mode, source_stat.st_rdev) < 0) {\n+\t\t\tbb_perror_msg(\"can't create '%s'\", dest);\n+\t\t\treturn -1;\n+\t\t}\n+\t} else {\n+\t\tbb_error_msg(\"unrecognized file '%s' with mode %x\", source, source_stat.st_mode);\n+\t\treturn -1;\n+\t}\n+\n+ preserve_mode_ugid_time:\n+\n+\tif (flags & FILEUTILS_PRESERVE_STATUS\n+\t/* Cannot happen: */\n+\t/* && !(flags & (FILEUTILS_MAKE_SOFTLINK|FILEUTILS_MAKE_HARDLINK)) */\n+\t) {\n+\t\tstruct timeval times[2];\n+\n+\t\ttimes[1].tv_sec = times[0].tv_sec = source_stat.st_mtime;\n+\t\ttimes[1].tv_usec = times[0].tv_usec = 0;\n+\t\t/* BTW, utimes sets usec-precision time - just FYI */\n+\t\tif (utimes(dest, times) < 0)\n+\t\t\tbb_perror_msg(\"can't preserve %s of '%s'\", \"times\", dest);\n+\t\tif (chown(dest, source_stat.st_uid, source_stat.st_gid) < 0) {\n+\t\t\tsource_stat.st_mode &= ~(S_ISUID | S_ISGID);\n+\t\t\tbb_perror_msg(\"can't preserve %s of '%s'\", \"ownership\", dest);\n+\t\t}\n+\t\tif (chmod(dest, source_stat.st_mode) < 0)\n+\t\t\tbb_perror_msg(\"can't preserve %s of '%s'\", \"permissions\", dest);\n+\t}\n+\n+\tif (flags & FILEUTILS_VERBOSE) {\n+\t\tprintf(\"'%s' -> '%s'\\n\", source, dest);\n+\t}\n+\n+\treturn retval;\n+}\n+#endif\n-- \n2.8.2.526.g02eed6d\n\n"},{"id":"290124","messageId":"20160625075433.4608-11-pclouds@gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-1-pclouds@gmail.com","subject":"[PATCH 10/10] worktree: add \"remove\" command","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-06-25T07:54:33Z","receivedAt":"2016-06-25T08:10:36Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git-worktree.txt         | 21 +++++----\n builtin/worktree.c                     | 78 ++++++++++++++++++++++++++++++++++\n contrib/completion/git-completion.bash |  5 ++-\n t/t2028-worktree-move.sh               | 26 ++++++++++++\n 4 files changed, 120 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt\nindex b842136..2d2fe57 100644\n--- a/Documentation/git-worktree.txt\n+++ b/Documentation/git-worktree.txt\n@@ -14,6 +14,7 @@ SYNOPSIS\n 'git worktree lock' [--reason <string>] <worktree>\n 'git worktree move' <worktree> <new-path>\n 'git worktree prune' [-n] [-v] [--expire <expire>]\n+'git worktree remove' [--force] <worktree>\n 'git worktree unlock' <worktree>\n \n DESCRIPTION\n@@ -81,6 +82,13 @@ prune::\n \n Prune working tree information in $GIT_DIR/worktrees.\n \n+remove::\n+\n+Remove a working tree. Only clean working trees (no untracked files\n+and no modification in tracked files) can be removed. Unclean working\n+trees can be removed with `--force`. The main working tree cannot be\n+removed.\n+\n unlock::\n \n Unlock a working tree, allowing it to be pruned, moved or deleted.\n@@ -90,9 +98,10 @@ OPTIONS\n \n -f::\n --force::\n-\tBy default, `add` refuses to create a new working tree when `<branch>`\n-\tis already checked out by another working tree. This option overrides\n-\tthat safeguard.\n+\tBy default, `add` refuses to create a new working tree when\n+\t`<branch>` is already checked out by another working tree and\n+\t`remove` refuses to remove an unclean working tree. This option\n+\toverrides that safeguard.\n \n -b <new-branch>::\n -B <new-branch>::\n@@ -253,12 +262,6 @@ Multiple checkout in general is still experimental, and the support\n for submodules is incomplete. It is NOT recommended to make multiple\n checkouts of a superproject.\n \n-git-worktree could provide more automation for tasks currently\n-performed manually, such as:\n-\n-- `remove` to remove a linked working tree and its administrative files (and\n-  warn if the working tree is dirty)\n-\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 443a5ae..873840a 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -17,6 +17,7 @@ static const char * const worktree_usage[] = {\n \tN_(\"git worktree lock [<options>] <path>\"),\n \tN_(\"git worktree move <worktree> <new-path>\"),\n \tN_(\"git worktree prune [<options>]\"),\n+\tN_(\"git worktree remove [<options>] <worktree>\"),\n \tN_(\"git worktree unlock <path>\"),\n \tNULL\n };\n@@ -600,6 +601,81 @@ static int move_worktree(int ac, const char **av, const char *prefix)\n \treturn update_worktree_location(wt, dst.buf);\n }\n \n+static int remove_worktree(int ac, const char **av, const char *prefix)\n+{\n+\tint force = 0;\n+\tstruct option options[] = {\n+\t\tOPT_BOOL(0, \"force\", &force,\n+\t\t\t N_(\"force removing even if the worktree is dirty\")),\n+\t\tOPT_END()\n+\t};\n+\tstruct worktree **worktrees, *wt;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *reason;\n+\tint ret = 0;\n+\n+\tac = parse_options(ac, av, prefix, options, worktree_usage, 0);\n+\tif (ac != 1)\n+\t\tusage_with_options(worktree_usage, options);\n+\n+\tworktrees = get_worktrees();\n+\twt = find_worktree(worktrees, prefix, av[0]);\n+\tif (!wt)\n+\t\tdie(_(\"'%s' is not a working directory\"), av[0]);\n+\tif (is_main_worktree(wt))\n+\t\tdie(_(\"'%s' is a main working directory\"), av[0]);\n+\tif ((reason = is_worktree_locked(wt))) {\n+\t\tif (*reason)\n+\t\t\tdie(_(\"already locked, reason: %s\"), reason);\n+\t\tdie(_(\"already locked, no reason\"));\n+\t}\n+\tif (validate_worktree(wt, 0))\n+\t\treturn -1;\n+\n+\tif (!force) {\n+\t\tstruct argv_array child_env = ARGV_ARRAY_INIT;\n+\t\tstruct child_process cp;\n+\t\tchar buf[1];\n+\n+\t\targv_array_pushf(&child_env, \"%s=%s/.git\",\n+\t\t\t\t GIT_DIR_ENVIRONMENT, wt->path);\n+\t\targv_array_pushf(&child_env, \"%s=%s\",\n+\t\t\t\t GIT_WORK_TREE_ENVIRONMENT, wt->path);\n+\t\tmemset(&cp, 0, sizeof(cp));\n+\t\targv_array_pushl(&cp.args, \"status\", \"--porcelain\", NULL);\n+\t\tcp.env = child_env.argv;\n+\t\tcp.git_cmd = 1;\n+\t\tcp.dir = wt->path;\n+\t\tcp.out = -1;\n+\t\tret = start_command(&cp);\n+\t\tif (ret)\n+\t\t\tdie_errno(_(\"failed to run git-status on '%s', code %d\"),\n+\t\t\t\t  av[0], ret);\n+\t\tret = xread(cp.out, buf, sizeof(buf));\n+\t\tif (ret)\n+\t\t\tdie(_(\"'%s' is dirty, use --force to delete it\"), av[0]);\n+\t\tclose(cp.out);\n+\t\tret = finish_command(&cp);\n+\t\tif (ret)\n+\t\t\tdie_errno(_(\"failed to run git-status on '%s', code %d\"),\n+\t\t\t\t  av[0], ret);\n+\t}\n+\tstrbuf_addstr(&sb, wt->path);\n+\tif (remove_dir_recursively(&sb, 0)) {\n+\t\terror_errno(_(\"failed to delete '%s'\"), sb.buf);\n+\t\tret = -1;\n+\t}\n+\tstrbuf_reset(&sb);\n+\tstrbuf_addstr(&sb, git_common_path(\"worktrees/%s\", wt->id));\n+\tif (remove_dir_recursively(&sb, 0)) {\n+\t\terror_errno(_(\"failed to delete '%s'\"), sb.buf);\n+\t\tret = -1;\n+\t}\n+\tstrbuf_release(&sb);\n+\tfree_worktrees(worktrees);\n+\treturn ret;\n+}\n+\n int cmd_worktree(int ac, const char **av, const char *prefix)\n {\n \tstruct option options[] = {\n@@ -622,5 +698,7 @@ int cmd_worktree(int ac, const char **av, const char *prefix)\n \t\treturn unlock_worktree(ac - 1, av + 1, prefix);\n \tif (!strcmp(av[1], \"move\"))\n \t\treturn move_worktree(ac - 1, av + 1, prefix);\n+\tif (!strcmp(av[1], \"remove\"))\n+\t\treturn remove_worktree(ac - 1, av + 1, prefix);\n \tusage_with_options(worktree_usage, options);\n }\ndiff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash\nindex b2028e2..c2981cf 100644\n--- a/contrib/completion/git-completion.bash\n+++ b/contrib/completion/git-completion.bash\n@@ -2597,7 +2597,7 @@ _git_whatchanged ()\n \n _git_worktree ()\n {\n-\tlocal subcommands=\"add list lock move prune unlock\"\n+\tlocal subcommands=\"add list lock move prune remove unlock\"\n \tlocal subcommand=\"$(__git_find_on_cmdline \"$subcommands\")\"\n \tif [ -z \"$subcommand\" ]; then\n \t\t__gitcomp \"$subcommands\"\n@@ -2615,6 +2615,9 @@ _git_worktree ()\n \t\tprune,--*)\n \t\t\t__gitcomp \"--dry-run --expire --verbose\"\n \t\t\t;;\n+\t\tremove,--*)\n+\t\t\t__gitcomp \"--force\"\n+\t\t\t;;\n \t\t*)\n \t\t\t;;\n \t\tesac\ndiff --git a/t/t2028-worktree-move.sh b/t/t2028-worktree-move.sh\nindex 3cf5305..d98e542 100755\n--- a/t/t2028-worktree-move.sh\n+++ b/t/t2028-worktree-move.sh\n@@ -89,4 +89,30 @@ test_expect_success 'move main worktree' '\n \ttest_must_fail git worktree move . def\n '\n \n+test_expect_success 'remove main worktree' '\n+\ttest_must_fail git worktree remove .\n+'\n+\n+test_expect_success 'remove locked worktree' '\n+\tgit worktree lock destination &&\n+\ttest_must_fail git worktree remove destination &&\n+\tgit worktree unlock destination\n+'\n+\n+test_expect_success 'remove worktree with dirty tracked file' '\n+\techo dirty >>destination/init.t &&\n+\ttest_must_fail git worktree remove destination\n+'\n+\n+test_expect_success 'remove worktree with untracked file' '\n+\tgit -C destination checkout init.t &&\n+\t: >destination/untracked &&\n+\ttest_must_fail git worktree remove destination\n+'\n+\n+test_expect_success 'force remove worktree with untracked file' '\n+\tgit worktree remove --force destination &&\n+\ttest_path_is_missing destination\n+'\n+\n test_done\n-- \n2.8.2.526.g02eed6d\n\n"},{"id":"290125","messageId":"20160625075433.4608-7-pclouds@gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-1-pclouds@gmail.com","subject":"[PATCH 06/10] worktree.c: add validate_worktree()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-06-25T07:54:29Z","receivedAt":"2016-06-25T08:11:12Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This function is later used by \"worktree move\" and \"worktree remove\"\nto ensure that we have a good connection between the repository and\nthe worktree. For example, if a worktree is moved manually, the\nworktree location recorded in $GIT_DIR/worktrees/.../gitdir is\nincorrect and we should not move that one.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n worktree.c | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n worktree.h |  5 +++++\n 2 files changed, 68 insertions(+)\n\ndiff --git a/worktree.c b/worktree.c\nindex 03f8ce9..d5149d8 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -291,6 +291,69 @@ const char *is_worktree_locked(struct worktree *wt)\n \treturn wt->lock_reason;\n }\n \n+static int report(int quiet, const char *fmt, ...)\n+{\n+\tva_list params;\n+\n+\tif (quiet)\n+\t\treturn -1;\n+\n+\tva_start(params, fmt);\n+\tvfprintf(stderr, fmt, params);\n+\tfputc('\\n', stderr);\n+\tva_end(params);\n+\treturn -1;\n+}\n+\n+int validate_worktree(const struct worktree *wt, int quiet)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *path;\n+\tint err;\n+\n+\tif (is_main_worktree(wt)) {\n+\t\t/*\n+\t\t * Main worktree using .git file to point to the\n+\t\t * repository would make it impossible to know where\n+\t\t * the actual worktree is if this function is executed\n+\t\t * from another worktree. No .git file support for now.\n+\t\t */\n+\t\tstrbuf_addf(&sb, \"%s/.git\", wt->path);\n+\t\tif (!is_directory(sb.buf)) {\n+\t\t\tstrbuf_release(&sb);\n+\t\t\treturn report(quiet, _(\"'%s/.git' at main worktree is not the repository directory\"),\n+\t\t\t\t      wt->path);\n+\t\t}\n+\t\treturn 0;\n+\t}\n+\n+\t/*\n+\t * Make sure \"gitdir\" file points to a real .git file and that\n+\t * file points back here.\n+\t */\n+\tif (!is_absolute_path(wt->path))\n+\t\treturn report(quiet, _(\"'%s' file does not contain absolute path to the worktree location\"),\n+\t\t\t      git_common_path(\"worktrees/%s/gitdir\", wt->id));\n+\n+\tstrbuf_addf(&sb, \"%s/.git\", wt->path);\n+\tif (!file_exists(sb.buf)) {\n+\t\tstrbuf_release(&sb);\n+\t\treturn report(quiet, _(\"'%s/.git' does not exist\"), wt->path);\n+\t}\n+\n+\tpath = read_gitfile_gently(sb.buf, &err);\n+\tstrbuf_release(&sb);\n+\tif (!path)\n+\t\treturn report(quiet, _(\"'%s/.git' is not a .git file, error code %d\"),\n+\t\t\t      wt->path, err);\n+\n+\tif (fspathcmp(path, real_path(git_common_path(\"worktrees/%s\", wt->id))))\n+\t\treturn report(quiet, _(\"'%s' does not point back to\"),\n+\t\t\t      wt->path, git_common_path(\"worktrees/%s\", wt->id));\n+\n+\treturn 0;\n+}\n+\n int is_worktree_being_rebased(const struct worktree *wt,\n \t\t\t      const char *target)\n {\ndiff --git a/worktree.h b/worktree.h\nindex 90e1311..e782ae5 100644\n--- a/worktree.h\n+++ b/worktree.h\n@@ -50,6 +50,11 @@ extern int is_main_worktree(const struct worktree *wt);\n  */\n extern const char *is_worktree_locked(struct worktree *wt);\n \n+/*\n+ * Return zero if the worktree is in good condition.\n+ */\n+extern int validate_worktree(const struct worktree *wt, int quiet);\n+\n /*\n  * Free up the memory for worktree(s)\n  */\n-- \n2.8.2.526.g02eed6d\n\n"},{"id":"290126","messageId":"20160625075433.4608-9-pclouds@gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-1-pclouds@gmail.com","subject":"[PATCH 08/10] worktree: add \"move\" commmand","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-06-25T07:54:31Z","receivedAt":"2016-06-25T08:12:42Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git-worktree.txt         |  7 +++-\n builtin/worktree.c                     | 61 ++++++++++++++++++++++++++++++++++\n contrib/completion/git-completion.bash |  2 +-\n t/t2028-worktree-move.sh               | 30 +++++++++++++++++\n 4 files changed, 98 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt\nindex 0aeb020..b842136 100644\n--- a/Documentation/git-worktree.txt\n+++ b/Documentation/git-worktree.txt\n@@ -12,6 +12,7 @@ SYNOPSIS\n 'git worktree add' [-f] [--detach] [--checkout] [-b <new-branch>] <path> [<branch>]\n 'git worktree list' [--porcelain]\n 'git worktree lock' [--reason <string>] <worktree>\n+'git worktree move' <worktree> <new-path>\n 'git worktree prune' [-n] [-v] [--expire <expire>]\n 'git worktree unlock' <worktree>\n \n@@ -71,6 +72,11 @@ files from being pruned automatically. This also prevents it from\n being moved or deleted. Optionally, specify a reason for the lock\n with `--reason`.\n \n+move::\n+\n+Move a working tree to a new location. Note that the main working tree\n+cannot be moved.\n+\n prune::\n \n Prune working tree information in $GIT_DIR/worktrees.\n@@ -252,7 +258,6 @@ performed manually, such as:\n \n - `remove` to remove a linked working tree and its administrative files (and\n   warn if the working tree is dirty)\n-- `mv` to move or rename a working tree and update its administrative files\n \n GIT\n ---\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex e7d4b04..5d7ca27 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -15,6 +15,7 @@ static const char * const worktree_usage[] = {\n \tN_(\"git worktree add [<options>] <path> [<branch>]\"),\n \tN_(\"git worktree list [<options>]\"),\n \tN_(\"git worktree lock [<options>] <path>\"),\n+\tN_(\"git worktree move <worktree> <new-path>\"),\n \tN_(\"git worktree prune [<options>]\"),\n \tN_(\"git worktree unlock <path>\"),\n \tNULL\n@@ -524,6 +525,64 @@ static int unlock_worktree(int ac, const char **av, const char *prefix)\n \treturn ret;\n }\n \n+static int move_worktree(int ac, const char **av, const char *prefix)\n+{\n+\tstruct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\tstruct worktree **worktrees, *wt;\n+\tstruct strbuf dst = STRBUF_INIT;\n+\tconst char *reason;\n+\n+\tac = parse_options(ac, av, prefix, options, worktree_usage, 0);\n+\tif (ac != 2)\n+\t\tusage_with_options(worktree_usage, options);\n+\n+\tstrbuf_addstr(&dst, prefix_filename(prefix,\n+\t\t\t\t\t    strlen(prefix),\n+\t\t\t\t\t    av[1]));\n+\tif (file_exists(dst.buf))\n+\t\tdie(_(\"target '%s' already exists\"), av[1]);\n+\n+\tworktrees = get_worktrees();\n+\twt = find_worktree(worktrees, prefix, av[0]);\n+\tif (!wt)\n+\t\tdie(_(\"'%s' is not a working directory\"), av[0]);\n+\tif (is_main_worktree(wt))\n+\t\tdie(_(\"'%s' is a main working directory\"), av[0]);\n+\tif ((reason = is_worktree_locked(wt))) {\n+\t\tif (*reason)\n+\t\t\tdie(_(\"already locked, reason: %s\"), reason);\n+\t\tdie(_(\"already locked, no reason\"));\n+\t}\n+\tif (validate_worktree(wt, 0))\n+\t\treturn -1;\n+\n+\t/*\n+\t * First try. Atomically move, and probably cheaper, if both\n+\t * source and target are on the same file system.\n+\t */\n+\tif (rename(wt->path, dst.buf) == -1) {\n+\t\tif (errno != EXDEV)\n+\t\t\tdie_errno(_(\"failed to move '%s' to '%s'\"),\n+\t\t\t\t  wt->path, dst.buf);\n+\n+\t\t/* second try.. */\n+\t\tif (copy_dir_recursively(wt->path, dst.buf))\n+\t\t\tdie(_(\"failed to copy '%s' to '%s'\"),\n+\t\t\t    wt->path, dst.buf);\n+\t\telse {\n+\t\t\tstruct strbuf sb = STRBUF_INIT;\n+\n+\t\t\tstrbuf_addstr(&sb, wt->path);\n+\t\t\t(void)remove_dir_recursively(&sb, 0);\n+\t\t\tstrbuf_release(&sb);\n+\t\t}\n+\t}\n+\n+\treturn update_worktree_location(wt, dst.buf);\n+}\n+\n int cmd_worktree(int ac, const char **av, const char *prefix)\n {\n \tstruct option options[] = {\n@@ -544,5 +603,7 @@ int cmd_worktree(int ac, const char **av, const char *prefix)\n \t\treturn lock_worktree(ac - 1, av + 1, prefix);\n \tif (!strcmp(av[1], \"unlock\"))\n \t\treturn unlock_worktree(ac - 1, av + 1, prefix);\n+\tif (!strcmp(av[1], \"move\"))\n+\t\treturn move_worktree(ac - 1, av + 1, prefix);\n \tusage_with_options(worktree_usage, options);\n }\ndiff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash\nindex 0e3841d..b2028e2 100644\n--- a/contrib/completion/git-completion.bash\n+++ b/contrib/completion/git-completion.bash\n@@ -2597,7 +2597,7 @@ _git_whatchanged ()\n \n _git_worktree ()\n {\n-\tlocal subcommands=\"add list lock prune unlock\"\n+\tlocal subcommands=\"add list lock move prune unlock\"\n \tlocal subcommand=\"$(__git_find_on_cmdline \"$subcommands\")\"\n \tif [ -z \"$subcommand\" ]; then\n \t\t__gitcomp \"$subcommands\"\ndiff --git a/t/t2028-worktree-move.sh b/t/t2028-worktree-move.sh\nindex 68d3fe8..3cf5305 100755\n--- a/t/t2028-worktree-move.sh\n+++ b/t/t2028-worktree-move.sh\n@@ -59,4 +59,34 @@ test_expect_success 'unlock worktree twice' '\n \ttest_path_is_missing .git/worktrees/source/locked\n '\n \n+test_expect_success 'move non-worktree' '\n+\tmkdir abc &&\n+\ttest_must_fail git worktree move abc def\n+'\n+\n+test_expect_success 'move locked worktree' '\n+\tgit worktree lock source &&\n+\ttest_must_fail git worktree move source destination &&\n+\tgit worktree unlock source\n+'\n+\n+test_expect_success 'move worktree' '\n+\tgit worktree move source destination &&\n+\ttest_path_is_missing source &&\n+\tgit worktree list --porcelain | grep \"^worktree\" >actual &&\n+\tcat <<-EOF >expected &&\n+\tworktree $TRASH_DIRECTORY\n+\tworktree $TRASH_DIRECTORY/elsewhere\n+\tworktree $TRASH_DIRECTORY/destination\n+\tEOF\n+\ttest_cmp expected actual &&\n+\tgit -C destination log --format=%s >actual2 &&\n+\techo init >expected2 &&\n+\ttest_cmp expected2 actual2\n+'\n+\n+test_expect_success 'move main worktree' '\n+\ttest_must_fail git worktree move . def\n+'\n+\n test_done\n-- \n2.8.2.526.g02eed6d\n\n"},{"id":"290127","messageId":"20160625075433.4608-8-pclouds@gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-1-pclouds@gmail.com","subject":"[PATCH 07/10] worktree.c: add update_worktree_location()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-06-25T07:54:30Z","receivedAt":"2016-06-25T08:13:18Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n worktree.c | 25 +++++++++++++++++++++++++\n worktree.h |  6 ++++++\n 2 files changed, 31 insertions(+)\n\ndiff --git a/worktree.c b/worktree.c\nindex d5149d8..9b227a4 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -354,6 +354,31 @@ int validate_worktree(const struct worktree *wt, int quiet)\n \treturn 0;\n }\n \n+int update_worktree_location(struct worktree *wt, const char *path_)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\tint ret = 0;\n+\n+\tif (is_main_worktree(wt))\n+\t\treturn 0;\n+\n+\tstrbuf_add_absolute_path(&path, path_);\n+\tif (fspathcmp(wt->path, path.buf)) {\n+\t\tif (!write_file_gently(git_common_path(\"worktrees/%s/gitdir\",\n+\t\t\t\t\t\t       wt->id),\n+\t\t\t\t       \"%s/.git\", real_path(path.buf))) {\n+\t\t\tfree(wt->path);\n+\t\t\twt->path = strbuf_detach(&path, NULL);\n+\t\t\tret = 0;\n+\t\t} else\n+\t\t\tret = error_errno(_(\"failed to update '%s'\"),\n+\t\t\t\t\t  git_common_path(\"worktrees/%s/gitdir\",\n+\t\t\t\t\t\t\t  wt->id));\n+\t}\n+\tstrbuf_release(&path);\n+\treturn ret;\n+}\n+\n int is_worktree_being_rebased(const struct worktree *wt,\n \t\t\t      const char *target)\n {\ndiff --git a/worktree.h b/worktree.h\nindex e782ae5..0477a3d 100644\n--- a/worktree.h\n+++ b/worktree.h\n@@ -55,6 +55,12 @@ extern const char *is_worktree_locked(struct worktree *wt);\n  */\n extern int validate_worktree(const struct worktree *wt, int quiet);\n \n+/*\n+ * Update worktrees/xxx/gitdir with the new path.\n+ */\n+extern int update_worktree_location(struct worktree *wt,\n+\t\t\t\t    const char *path_);\n+\n /*\n  * Free up the memory for worktree(s)\n  */\n-- \n2.8.2.526.g02eed6d\n\n"},{"id":"290128","messageId":"20160625075433.4608-4-pclouds@gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-1-pclouds@gmail.com","subject":"[PATCH 03/10] copy.c: convert bb_(p)error_msg to error(_errno)","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-06-25T07:54:26Z","receivedAt":"2016-06-25T08:13:28Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n copy.c | 85 ++++++++++++++++++++++++------------------------------------------\n 1 file changed, 31 insertions(+), 54 deletions(-)\n\ndiff --git a/copy.c b/copy.c\nindex b7a87f1..074b609 100644\n--- a/copy.c\n+++ b/copy.c\n@@ -82,23 +82,16 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \tsmallint dest_exists = 0;\n \tsmallint ovr;\n \n-\tif (lstat(source, &source_stat) < 0) {\n-\t\tbb_perror_msg(\"can't stat '%s'\", source);\n-\t\treturn -1;\n-\t}\n+\tif (lstat(source, &source_stat) < 0)\n+\t\treturn error_errno(_(\"can't stat '%s'\"), source);\n \n \tif (lstat(dest, &dest_stat) < 0) {\n-\t\tif (errno != ENOENT) {\n-\t\t\tbb_perror_msg(\"can't stat '%s'\", dest);\n-\t\t\treturn -1;\n-\t\t}\n+\t\tif (errno != ENOENT)\n+\t\t\treturn error_errno(_(\"can't stat '%s'\"), dest);\n \t} else {\n-\t\tif (source_stat.st_dev == dest_stat.st_dev\n-\t\t && source_stat.st_ino == dest_stat.st_ino\n-\t\t) {\n-\t\t\tbb_error_msg(\"'%s' and '%s' are the same file\", source, dest);\n-\t\t\treturn -1;\n-\t\t}\n+\t\tif (source_stat.st_dev == dest_stat.st_dev &&\n+\t\t    source_stat.st_ino == dest_stat.st_ino)\n+\t\t\treturn error(_(\"'%s' and '%s' are the same file\"), source, dest);\n \t\tdest_exists = 1;\n \t}\n \n@@ -110,18 +103,14 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \n \t\t/* Did we ever create source ourself before? */\n \t\ttp = is_in_ino_dev_hashtable(&source_stat);\n-\t\tif (tp) {\n+\t\tif (tp)\n \t\t\t/* We did! it's a recursion! man the lifeboats... */\n-\t\t\tbb_error_msg(\"recursion detected, omitting directory '%s'\",\n-\t\t\t\t\tsource);\n-\t\t\treturn -1;\n-\t\t}\n+\t\t\treturn error(_(\"recursion detected, omitting directory '%s'\"),\n+\t\t\t\t     source);\n \n \t\tif (dest_exists) {\n-\t\t\tif (!S_ISDIR(dest_stat.st_mode)) {\n-\t\t\t\tbb_error_msg(\"target '%s' is not a directory\", dest);\n-\t\t\t\treturn -1;\n-\t\t\t}\n+\t\t\tif (!S_ISDIR(dest_stat.st_mode))\n+\t\t\t\treturn error(_(\"target '%s' is not a directory\"), dest);\n \t\t\t/* race here: user can substitute a symlink between\n \t\t\t * this check and actual creation of files inside dest */\n \t\t} else {\n@@ -134,15 +123,12 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\t\tmode |= S_IRWXU;\n \t\t\tif (mkdir(dest, mode) < 0) {\n \t\t\t\tumask(saved_umask);\n-\t\t\t\tbb_perror_msg(\"can't create directory '%s'\", dest);\n-\t\t\t\treturn -1;\n+\t\t\t\treturn error_errno(_(\"can't create directory '%s'\"), dest);\n \t\t\t}\n \t\t\tumask(saved_umask);\n \t\t\t/* need stat info for add_to_ino_dev_hashtable */\n-\t\t\tif (lstat(dest, &dest_stat) < 0) {\n-\t\t\t\tbb_perror_msg(\"can't stat '%s'\", dest);\n-\t\t\t\treturn -1;\n-\t\t\t}\n+\t\t\tif (lstat(dest, &dest_stat) < 0)\n+\t\t\t\treturn error_errno(_(\"can't stat '%s'\"), dest);\n \t\t}\n \t\t/* remember (dev,inode) of each created dir.\n \t\t * NULL: name is not remembered */\n@@ -172,7 +158,7 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\tif (!dest_exists\n \t\t && chmod(dest, source_stat.st_mode & ~saved_umask) < 0\n \t\t) {\n-\t\t\tbb_perror_msg(\"can't preserve %s of '%s'\", \"permissions\", dest);\n+\t\t\terror_errno(_(\"can't preserve permissions of '%s'\"), dest);\n \t\t\t/* retval = -1; - WRONG! copy *WAS* made */\n \t\t}\n \t\tgoto preserve_mode_ugid_time;\n@@ -196,10 +182,8 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\t\t\t\tovr = ask_and_unlink(dest, flags);\n \t\t\t\t\tif (ovr <= 0)\n \t\t\t\t\t\treturn ovr;\n-\t\t\t\t\tif (link(link_target, dest) < 0) {\n-\t\t\t\t\t\tbb_perror_msg(\"can't create link '%s'\", dest);\n-\t\t\t\t\t\treturn -1;\n-\t\t\t\t\t}\n+\t\t\t\t\tif (link(link_target, dest) < 0)\n+\t\t\t\t\t\treturn error_errno(_(\"can't create link '%s'\"), dest);\n \t\t\t\t}\n \t\t\t\treturn 0;\n \t\t\t}\n@@ -238,10 +222,8 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\tif (bb_copyfd_eof(src_fd, dst_fd) == -1)\n \t\t\tretval = -1;\n \t\t/* Careful with writing... */\n-\t\tif (close(dst_fd) < 0) {\n-\t\t\tbb_perror_msg(\"error writing to '%s'\", dest);\n-\t\t\tretval = -1;\n-\t\t}\n+\t\tif (close(dst_fd) < 0)\n+\t\t\tretval = error_errno(_(\"error writing to '%s'\"), dest);\n \t\t/* ...but read size is already checked by bb_copyfd_eof */\n \t\tclose(src_fd);\n \t\t/* \"cp /dev/something new_file\" should not\n@@ -265,12 +247,10 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\tif (lpath) {\n \t\t\tint r = symlink(lpath, dest);\n \t\t\tfree(lpath);\n-\t\t\tif (r < 0) {\n-\t\t\t\tbb_perror_msg(\"can't create symlink '%s'\", dest);\n-\t\t\t\treturn -1;\n-\t\t\t}\n+\t\t\tif (r < 0)\n+\t\t\t\treturn error_errno(_(\"can't create symlink '%s'\"), dest);\n \t\t\tif (lchown(dest, source_stat.st_uid, source_stat.st_gid) < 0)\n-\t\t\t\tbb_perror_msg(\"can't preserve %s of '%s'\", \"ownership\", dest);\n+\t\t\t\terror_errno(_(\"can't preserve %s of '%s'\"), \"ownership\", dest);\n \t\t}\n \t\t/* _Not_ jumping to preserve_mode_ugid_time:\n \t\t * symlinks don't have those */\n@@ -279,14 +259,11 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \tif (S_ISBLK(source_stat.st_mode) || S_ISCHR(source_stat.st_mode)\n \t || S_ISSOCK(source_stat.st_mode) || S_ISFIFO(source_stat.st_mode)\n \t) {\n-\t\tif (mknod(dest, source_stat.st_mode, source_stat.st_rdev) < 0) {\n-\t\t\tbb_perror_msg(\"can't create '%s'\", dest);\n-\t\t\treturn -1;\n-\t\t}\n-\t} else {\n-\t\tbb_error_msg(\"unrecognized file '%s' with mode %x\", source, source_stat.st_mode);\n-\t\treturn -1;\n-\t}\n+\t\tif (mknod(dest, source_stat.st_mode, source_stat.st_rdev) < 0)\n+\t\t\treturn error_errno(_(\"can't create '%s'\"), dest);\n+\t} else\n+\t\treturn error(_(\"unrecognized file '%s' with mode %x\"),\n+\t\t\t     source, source_stat.st_mode);\n \n  preserve_mode_ugid_time:\n \n@@ -297,13 +274,13 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)\n \t\ttimes[1].tv_usec = times[0].tv_usec = 0;\n \t\t/* BTW, utimes sets usec-precision time - just FYI */\n \t\tif (utimes(dest, times) < 0)\n-\t\t\tbb_perror_msg(\"can't preserve %s of '%s'\", \"times\", dest);\n+\t\t\terror_errno(_(\"can't preserve %s of '%s'\"), \"times\", dest);\n \t\tif (chown(dest, source_stat.st_uid, source_stat.st_gid) < 0) {\n \t\t\tsource_stat.st_mode &= ~(S_ISUID | S_ISGID);\n-\t\t\tbb_perror_msg(\"can't preserve %s of '%s'\", \"ownership\", dest);\n+\t\t\terror_errno(_(\"can't preserve %s of '%s'\"), \"ownership\", dest);\n \t\t}\n \t\tif (chmod(dest, source_stat.st_mode) < 0)\n-\t\t\tbb_perror_msg(\"can't preserve %s of '%s'\", \"permissions\", dest);\n+\t\t\terror_errno(_(\"can't preserve %s of '%s'\"), \"permissions\", dest);\n \t}\n \n \treturn retval;\n-- \n2.8.2.526.g02eed6d\n\n"},{"id":"292037","messageId":"CACsJy8Dv=X6Fkqe8U+X_muv4XbsqddiaKo56a+3NZH95CUp2Tw@mail.gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-1-pclouds@gmail.com","subject":"Re: [PATCH 00/10] git worktree (re)move","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2016-07-23T11:09:37Z","receivedAt":"2016-07-23T11:10:17Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Sat, Jun 25, 2016 at 9:54 AM, Nguyễn Thái Ngọc Duy <pclouds@gmail.com> wrote:\n> Now that nd/worktree-lock seems to be on its way to 'next', let's\n> continue to review this one. The first 5 patches bring busybox's copy\n> functionality to git. The last 5 add support functions and the two new\n> commands.\n\nMostly a note to myself, but also for reviewers in case I forget. \"git\nworktree move\" must fix up all core.worktree for all submodules (or\nforbid moving a worktree with submodules until we can do that).\n-- \nDuy\n"},{"id":"325390","messageId":"CAPig+cTR0CODi_mgjYQtQJ7c_BMvMxKq0qqjFeo8D+k77=qXyA@mail.gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-6-pclouds@gmail.com","subject":"Re: [PATCH 05/10] copy.c: convert copy_file() to copy_dir_recursively()","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2017-08-01T18:23:22Z","receivedAt":"2017-08-01T18:23:29Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"Sorry for the (more than a year) late review. I realize that a\nsubsequent version of this patch series renders a few of the review\ncomments meaningless, but I'm including them in case the code is\nrevived later.\n\nOn Sat, Jun 25, 2016 at 3:54 AM, Nguyễn Thái Ngọc Duy <pclouds@gmail.com> wrote:\n> This finally enables busybox's copy_file() code under a new name\n> (because \"copy_file\" is already taken in Git code base). Because this\n> comes from busybox, POSIXy (or even Linuxy) behavior is expected. More\n> changes may be needed for Windows support.\n>\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n> diff --git a/copy.c b/copy.c\n> +/*\n> + * Find out if the last character of a string matches the one given.\n> + * Don't underrun the buffer if the string length is 0.\n>   */\n> +static inline char *last_char_is(const char *s, int c)\n\nIs this function ever used in anything other than a boolean capacity?\nIf not, then perhaps a simpler signature would work?\n\n    static inline int ends_with(const char *s, char c)\n\n(and you could drop the ugly casting in the function body)\n\n> +{\n> +       if (s && *s) {\n> +               size_t sz = strlen(s) - 1;\n> +               s += sz;\n> +               if ( (unsigned char)*s == c)\n\nStyle: if ((...\n\n> +                       return (char*)s;\n> +       }\n> +       return NULL;\n> +}\n> +\n> +static int do_unlink(const char *dest)\n> +{\n> +       int e = errno;\n> +\n> +       if (unlink(dest) < 0) {\n> +               errno = e; /* do not use errno from unlink */\n\nThis comment is repeating the code itself but does not explain _why_.\nMore helpful might be a function-level comment explaining that\ndo_unlink() does not clobber errno.\n\n> +               return error_errno(_(\"can't create '%s'\"), dest);\n\nHowever, it's not really clear what the intention is here. This is\nemitting an error message only if unlink() failed, but the actual\nerror message is from some entirely unrelated operation. Confusing.\n\nAlso, is the idea that 'errno' should be restored no matter what happens?\n\n> +       }\n> +       return 0;\n> +}\n"},{"id":"325391","messageId":"CAPig+cRs0V8O7F1N9yQd6vC8nBabfiOEyOSoP61J5o+gB+KXig@mail.gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-8-pclouds@gmail.com","subject":"Re: [PATCH 07/10] worktree.c: add update_worktree_location()","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2017-08-01T18:23:36Z","receivedAt":"2017-08-01T18:23:42Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sat, Jun 25, 2016 at 3:54 AM, Nguyễn Thái Ngọc Duy <pclouds@gmail.com> wrote:\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n> diff --git a/worktree.c b/worktree.c\n> +int update_worktree_location(struct worktree *wt, const char *path_)\n> +{\n> +       struct strbuf path = STRBUF_INIT;\n> +       int ret = 0;\n> +\n> +       if (is_main_worktree(wt))\n> +               return 0;\n> +\n> +       strbuf_add_absolute_path(&path, path_);\n> +       if (fspathcmp(wt->path, path.buf)) {\n> +               if (!write_file_gently(git_common_path(\"worktrees/%s/gitdir\",\n> +                                                      wt->id),\n> +                                      \"%s/.git\", real_path(path.buf))) {\n> +                       free(wt->path);\n> +                       wt->path = strbuf_detach(&path, NULL);\n> +                       ret = 0;\n\nUseless assignment?\n\n> +               } else\n> +                       ret = error_errno(_(\"failed to update '%s'\"),\n> +                                         git_common_path(\"worktrees/%s/gitdir\",\n> +                                                         wt->id));\n> +       }\n> +       strbuf_release(&path);\n> +       return ret;\n> +}\n"},{"id":"325392","messageId":"CAPig+cRqgzB4tiTb=fHuFBTqo3QEGM3m378PvOse06KdreKo2Q@mail.gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-9-pclouds@gmail.com","subject":"Re: [PATCH 08/10] worktree: add \"move\" commmand","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2017-08-01T18:23:55Z","receivedAt":"2017-08-01T18:24:01Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sat, Jun 25, 2016 at 3:54 AM, Nguyễn Thái Ngọc Duy <pclouds@gmail.com> wrote:\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n> diff --git a/builtin/worktree.c b/builtin/worktree.c\n> +static int move_worktree(int ac, const char **av, const char *prefix)\n> +{\n> + [...]\n> +       if (file_exists(dst.buf))\n> +               die(_(\"target '%s' already exists\"), av[1]);\n> +\n> +       worktrees = get_worktrees();\n\n'worktrees' is being leaked at each 'return'. Probably need to call\nfree_worktrees().\n\n> +       wt = find_worktree(worktrees, prefix, av[0]);\n> +       if (!wt)\n> +               die(_(\"'%s' is not a working directory\"), av[0]);\n> +       if (is_main_worktree(wt))\n> +               die(_(\"'%s' is a main working directory\"), av[0]);\n> +       if ((reason = is_worktree_locked(wt))) {\n> +               if (*reason)\n> +                       die(_(\"already locked, reason: %s\"), reason);\n> +               die(_(\"already locked, no reason\"));\n\nSaying \"already locked\" makes sense for the 'worktree locked' command,\nhowever, for 'worktree move', a simple \"locked\" would be less\nconfusing.\n\n> +       }\n> +       if (validate_worktree(wt, 0))\n> +               return -1;\n> diff --git a/t/t2028-worktree-move.sh b/t/t2028-worktree-move.sh\n> @@ -59,4 +59,34 @@ test_expect_success 'unlock worktree twice' '\n> +test_expect_success 'move non-worktree' '\n> +       mkdir abc &&\n> +       test_must_fail git worktree move abc def\n> +'\n> +\n> +test_expect_success 'move locked worktree' '\n> +       git worktree lock source &&\n> +       test_must_fail git worktree move source destination &&\n> +       git worktree unlock source\n> +'\n\nShould the unlock be wrapped in a test_when_finished()?\n"},{"id":"325394","messageId":"CAPig+cSV9_6j6Nkptma3BewKW8QQcem7gwFCb42VBW4Xe0Vr2w@mail.gmail.com","threadId":"42711","inReplyTo":"20160625075433.4608-11-pclouds@gmail.com","subject":"Re: [PATCH 10/10] worktree: add \"remove\" command","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2017-08-01T18:24:32Z","receivedAt":"2017-08-01T18:24:38Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sat, Jun 25, 2016 at 3:54 AM, Nguyễn Thái Ngọc Duy <pclouds@gmail.com> wrote:\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n> diff --git a/builtin/worktree.c b/builtin/worktree.c\n> @@ -600,6 +601,81 @@ static int move_worktree(int ac, const char **av, const char *prefix)\n> +static int remove_worktree(int ac, const char **av, const char *prefix)\n> +{\n> +       worktrees = get_worktrees();\n> +       wt = find_worktree(worktrees, prefix, av[0]);\n> +       if (!wt)\n> +               die(_(\"'%s' is not a working directory\"), av[0]);\n> +       if (is_main_worktree(wt))\n> +               die(_(\"'%s' is a main working directory\"), av[0]);\n> +       if ((reason = is_worktree_locked(wt))) {\n> +               if (*reason)\n> +                       die(_(\"already locked, reason: %s\"), reason);\n> +               die(_(\"already locked, no reason\"));\n\n\"already locked\" makes sense for 'worktree lock' command, but not\nhere. A simple \"locked\" would be clearer.\n\n> +       }\n> +       if (validate_worktree(wt, 0))\n> +               return -1;\n\nLeaking 'worktrees'. free_worktrees(...) recommended.\n\n> +       strbuf_addstr(&sb, wt->path);\n> +       if (remove_dir_recursively(&sb, 0)) {\n> +               error_errno(_(\"failed to delete '%s'\"), sb.buf);\n> +               ret = -1;\n> +       }\n> +       strbuf_reset(&sb);\n> +       strbuf_addstr(&sb, git_common_path(\"worktrees/%s\", wt->id));\n> +       if (remove_dir_recursively(&sb, 0)) {\n> +               error_errno(_(\"failed to delete '%s'\"), sb.buf);\n> +               ret = -1;\n> +       }\n> +       strbuf_release(&sb);\n> +       free_worktrees(worktrees);\n> +       return ret;\n> +}\n> diff --git a/t/t2028-worktree-move.sh b/t/t2028-worktree-move.sh\n> @@ -89,4 +89,30 @@ test_expect_success 'move main worktree' '\n> +test_expect_success 'remove locked worktree' '\n> +       git worktree lock destination &&\n> +       test_must_fail git worktree remove destination &&\n> +       git worktree unlock destination\n> +'\n\n'unlock' within test_when_finished(), perhaps?\n\n> +test_expect_success 'remove worktree with dirty tracked file' '\n> +       echo dirty >>destination/init.t &&\n> +       test_must_fail git worktree remove destination\n> +'\n> +\n> +test_expect_success 'remove worktree with untracked file' '\n> +       git -C destination checkout init.t &&\n\nPerhaps this reversion of 'init.t' belongs in the preceding test which\nmodified it (wrapped in test_when_finished())?\n\n> +       : >destination/untracked &&\n> +       test_must_fail git worktree remove destination\n> +'\n> +\n> +test_expect_success 'force remove worktree with untracked file' '\n> +       git worktree remove --force destination &&\n> +       test_path_is_missing destination\n> +'\n"}]}