This is basically a resend from last time, which happened during rc
time. It adds 4 more commands, basically cleaning up the "TODO" list
in git-worktree.txt.
So far I've only actually used move and remove (and maybe unlock once
because worktree-add failed on me and I had to unlock it manually).
And I don't get to move worktrees a lot either so not really extensive
testing.
[01/25] usage.c: move format processing out of die_errno()
[02/25] usage.c: add sys_error() that prints strerror() automatically
[03/25] copy.c: import copy_file() from busybox
[04/25] copy.c: delete unused code in copy_file()
[05/25] copy.c: convert bb_(p)error_msg to (sys_)error
[06/25] copy.c: style fix
[07/25] copy.c: convert copy_file() to copy_dir_recursively()
[08/25] completion: support git-worktree
[09/25] git-worktree.txt: keep subcommand listing in alphabetical order
[10/25] path.c: add git_common_path() and strbuf_git_common_path()
[11/25] worktree.c: use is_dot_or_dotdot()
[12/25] worktree.c: store "id" instead of "git_dir"
[13/25] worktree.c: add clear_worktree()
[14/25] worktree.c: add find_worktree_by_path()
[15/25] worktree.c: add is_main_worktree()
[16/25] worktree.c: add validate_worktree()
[17/25] worktree.c: add update_worktree_location()
[18/25] worktree.c: add is_worktree_locked()
[19/25] worktree: avoid 0{40}, too many zeroes, hard to read
[20/25] worktree: simplify prefixing paths
[21/25] worktree: add "lock" command
[22/25] worktree: add "unlock" command
[23/25] worktree: add "move" commmand
[24/25] worktree move: accept destination as directory
[25/25] worktree: add "remove" command
Total 11 files changed, 1028 insertions(+), 48 deletions(-)
@@ -65,3 +65,334 @@ int copy_file_with_time(const char *dst, const char *src, int mode)returncopy_times(dst,src);returnstatus;}++#if 0+/* Return:+*-1error,copynotmade+*0copyismadeoruseranswered"no"ininteractivemode+*(failurestopreservemode/owner/timesarenotreportedinexitcode)+*/+intFAST_FUNCcopy_file(constchar*source,constchar*dest,intflags)+{+/* This is a recursive function, try to minimize stack usage */+/* NB: each struct stat is ~100 bytes */+structstatsource_stat;+structstatdest_stat;+smallintretval=0;+smallintdest_exists=0;+smallintovr;++/* Inverse of cp -d ("cp without -d") */+#define FLAGS_DEREF (flags & (FILEUTILS_DEREFERENCE + FILEUTILS_DEREFERENCE_L0))++if((FLAGS_DEREF?stat:lstat)(source,&source_stat)<0){+/* This may be a dangling symlink.+*Making[sym]linkstodanglingsymlinksworks,so...*/+if(flags&(FILEUTILS_MAKE_SOFTLINK|FILEUTILS_MAKE_HARDLINK))+gotomake_links;+bb_perror_msg("can't stat '%s'",source);+return-1;+}++if(lstat(dest,&dest_stat)<0){+if(errno!=ENOENT){+bb_perror_msg("can't stat '%s'",dest);+return-1;+}+}else{+if(source_stat.st_dev==dest_stat.st_dev+&&source_stat.st_ino==dest_stat.st_ino+){+bb_error_msg("'%s' and '%s' are the same file",source,dest);+return-1;+}+dest_exists=1;+}++#if ENABLE_SELINUX+if((flags&FILEUTILS_PRESERVE_SECURITY_CONTEXT)&&is_selinux_enabled()>0){+security_context_tcon;+if(lgetfilecon(source,&con)>=0){+if(setfscreatecon(con)<0){+bb_perror_msg("can't set setfscreatecon %s",con);+freecon(con);+return-1;+}+}elseif(errno==ENOTSUP||errno==ENODATA){+setfscreatecon_or_die(NULL);+}else{+bb_perror_msg("can't lgetfilecon %s",source);+return-1;+}+}+#endif++if(S_ISDIR(source_stat.st_mode)){+DIR*dp;+constchar*tp;+structdirent*d;+mode_tsaved_umask=0;++if(!(flags&FILEUTILS_RECUR)){+bb_error_msg("omitting directory '%s'",source);+return-1;+}++/* Did we ever create source ourself before? */+tp=is_in_ino_dev_hashtable(&source_stat);+if(tp){+/* We did! it's a recursion! man the lifeboats... */+bb_error_msg("recursion detected, omitting directory '%s'",+source);+return-1;+}++if(dest_exists){+if(!S_ISDIR(dest_stat.st_mode)){+bb_error_msg("target '%s' is not a directory",dest);+return-1;+}+/* race here: user can substitute a symlink between+*thischeckandactualcreationoffilesinsidedest*/+}else{+/* Create DEST */+mode_tmode;+saved_umask=umask(0);++mode=source_stat.st_mode;+if(!(flags&FILEUTILS_PRESERVE_STATUS))+mode=source_stat.st_mode&~saved_umask;+/* Allow owner to access new dir (at least for now) */+mode|=S_IRWXU;+if(mkdir(dest,mode)<0){+umask(saved_umask);+bb_perror_msg("can't create directory '%s'",dest);+return-1;+}+umask(saved_umask);+/* need stat info for add_to_ino_dev_hashtable */+if(lstat(dest,&dest_stat)<0){+bb_perror_msg("can't stat '%s'",dest);+return-1;+}+}+/* remember (dev,inode) of each created dir.+*NULL:nameisnotremembered*/+add_to_ino_dev_hashtable(&dest_stat,NULL);++/* Recursively copy files in SOURCE */+dp=opendir(source);+if(dp==NULL){+retval=-1;+gotopreserve_mode_ugid_time;+}++while((d=readdir(dp))!=NULL){+char*new_source,*new_dest;++new_source=concat_subpath_file(source,d->d_name);+if(new_source==NULL)+continue;+new_dest=concat_path_file(dest,d->d_name);+if(copy_file(new_source,new_dest,flags&~FILEUTILS_DEREFERENCE_L0)<0)+retval=-1;+free(new_source);+free(new_dest);+}+closedir(dp);++if(!dest_exists+&&chmod(dest,source_stat.st_mode&~saved_umask)<0+){+bb_perror_msg("can't preserve %s of '%s'","permissions",dest);+/* retval = -1; - WRONG! copy *WAS* made */+}+gotopreserve_mode_ugid_time;+}++if(flags&(FILEUTILS_MAKE_SOFTLINK|FILEUTILS_MAKE_HARDLINK)){+int(*lf)(constchar*oldpath,constchar*newpath);+make_links:+/* Hmm... maybe+*if(DEREF&&MAKE_SOFTLINK)source=realpath(source)?+*(butrealpathreturnsNULLondanglingsymlinks...)*/+lf=(flags&FILEUTILS_MAKE_SOFTLINK)?symlink:link;+if(lf(source,dest)<0){+ovr=ask_and_unlink(dest,flags);+if(ovr<=0)+returnovr;+if(lf(source,dest)<0){+bb_perror_msg("can't create link '%s'",dest);+return-1;+}+}+/* _Not_ jumping to preserve_mode_ugid_time:+*(sym)linksdon'thavethose*/+return0;+}++if(/* "cp thing1 thing2" without -R: just open and read() from thing1 */+!(flags&FILEUTILS_RECUR)+/* "cp [-opts] regular_file thing2" */+||S_ISREG(source_stat.st_mode)+/* DEREF uses stat, which never returns S_ISLNK() == true.+*Sothebelowisnevertrue:*/+/* || (FLAGS_DEREF && S_ISLNK(source_stat.st_mode)) */+){+intsrc_fd;+intdst_fd;+mode_tnew_mode;++if(!FLAGS_DEREF&&S_ISLNK(source_stat.st_mode)){+/* "cp -d symlink dst": create a link */+gotodont_cat;+}++if(ENABLE_FEATURE_PRESERVE_HARDLINKS&&!FLAGS_DEREF){+constchar*link_target;+link_target=is_in_ino_dev_hashtable(&source_stat);+if(link_target){+if(link(link_target,dest)<0){+ovr=ask_and_unlink(dest,flags);+if(ovr<=0)+returnovr;+if(link(link_target,dest)<0){+bb_perror_msg("can't create link '%s'",dest);+return-1;+}+}+return0;+}+add_to_ino_dev_hashtable(&source_stat,dest);+}++src_fd=open_or_warn(source,O_RDONLY);+if(src_fd<0)+return-1;++/* Do not try to open with weird mode fields */+new_mode=source_stat.st_mode;+if(!S_ISREG(source_stat.st_mode))+new_mode=0666;++// POSIX way is a security problem versus (sym)link attacks+if(!ENABLE_FEATURE_NON_POSIX_CP){+dst_fd=open(dest,O_WRONLY|O_CREAT|O_TRUNC,new_mode);+}else{/* safe way: */+dst_fd=open(dest,O_WRONLY|O_CREAT|O_EXCL,new_mode);+}+if(dst_fd==-1){+ovr=ask_and_unlink(dest,flags);+if(ovr<=0){+close(src_fd);+returnovr;+}+/* It shouldn't exist. If it exists, do not open (symlink attack?) */+dst_fd=open3_or_warn(dest,O_WRONLY|O_CREAT|O_EXCL,new_mode);+if(dst_fd<0){+close(src_fd);+return-1;+}+}++#if ENABLE_SELINUX+if((flags&(FILEUTILS_PRESERVE_SECURITY_CONTEXT|FILEUTILS_SET_SECURITY_CONTEXT))+&&is_selinux_enabled()>0+){+security_context_tcon;+if(getfscreatecon(&con)==-1){+bb_perror_msg("getfscreatecon");+return-1;+}+if(con){+if(setfilecon(dest,con)==-1){+bb_perror_msg("setfilecon:%s,%s",dest,con);+freecon(con);+return-1;+}+freecon(con);+}+}+#endif+if(bb_copyfd_eof(src_fd,dst_fd)==-1)+retval=-1;+/* Careful with writing... */+if(close(dst_fd)<0){+bb_perror_msg("error writing to '%s'",dest);+retval=-1;+}+/* ...but read size is already checked by bb_copyfd_eof */+close(src_fd);+/* "cp /dev/something new_file" should not+*copymodeof/dev/something*/+if(!S_ISREG(source_stat.st_mode))+returnretval;+gotopreserve_mode_ugid_time;+}+dont_cat:++/* Source is a symlink or a special file */+/* We are lazy here, a bit lax with races... */+if(dest_exists){+errno=EEXIST;+ovr=ask_and_unlink(dest,flags);+if(ovr<=0)+returnovr;+}+if(S_ISLNK(source_stat.st_mode)){+char*lpath=xmalloc_readlink_or_warn(source);+if(lpath){+intr=symlink(lpath,dest);+free(lpath);+if(r<0){+bb_perror_msg("can't create symlink '%s'",dest);+return-1;+}+if(flags&FILEUTILS_PRESERVE_STATUS)+if(lchown(dest,source_stat.st_uid,source_stat.st_gid)<0)+bb_perror_msg("can't preserve %s of '%s'","ownership",dest);+}+/* _Not_ jumping to preserve_mode_ugid_time:+*symlinksdon'thavethose*/+return0;+}+if(S_ISBLK(source_stat.st_mode)||S_ISCHR(source_stat.st_mode)+||S_ISSOCK(source_stat.st_mode)||S_ISFIFO(source_stat.st_mode)+){+if(mknod(dest,source_stat.st_mode,source_stat.st_rdev)<0){+bb_perror_msg("can't create '%s'",dest);+return-1;+}+}else{+bb_error_msg("unrecognized file '%s' with mode %x",source,source_stat.st_mode);+return-1;+}++preserve_mode_ugid_time:++if(flags&FILEUTILS_PRESERVE_STATUS+/* Cannot happen: */+/* && !(flags & (FILEUTILS_MAKE_SOFTLINK|FILEUTILS_MAKE_HARDLINK)) */+){+structtimevaltimes[2];++times[1].tv_sec=times[0].tv_sec=source_stat.st_mtime;+times[1].tv_usec=times[0].tv_usec=0;+/* BTW, utimes sets usec-precision time - just FYI */+if(utimes(dest,times)<0)+bb_perror_msg("can't preserve %s of '%s'","times",dest);+if(chown(dest,source_stat.st_uid,source_stat.st_gid)<0){+source_stat.st_mode&=~(S_ISUID|S_ISGID);+bb_perror_msg("can't preserve %s of '%s'","ownership",dest);+}+if(chmod(dest,source_stat.st_mode)<0)+bb_perror_msg("can't preserve %s of '%s'","permissions",dest);+}++if(flags&FILEUTILS_VERBOSE){+printf("'%s' -> '%s'\n",source,dest);+}++returnretval;+}+#endif
@@ -82,14 +82,7 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)smallintdest_exists=0;smallintovr;-/* Inverse of cp -d ("cp without -d") */-#define FLAGS_DEREF (flags & (FILEUTILS_DEREFERENCE + FILEUTILS_DEREFERENCE_L0))--if((FLAGS_DEREF?stat:lstat)(source,&source_stat)<0){-/* This may be a dangling symlink.-*Making[sym]linkstodanglingsymlinksworks,so...*/-if(flags&(FILEUTILS_MAKE_SOFTLINK|FILEUTILS_MAKE_HARDLINK))-gotomake_links;+if(lstat(source,&source_stat)<0){bb_perror_msg("can't stat '%s'",source);return-1;}
@@ -109,35 +102,12 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)dest_exists=1;}-#if ENABLE_SELINUX-if((flags&FILEUTILS_PRESERVE_SECURITY_CONTEXT)&&is_selinux_enabled()>0){-security_context_tcon;-if(lgetfilecon(source,&con)>=0){-if(setfscreatecon(con)<0){-bb_perror_msg("can't set setfscreatecon %s",con);-freecon(con);-return-1;-}-}elseif(errno==ENOTSUP||errno==ENODATA){-setfscreatecon_or_die(NULL);-}else{-bb_perror_msg("can't lgetfilecon %s",source);-return-1;-}-}-#endif-if(S_ISDIR(source_stat.st_mode)){DIR*dp;constchar*tp;structdirent*d;mode_tsaved_umask=0;-if(!(flags&FILEUTILS_RECUR)){-bb_error_msg("omitting directory '%s'",source);-return-1;-}-/* Did we ever create source ourself before? */tp=is_in_ino_dev_hashtable(&source_stat);if(tp){
@@ -160,8 +130,6 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)saved_umask=umask(0);mode=source_stat.st_mode;-if(!(flags&FILEUTILS_PRESERVE_STATUS))-mode=source_stat.st_mode&~saved_umask;/* Allow owner to access new dir (at least for now) */mode|=S_IRWXU;if(mkdir(dest,mode)<0){
@@ -210,45 +178,17 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)gotopreserve_mode_ugid_time;}-if(flags&(FILEUTILS_MAKE_SOFTLINK|FILEUTILS_MAKE_HARDLINK)){-int(*lf)(constchar*oldpath,constchar*newpath);-make_links:-/* Hmm... maybe-*if(DEREF&&MAKE_SOFTLINK)source=realpath(source)?-*(butrealpathreturnsNULLondanglingsymlinks...)*/-lf=(flags&FILEUTILS_MAKE_SOFTLINK)?symlink:link;-if(lf(source,dest)<0){-ovr=ask_and_unlink(dest,flags);-if(ovr<=0)-returnovr;-if(lf(source,dest)<0){-bb_perror_msg("can't create link '%s'",dest);-return-1;-}-}-/* _Not_ jumping to preserve_mode_ugid_time:-*(sym)linksdon'thavethose*/-return0;-}--if(/* "cp thing1 thing2" without -R: just open and read() from thing1 */-!(flags&FILEUTILS_RECUR)-/* "cp [-opts] regular_file thing2" */-||S_ISREG(source_stat.st_mode)-/* DEREF uses stat, which never returns S_ISLNK() == true.-*Sothebelowisnevertrue:*/-/* || (FLAGS_DEREF && S_ISLNK(source_stat.st_mode)) */-){+if(S_ISREG(source_stat.st_mode)){/* "cp [-opts] regular_file thing2" */intsrc_fd;intdst_fd;mode_tnew_mode;-if(!FLAGS_DEREF&&S_ISLNK(source_stat.st_mode)){+if(S_ISLNK(source_stat.st_mode)){/* "cp -d symlink dst": create a link */gotodont_cat;}-if(ENABLE_FEATURE_PRESERVE_HARDLINKS&&!FLAGS_DEREF){+if(ENABLE_FEATURE_PRESERVE_HARDLINKS){constchar*link_target;link_target=is_in_ino_dev_hashtable(&source_stat);if(link_target){
@@ -295,25 +235,6 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)}}-#if ENABLE_SELINUX-if((flags&(FILEUTILS_PRESERVE_SECURITY_CONTEXT|FILEUTILS_SET_SECURITY_CONTEXT))-&&is_selinux_enabled()>0-){-security_context_tcon;-if(getfscreatecon(&con)==-1){-bb_perror_msg("getfscreatecon");-return-1;-}-if(con){-if(setfilecon(dest,con)==-1){-bb_perror_msg("setfilecon:%s,%s",dest,con);-freecon(con);-return-1;-}-freecon(con);-}-}-#endifif(bb_copyfd_eof(src_fd,dst_fd)==-1)retval=-1;/* Careful with writing... */
@@ -348,9 +269,8 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)bb_perror_msg("can't create symlink '%s'",dest);return-1;}-if(flags&FILEUTILS_PRESERVE_STATUS)-if(lchown(dest,source_stat.st_uid,source_stat.st_gid)<0)-bb_perror_msg("can't preserve %s of '%s'","ownership",dest);+if(lchown(dest,source_stat.st_uid,source_stat.st_gid)<0)+bb_perror_msg("can't preserve %s of '%s'","ownership",dest);}/* _Not_ jumping to preserve_mode_ugid_time:*symlinksdon'thavethose*/
@@ -82,23 +82,16 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)smallintdest_exists=0;smallintovr;-if(lstat(source,&source_stat)<0){-bb_perror_msg("can't stat '%s'",source);-return-1;-}+if(lstat(source,&source_stat)<0)+returnsys_error(_("can't stat '%s'"),source);if(lstat(dest,&dest_stat)<0){-if(errno!=ENOENT){-bb_perror_msg("can't stat '%s'",dest);-return-1;-}+if(errno!=ENOENT)+returnsys_error(_("can't stat '%s'"),dest);}else{-if(source_stat.st_dev==dest_stat.st_dev-&&source_stat.st_ino==dest_stat.st_ino-){-bb_error_msg("'%s' and '%s' are the same file",source,dest);-return-1;-}+if(source_stat.st_dev==dest_stat.st_dev&&+source_stat.st_ino==dest_stat.st_ino)+returnerror(_("'%s' and '%s' are the same file"),source,dest);dest_exists=1;}
@@ -110,18 +103,14 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)/* Did we ever create source ourself before? */tp=is_in_ino_dev_hashtable(&source_stat);-if(tp){+if(tp)/* We did! it's a recursion! man the lifeboats... */-bb_error_msg("recursion detected, omitting directory '%s'",-source);-return-1;-}+returnerror(_("recursion detected, omitting directory '%s'"),+source);if(dest_exists){-if(!S_ISDIR(dest_stat.st_mode)){-bb_error_msg("target '%s' is not a directory",dest);-return-1;-}+if(!S_ISDIR(dest_stat.st_mode))+returnerror(_("target '%s' is not a directory"),dest);/* race here: user can substitute a symlink between*thischeckandactualcreationoffilesinsidedest*/}else{
@@ -134,15 +123,12 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)mode|=S_IRWXU;if(mkdir(dest,mode)<0){umask(saved_umask);-bb_perror_msg("can't create directory '%s'",dest);-return-1;+returnsys_error(_("can't create directory '%s'"),dest);}umask(saved_umask);/* need stat info for add_to_ino_dev_hashtable */-if(lstat(dest,&dest_stat)<0){-bb_perror_msg("can't stat '%s'",dest);-return-1;-}+if(lstat(dest,&dest_stat)<0)+returnsys_error(_("can't stat '%s'"),dest);}/* remember (dev,inode) of each created dir.*NULL:nameisnotremembered*/
@@ -172,7 +158,7 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)if(!dest_exists&&chmod(dest,source_stat.st_mode&~saved_umask)<0){-bb_perror_msg("can't preserve %s of '%s'","permissions",dest);+sys_error(_("can't preserve permissions of '%s'"),dest);/* retval = -1; - WRONG! copy *WAS* made */}gotopreserve_mode_ugid_time;
@@ -196,10 +182,8 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)ovr=ask_and_unlink(dest,flags);if(ovr<=0)returnovr;-if(link(link_target,dest)<0){-bb_perror_msg("can't create link '%s'",dest);-return-1;-}+if(link(link_target,dest)<0)+returnsys_error(_("can't create link '%s'"),dest);}return0;}
@@ -238,10 +222,8 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)if(bb_copyfd_eof(src_fd,dst_fd)==-1)retval=-1;/* Careful with writing... */-if(close(dst_fd)<0){-bb_perror_msg("error writing to '%s'",dest);-retval=-1;-}+if(close(dst_fd)<0)+retval=sys_error(_("error writing to '%s'"),dest);/* ...but read size is already checked by bb_copyfd_eof */close(src_fd);/* "cp /dev/something new_file" should not
@@ -265,12 +247,10 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)if(lpath){intr=symlink(lpath,dest);free(lpath);-if(r<0){-bb_perror_msg("can't create symlink '%s'",dest);-return-1;-}+if(r<0)+returnsys_error(_("can't create symlink '%s'"),dest);if(lchown(dest,source_stat.st_uid,source_stat.st_gid)<0)-bb_perror_msg("can't preserve %s of '%s'","ownership",dest);+sys_error(_("can't preserve %s of '%s'"),"ownership",dest);}/* _Not_ jumping to preserve_mode_ugid_time:*symlinksdon'thavethose*/
@@ -279,14 +259,11 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)if(S_ISBLK(source_stat.st_mode)||S_ISCHR(source_stat.st_mode)||S_ISSOCK(source_stat.st_mode)||S_ISFIFO(source_stat.st_mode)){-if(mknod(dest,source_stat.st_mode,source_stat.st_rdev)<0){-bb_perror_msg("can't create '%s'",dest);-return-1;-}-}else{-bb_error_msg("unrecognized file '%s' with mode %x",source,source_stat.st_mode);-return-1;-}+if(mknod(dest,source_stat.st_mode,source_stat.st_rdev)<0)+returnsys_error(_("can't create '%s'"),dest);+}else+returnerror(_("unrecognized file '%s' with mode %x"),+source,source_stat.st_mode);preserve_mode_ugid_time:
@@ -297,13 +274,13 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)times[1].tv_usec=times[0].tv_usec=0;/* BTW, utimes sets usec-precision time - just FYI */if(utimes(dest,times)<0)-bb_perror_msg("can't preserve %s of '%s'","times",dest);+sys_error(_("can't preserve %s of '%s'"),"times",dest);if(chown(dest,source_stat.st_uid,source_stat.st_gid)<0){source_stat.st_mode&=~(S_ISUID|S_ISGID);-bb_perror_msg("can't preserve %s of '%s'","ownership",dest);+sys_error(_("can't preserve %s of '%s'"),"ownership",dest);}if(chmod(dest,source_stat.st_mode)<0)-bb_perror_msg("can't preserve %s of '%s'","permissions",dest);+sys_error(_("can't preserve %s of '%s'"),"permissions",dest);}returnretval;
This finally enables busybox's copy_file() code under a new name
(because "copy_file" is already taken in Git code base). Because this
comes from busybox, POSIXy (or even Linuxy) behavior is expected. More
changes may be needed for Windows support.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
cache.h | 1 +
copy.c | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++------------
2 files changed, 179 insertions(+), 38 deletions(-)
@@ -66,21 +68,126 @@ int copy_file_with_time(const char *dst, const char *src, int mode)returnstatus;}-#if 0-/* Return:-*-1error,copynotmade-*0copyismadeoruseranswered"no"ininteractivemode-*(failurestopreservemode/owner/timesarenotreportedinexitcode)+structinode_key{+structhashmap_entryentry;+ino_tino;+dev_tdev;+/*+*Reportedly,oncramfsafileandadircanhavesameino.+*Needtoalsoremember"file/dir"bit:+*/+charisdir;/* bool */+};++structinode_value{+structinode_keykey;+charname[FLEX_ARRAY];+};++#define HASH_SIZE 311u /* Should be prime */+staticinlineunsignedhash_inode(ino_ti)+{+returni%HASH_SIZE;+}++staticintinode_cmp(constvoid*entry,constvoid*entry_or_key,+constvoid*keydata)+{+conststructinode_value*inode=entry;+conststructinode_key*key=entry_or_key;++return!(inode->key.ino==key->ino&&+inode->key.dev==key->dev&&+inode->key.isdir==key->isdir);+}++staticconstchar*is_in_ino_dev_hashtable(conststructhashmap*map,+conststructstat*st)+{+structinode_keykey;+structinode_value*value;++key.entry.hash=hash_inode(st->st_ino);+key.ino=st->st_ino;+key.dev=st->st_dev;+key.isdir=!!S_ISDIR(st->st_mode);+value=hashmap_get(map,&key,NULL);+returnvalue?value->name:NULL;+}++staticvoidadd_to_ino_dev_hashtable(structhashmap*map,+conststructstat*st,+constchar*path)+{+structinode_value*v;+intlen=strlen(path);++v=xmalloc(offsetof(structinode_value,name)+len+1);+v->key.entry.hash=hash_inode(st->st_ino);+v->key.ino=st->st_ino;+v->key.dev=st->st_dev;+v->key.isdir=!!S_ISDIR(st->st_mode);+memcpy(v->name,path,len+1);+hashmap_add(map,v);+}++/*+*Findoutifthelastcharacterofastringmatchestheonegiven.+*Don'tunderrunthebufferifthestringlengthis0.*/-intFAST_FUNCcopy_file(constchar*source,constchar*dest,intflags)+staticinlinechar*last_char_is(constchar*s,intc)+{+if(s&&*s){+size_tsz=strlen(s)-1;+s+=sz;+if((unsignedchar)*s==c)+return(char*)s;+}+returnNULL;+}++staticinlinechar*concat_path_file(constchar*path,constchar*filename)+{+structstrbufsb=STRBUF_INIT;+char*lc;++if(!path)+path="";+lc=last_char_is(path,'/');+while(*filename=='/')+filename++;+strbuf_addf(&sb,"%s%s%s",path,(lc==NULL?"/":""),filename);+returnstrbuf_detach(&sb,NULL);+}++staticchar*concat_subpath_file(constchar*path,constchar*f)+{+if(f&&is_dot_or_dotdot(f))+returnNULL;+returnconcat_path_file(path,f);+}++staticintdo_unlink(constchar*dest)+{+inte=errno;++if(unlink(dest)<0){+errno=e;/* do not use errno from unlink */+returnsys_error(_("can't create '%s'"),dest);+}+return0;+}++staticintcopy_dir_1(structhashmap*inode_map,+constchar*source,+constchar*dest){/* This is a recursive function, try to minimize stack usage */-/* NB: each struct stat is ~100 bytes */structstatsource_stat;structstatdest_stat;-smallintretval=0;-smallintdest_exists=0;-smallintovr;+intretval=0;+intdest_exists=0;+intovr;if(lstat(source,&source_stat)<0)returnsys_error(_("can't stat '%s'"),source);
@@ -102,7 +209,7 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)mode_tsaved_umask=0;/* Did we ever create source ourself before? */-tp=is_in_ino_dev_hashtable(&source_stat);+tp=is_in_ino_dev_hashtable(inode_map,&source_stat);if(tp)/* We did! it's a recursion! man the lifeboats... */returnerror(_("recursion detected, omitting directory '%s'"),
@@ -132,11 +239,12 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)if(lstat(dest,&dest_stat)<0)returnsys_error(_("can't stat '%s'"),dest);}+/**remember(dev,inode)ofeachcreateddir.nameis*notremembered*/-add_to_ino_dev_hashtable(&dest_stat,NULL);+add_to_ino_dev_hashtable(inode_map,&dest_stat,"");/* Recursively copy files in SOURCE */dp=opendir(source);
@@ -152,7 +260,7 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)if(!new_source)continue;new_dest=concat_path_file(dest,d->d_name);-if(copy_file(new_source,new_dest,flags&~FILEUTILS_DEREFERENCE_L0)<0)+if(copy_dir_1(inode_map,new_source,new_dest)<0)retval=-1;free(new_source);free(new_dest);
@@ -177,53 +285,57 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)gotodont_cat;}-if(ENABLE_FEATURE_PRESERVE_HARDLINKS){+if(1/*ENABLE_FEATURE_PRESERVE_HARDLINKS*/){constchar*link_target;-link_target=is_in_ino_dev_hashtable(&source_stat);+link_target=is_in_ino_dev_hashtable(inode_map,&source_stat);if(link_target){if(link(link_target,dest)<0){-ovr=ask_and_unlink(dest,flags);-if(ovr<=0)+ovr=do_unlink(dest);+if(ovr<0)returnovr;if(link(link_target,dest)<0)returnsys_error(_("can't create link '%s'"),dest);}return0;}-add_to_ino_dev_hashtable(&source_stat,dest);+add_to_ino_dev_hashtable(inode_map,&source_stat,dest);}-src_fd=open_or_warn(source,O_RDONLY);+src_fd=open(source,O_RDONLY);if(src_fd<0)-return-1;+returnsys_error(_("can't open '%s'"),source);/* Do not try to open with weird mode fields */new_mode=source_stat.st_mode;if(!S_ISREG(source_stat.st_mode))new_mode=0666;-/* POSIX way is a security problem versus (sym)link attacks */-if(!ENABLE_FEATURE_NON_POSIX_CP){-dst_fd=open(dest,O_WRONLY|O_CREAT|O_TRUNC,new_mode);-}else{/* safe way: */-dst_fd=open(dest,O_WRONLY|O_CREAT|O_EXCL,new_mode);-}+dst_fd=open(dest,O_WRONLY|O_CREAT|O_EXCL,new_mode);if(dst_fd==-1){-ovr=ask_and_unlink(dest,flags);-if(ovr<=0){+ovr=do_unlink(dest);+if(ovr<0){close(src_fd);returnovr;}/* It shouldn't exist. If it exists, do not open (symlink attack?) */-dst_fd=open3_or_warn(dest,O_WRONLY|O_CREAT|O_EXCL,new_mode);+dst_fd=open(dest,O_WRONLY|O_CREAT|O_EXCL,new_mode);if(dst_fd<0){close(src_fd);-return-1;+returnsys_error(_("can't open '%s'"),dest);}}-if(bb_copyfd_eof(src_fd,dst_fd)==-1)+switch(copy_fd(src_fd,dst_fd)){+caseCOPY_READ_ERROR:+error(_("copy-fd: read returned %s"),strerror(errno));retval=-1;+break;+caseCOPY_WRITE_ERROR:+error(_("copy-fd: write returned %s"),strerror(errno));+retval=-1;+break;+}+/* Careful with writing... */if(close(dst_fd)<0)retval=sys_error(_("error writing to '%s'"),dest);
@@ -243,19 +355,28 @@ dont_cat:/* We are lazy here, a bit lax with races... */if(dest_exists){errno=EEXIST;-ovr=ask_and_unlink(dest,flags);-if(ovr<=0)+ovr=do_unlink(dest);+if(ovr<0)returnovr;}if(S_ISLNK(source_stat.st_mode)){-char*lpath=xmalloc_readlink_or_warn(source);-if(lpath){-intr=symlink(lpath,dest);-free(lpath);+structstrbuflpath=STRBUF_INIT;+if(!strbuf_readlink(&lpath,source,0)){+intr=symlink(lpath.buf,dest);+strbuf_release(&lpath);if(r<0)returnsys_error(_("can't create symlink '%s'"),dest);if(lchown(dest,source_stat.st_uid,source_stat.st_gid)<0)sys_error(_("can't preserve %s of '%s'"),"ownership",dest);+}else{+/* EINVAL => "file: Invalid argument" => puzzled user */+constchar*errmsg=_("not a symlink");+interr=errno;++if(err!=EINVAL)+errmsg=strerror(err);+error(_("%s: cannot read link: %s"),source,errmsg);+strbuf_release(&lpath);}/**_Not_jumpingtopreserve_mode_ugid_time:symlinks
@@ -111,8 +111,10 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)if(dest_exists){if(!S_ISDIR(dest_stat.st_mode))returnerror(_("target '%s' is not a directory"),dest);-/* race here: user can substitute a symlink between-*thischeckandactualcreationoffilesinsidedest*/+/*+*racehere:usercansubstituteasymlinkbetween+*thischeckandactualcreationoffilesinsidedest+*/}else{/* Create DEST */mode_tmode;
@@ -130,22 +132,24 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)if(lstat(dest,&dest_stat)<0)returnsys_error(_("can't stat '%s'"),dest);}-/* remember (dev,inode) of each created dir.-*NULL:nameisnotremembered*/+/*+*remember(dev,inode)ofeachcreateddir.nameis+*notremembered+*/add_to_ino_dev_hashtable(&dest_stat,NULL);/* Recursively copy files in SOURCE */dp=opendir(source);-if(dp==NULL){+if(!dp){retval=-1;gotopreserve_mode_ugid_time;}-while((d=readdir(dp))!=NULL){+while((d=readdir(dp))){char*new_source,*new_dest;new_source=concat_subpath_file(source,d->d_name);-if(new_source==NULL)+if(!new_source)continue;new_dest=concat_path_file(dest,d->d_name);if(copy_file(new_source,new_dest,flags&~FILEUTILS_DEREFERENCE_L0)<0)
@@ -155,16 +159,15 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)}closedir(dp);-if(!dest_exists-&&chmod(dest,source_stat.st_mode&~saved_umask)<0-){+if(!dest_exists&&+chmod(dest,source_stat.st_mode&~saved_umask)<0){sys_error(_("can't preserve permissions of '%s'"),dest);/* retval = -1; - WRONG! copy *WAS* made */}gotopreserve_mode_ugid_time;}-if(S_ISREG(source_stat.st_mode)){/* "cp [-opts] regular_file thing2" */+if(S_ISREG(source_stat.st_mode)){/* "cp [-opts] regular_file thing2" */intsrc_fd;intdst_fd;mode_tnew_mode;
@@ -199,7 +202,7 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)if(!S_ISREG(source_stat.st_mode))new_mode=0666;-// POSIX way is a security problem versus (sym)link attacks+/* POSIX way is a security problem versus (sym)link attacks */if(!ENABLE_FEATURE_NON_POSIX_CP){dst_fd=open(dest,O_WRONLY|O_CREAT|O_TRUNC,new_mode);}else{/* safe way: */
@@ -226,13 +229,15 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)retval=sys_error(_("error writing to '%s'"),dest);/* ...but read size is already checked by bb_copyfd_eof */close(src_fd);-/* "cp /dev/something new_file" should not-*copymodeof/dev/something*/+/*+*"cp /dev/something new_file"shouldnot+*copymodeof/dev/something+*/if(!S_ISREG(source_stat.st_mode))returnretval;gotopreserve_mode_ugid_time;}-dont_cat:+dont_cat:/* Source is a symlink or a special file *//* We are lazy here, a bit lax with races... */
@@ -252,20 +257,23 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)if(lchown(dest,source_stat.st_uid,source_stat.st_gid)<0)sys_error(_("can't preserve %s of '%s'"),"ownership",dest);}-/* _Not_ jumping to preserve_mode_ugid_time:-*symlinksdon'thavethose*/+/*+*_Not_jumpingtopreserve_mode_ugid_time:symlinks+*don'thavethose+*/return0;}-if(S_ISBLK(source_stat.st_mode)||S_ISCHR(source_stat.st_mode)-||S_ISSOCK(source_stat.st_mode)||S_ISFIFO(source_stat.st_mode)-){+if(S_ISBLK(source_stat.st_mode)||+S_ISCHR(source_stat.st_mode)||+S_ISSOCK(source_stat.st_mode)||+S_ISFIFO(source_stat.st_mode)){if(mknod(dest,source_stat.st_mode,source_stat.st_rdev)<0)returnsys_error(_("can't create '%s'"),dest);}elsereturnerror(_("unrecognized file '%s' with mode %x"),source,source_stat.st_mode);-preserve_mode_ugid_time:+preserve_mode_ugid_time:if(1/*FILEUTILS_PRESERVE_STATUS*/){structtimevaltimes[2];
@@ -54,10 +54,6 @@ If `<branch>` is omitted and neither `-b` nor `-B` nor `--detached` used, then, as a convenience, a new branch based at HEAD is created automatically, as if `-b $(basename <path>)` was specified.-prune::--Prune working tree information in $GIT_DIR/worktrees.- list:: List details of each worktree. The main worktree is listed first, followed by
@@ -65,6 +61,10 @@ each of the linked worktrees. The output details include if the worktree is bare, the revision currently checked out, and the branch currently checked out (or 'detached HEAD' if none).+prune::++Prune working tree information in $GIT_DIR/worktrees.+ OPTIONS -------
We can reconstruct git_dir from id quite easily. It's a bit hackier to
do the reverse.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
worktree.c | 29 ++++++++++++++++-------------
worktree.h | 7 ++++++-
2 files changed, 22 insertions(+), 14 deletions(-)
This function is later used by "worktree move" and "worktree remove"
to ensure that we have a good connection between the repository and
the worktree.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
worktree.c | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
worktree.h | 5 +++++
2 files changed, 68 insertions(+)
@@ -219,6 +219,69 @@ int is_main_worktree(const struct worktree *wt)returnwt&&!wt->id;}+staticintreport(intquiet,constchar*fmt,...)+{+va_listparams;++if(quiet)+return-1;++va_start(params,fmt);+vfprintf(stderr,fmt,params);+fputc('\n',stderr);+va_end(params);+return-1;+}++intvalidate_worktree(conststructworktree*wt,intquiet)+{+structstrbufsb=STRBUF_INIT;+constchar*path;+interr;++if(is_main_worktree(wt)){+/*+*Mainworktreeusing.gitfiletopointtothe+*repositorywouldmakeitimpossibletoknowwhere+*theactualworktreeisifthisfunctionisexecuted+*fromanotherworktree.No.gitfilesupportfornow.+*/+strbuf_addf(&sb,"%s/.git",wt->path);+if(!is_directory(sb.buf)){+strbuf_release(&sb);+returnreport(quiet,_("'%s/.git' at main worktree is not the repository directory"),+wt->path);+}+return0;+}++/*+*Makesure"gitdir"filepointstoareal.gitfileandthat+*filepointsbackhere.+*/+if(!is_absolute_path(wt->path))+returnreport(quiet,_("'%s' file does not contain absolute path to the worktree location"),+git_common_path("worktrees/%s/gitdir",wt->id));++strbuf_addf(&sb,"%s/.git",wt->path);+if(!file_exists(sb.buf)){+strbuf_release(&sb);+returnreport(quiet,_("'%s/.git' does not exist"),wt->path);+}++path=read_gitfile_gently(sb.buf,&err);+strbuf_release(&sb);+if(!path)+returnreport(quiet,_("'%s/.git' is not a .git file, error code %d"),+wt->path,err);++if(strcmp_icase(path,real_path(git_common_path("worktrees/%s",wt->id))))+returnreport(quiet,_("'%s' does not point back to"),+wt->path,git_common_path("worktrees/%s",wt->id));++return0;+}+char*find_shared_symref(constchar*symref,constchar*target){char*existing=NULL;
@@ -282,6 +282,31 @@ int validate_worktree(const struct worktree *wt, int quiet)return0;}+intupdate_worktree_location(structworktree*wt,constchar*path_)+{+structstrbufpath=STRBUF_INIT;+intret=0;++if(is_main_worktree(wt))+return0;++strbuf_add_absolute_path(&path,path_);+if(strcmp_icase(wt->path,path.buf)){+if(!write_file_gently(git_common_path("worktrees/%s/gitdir",+wt->id),+"%s/.git",real_path(path.buf))){+free(wt->path);+wt->path=strbuf_detach(&path,NULL);+ret=0;+}else+ret=sys_error(_("failed to update '%s'"),+git_common_path("worktrees/%s/gitdir",+wt->id));+}+strbuf_release(&path);+returnret;+}+char*find_shared_symref(constchar*symref,constchar*target){char*existing=NULL;
@@ -61,6 +62,12 @@ each of the linked worktrees. The output details include if the worktree is bare, the revision currently checked out, and the branch currently checked out (or 'detached HEAD' if none).+lock::++When a worktree is locked, it cannot be pruned, moved or deleted. For+example, if the worktree is on portable device that is not available+when "git worktree <command>" is executed.+ prune:: Prune working tree information in $GIT_DIR/worktrees.
@@ -104,6 +111,9 @@ OPTIONS --expire <time>:: With `prune`, only expire unused working trees older than <time>.+--reason <string>:+ An explanation why the worktree is locked.+ DETAILS ------- Each linked working tree has a private sub-directory in the repository's
@@ -220,8 +230,6 @@ performed manually, such as: - `remove` to remove a linked working tree and its administrative files (and warn if the working tree is dirty) - `mv` to move or rename a working tree and update its administrative files-- `lock` to prevent automatic pruning of administrative files (for instance,- for a working tree on a portable device) GIT ---
@@ -452,6 +453,44 @@ static int list(int ac, const char **av, const char *prefix)return0;}+staticintlock_worktree(intac,constchar**av,constchar*prefix)+{+constchar*reason="",*old_reason;+structoptionoptions[]={+OPT_STRING(0,"reason",&reason,N_("string"),+N_("reason for locking")),+OPT_END()+};+structworktree**worktrees,*wt;+structstrbufdst=STRBUF_INIT;++ac=parse_options(ac,av,prefix,options,worktree_usage,0);+if(ac!=1)+usage_with_options(worktree_usage,options);++strbuf_addstr(&dst,prefix_filename(prefix,+strlen(prefix),+av[0]));++worktrees=get_worktrees();+wt=find_worktree_by_path(worktrees,dst.buf);+if(!wt)+die(_("'%s' is not a working directory"),av[0]);+if(is_main_worktree(wt))+die(_("'%s' is a main working directory"),av[0]);++old_reason=is_worktree_locked(wt);+if(old_reason){+if(*old_reason)+die(_("already locked, reason: %s"),old_reason);+die(_("already locked, no reason"));+}++write_file(git_common_path("worktrees/%s/locked",wt->id),+"%s",reason);+return0;+}+intcmd_worktree(intac,constchar**av,constchar*prefix){structoptionoptions[]={
@@ -69,6 +70,10 @@ When a worktree is locked, it cannot be pruned, moved or deleted. For example, if the worktree is on portable device that is not available when "git worktree <command>" is executed.+move::++Move a worktree to a new location. Note that the main worktree cannot be moved.+ prune:: Prune working tree information in $GIT_DIR/worktrees.
@@ -234,7 +239,6 @@ performed manually, such as: - `remove` to remove a linked working tree and its administrative files (and warn if the working tree is dirty)-- `mv` to move or rename a working tree and update its administrative files GIT ---
@@ -520,6 +521,63 @@ static int unlock_worktree(int ac, const char **av, const char *prefix)returnunlink_or_warn(git_common_path("worktrees/%s/locked",wt->id));}+staticintmove_worktree(intac,constchar**av,constchar*prefix)+{+structoptionoptions[]={+OPT_END()+};+structworktree**worktrees,*wt;+structstrbufdst=STRBUF_INIT;+structstrbufsrc=STRBUF_INIT;+constchar*reason;++ac=parse_options(ac,av,prefix,options,worktree_usage,0);+if(ac!=2)+usage_with_options(worktree_usage,options);++strbuf_addstr(&dst,prefix_filename(prefix,+strlen(prefix),+av[1]));+if(file_exists(dst.buf))+die(_("target '%s' already exists"),av[1]);++worktrees=get_worktrees();+strbuf_addstr(&src,prefix_filename(prefix,+strlen(prefix),+av[0]));+wt=find_worktree_by_path(worktrees,src.buf);+if(!wt)+die(_("'%s' is not a working directory"),av[0]);+if(is_main_worktree(wt))+die(_("'%s' is a main working directory"),av[0]);+if((reason=is_worktree_locked(wt))){+if(*reason)+die(_("already locked, reason: %s"),reason);+die(_("already locked, no reason"));+}+if(validate_worktree(wt,0))+return-1;++/*+*Firsttry.Atomicallymove,andprobablycheaper,ifboth+*sourceandtargetareonthesamefilesystem.+*/+if(rename(src.buf,dst.buf)==-1){+if(errno!=EXDEV)+die_errno(_("failed to move '%s' to '%s'"),+src.buf,dst.buf);++/* second try.. */+if(copy_dir_recursively(src.buf,dst.buf))+die(_("failed to copy '%s' to '%s'"),+src.buf,dst.buf);+else+(void)remove_dir_recursively(&src,0);+}++returnupdate_worktree_location(wt,dst.buf);+}+intcmd_worktree(intac,constchar**av,constchar*prefix){structoptionoptions[]={
@@ -72,6 +73,10 @@ prune:: Prune working tree information in $GIT_DIR/worktrees.+unlock::++Unlock a worktree, allowing it to be pruned, moved or deleted.+ OPTIONS -------
@@ -491,6 +492,34 @@ static int lock_worktree(int ac, const char **av, const char *prefix)return0;}+staticintunlock_worktree(intac,constchar**av,constchar*prefix)+{+structoptionoptions[]={+OPT_END()+};+structworktree**worktrees,*wt;+structstrbufdst=STRBUF_INIT;++ac=parse_options(ac,av,prefix,options,worktree_usage,0);+if(ac!=1)+usage_with_options(worktree_usage,options);++strbuf_addstr(&dst,prefix_filename(prefix,+strlen(prefix),+av[0]));++worktrees=get_worktrees();+wt=find_worktree_by_path(worktrees,dst.buf);+if(!wt)+die(_("'%s' is not a working directory"),av[0]);+if(is_main_worktree(wt))+die(_("'%s' is a main working directory"),av[0]);+if(!is_worktree_locked(wt))+die(_("not locked"));++returnunlink_or_warn(git_common_path("worktrees/%s/locked",wt->id));+}+intcmd_worktree(intac,constchar**av,constchar*prefix){structoptionoptions[]={
Similar to "mv a b/", which is actually "mv a b/a", we extract basename
of source worktree and create a directory of the same name at
destination if dst path is a directory.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
builtin/worktree.c | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
@@ -558,6 +564,17 @@ static int move_worktree(int ac, const char **av, const char *prefix)if(validate_worktree(wt,0))return-1;+if(is_directory(dst.buf)){+constchar*sep=strrchr(wt->path,'/');++if(!sep)+die(_("could not figure out destination name from '%s'"),+wt->path);+strbuf_addstr(&dst,sep);+if(file_exists(dst.buf))+die(_("target '%s' already exists"),dst.buf);+}+/**Firsttry.Atomicallymove,andprobablycheaper,ifboth*sourceandtargetareonthesamefilesystem.
@@ -78,6 +79,14 @@ prune:: Prune working tree information in $GIT_DIR/worktrees.+remove::++Remove a worktree. Only clean worktrees, no untracked files and no+modification in tracked files, can be removed. Unclean worktrees can+be removed with `--force`. Main worktree cannot be removed. It needs+to be converted to a linked worktree first by moving the repository+away.+ unlock:: Unlock a worktree, allowing it to be pruned, moved or deleted.
@@ -87,9 +96,10 @@ OPTIONS -f:: --force::- By default, `add` refuses to create a new working tree when `<branch>`- is already checked out by another working tree. This option overrides- that safeguard.+ By default, `add` refuses to create a new working tree when+ `<branch>` is already checked out by another working tree and+ `remove` refuses to remove an unclean worktree. This option+ overrides that safeguard. -b <new-branch>:: -B <new-branch>::
@@ -234,12 +244,6 @@ Multiple checkout in general is still experimental, and the support for submodules is incomplete. It is NOT recommended to make multiple checkouts of a superproject.-git-worktree could provide more automation for tasks currently-performed manually, such as:--- `remove` to remove a linked working tree and its administrative files (and- warn if the working tree is dirty)- GIT --- Part of the linkgit:git[1] suite
@@ -595,6 +596,84 @@ static int move_worktree(int ac, const char **av, const char *prefix)returnupdate_worktree_location(wt,dst.buf);}+staticintremove_worktree(intac,constchar**av,constchar*prefix)+{+intforce=0;+structoptionoptions[]={+OPT_BOOL(0,"force",&force,+N_("force removing even if the worktree is dirty")),+OPT_END()+};+structworktree**worktrees,*wt;+structstrbufdst=STRBUF_INIT;+constchar*reason;+intret=0;++ac=parse_options(ac,av,prefix,options,worktree_usage,0);+if(ac!=1)+usage_with_options(worktree_usage,options);++strbuf_addstr(&dst,prefix_filename(prefix,+strlen(prefix),+av[0]));++worktrees=get_worktrees();+wt=find_worktree_by_path(worktrees,dst.buf);+if(!wt)+die(_("'%s' is not a working directory"),av[0]);+if(is_main_worktree(wt))+die(_("'%s' is a main working directory"),av[0]);+if((reason=is_worktree_locked(wt))){+if(*reason)+die(_("already locked, reason: %s"),reason);+die(_("already locked, no reason"));+}+if(validate_worktree(wt,0))+return-1;++if(!force){+structargv_arraychild_env=ARGV_ARRAY_INIT;+structchild_processcp;+charbuf[1];++argv_array_pushf(&child_env,"%s=%s/.git",+GIT_DIR_ENVIRONMENT,wt->path);+argv_array_pushf(&child_env,"%s=%s",+GIT_WORK_TREE_ENVIRONMENT,wt->path);+memset(&cp,0,sizeof(cp));+argv_array_pushl(&cp.args,"status","--porcelain",NULL);+cp.env=child_env.argv;+cp.git_cmd=1;+cp.dir=wt->path;+cp.out=-1;+ret=start_command(&cp);+if(ret)+die_errno(_("failed to run git-status on '%s', code %d"),+av[0],ret);+ret=xread(cp.out,buf,sizeof(buf));+if(ret)+die(_("'%s' is dirty, use --force to delete it"),av[0]);+close(cp.out);+ret=finish_command(&cp);+if(ret)+die_errno(_("failed to run git-status on '%s', code %d"),+av[0],ret);+}+if(remove_dir_recursively(&dst,0)){+sys_error(_("failed to delete '%s'"),wt->path);+ret=-1;+}+strbuf_reset(&dst);+strbuf_addstr(&dst,git_common_path("worktrees/%s",wt->id));+if(remove_dir_recursively(&dst,0)){+sys_error(_("failed to delete '%s'"),dst.buf);+ret=-1;+}+strbuf_release(&dst);+free_worktrees(worktrees);+returnret;+}+intcmd_worktree(intac,constchar**av,constchar*prefix){structoptionoptions[]={
From: Eric Sunshine <hidden> Date: 2016-06-16 02:19:18
On Wed, Apr 13, 2016 at 9:15 AM, Nguyễn Thái Ngọc Duy [off-list ref] wrote:
quoted hunk
Similar to "mv a b/", which is actually "mv a b/a", we extract basename
of source worktree and create a directory of the same name at
destination if dst path is a directory.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
@@ -538,7 +538,13 @@ static int move_worktree(int ac, const char **av, const char *prefix)- if (file_exists(dst.buf))+ if (is_directory(dst.buf))+ /*+ * keep going, dst will be appended after we get the+ * source's absolute path+ */+ ;+ else if (file_exists(dst.buf)) die(_("target '%s' already exists"), av[1]);
Does this need to take Windows into account? Perhaps git_find_last_dir_sep()?
+
+ if (!sep)
+ die(_("could not figure out destination name from '%s'"),
+ wt->path);
+ strbuf_addstr(&dst, sep);
+ if (file_exists(dst.buf))
+ die(_("target '%s' already exists"), dst.buf);
+ }
On Wed, May 11, 2016 at 11:43 AM, Eric Sunshine [off-list ref] wrote:
On Wed, Apr 13, 2016 at 9:15 AM, Nguyễn Thái Ngọc Duy [off-list ref] wrote:
quoted
Similar to "mv a b/", which is actually "mv a b/a", we extract basename
of source worktree and create a directory of the same name at
destination if dst path is a directory.
Signed-off-by: Nguyễn Thái Ngọc Duy <redacted>
---
@@ -538,7 +538,13 @@ static int move_worktree(int ac, const char **av, const char *prefix)- if (file_exists(dst.buf))+ if (is_directory(dst.buf))+ /*+ * keep going, dst will be appended after we get the+ * source's absolute path+ */+ ;+ else if (file_exists(dst.buf)) die(_("target '%s' already exists"), av[1]);
wt->path comes from $GIT_DIR/worktrees/xxx/gitdir, which normally uses
forward slashes, so we should be safe. We already rely on forward
slashes in get_linked_worktree()
Perhaps git_find_last_dir_sep()?
But this is probably a good thing to do anyway, to be more robust in
future. But it could confuse the reader later on why it's necessary
when backward slashes can't exist in wt->path. I don't know. Maybe
just have a comment that backward slashes can't never appear here?
There is also a potential problem with find_worktree_by_path(). I was
counting on real_path() to normalize paths and could simply do
strcmp_icase (or its new name, fspathcmp). But real_path() does not
seem to convert unify slashes. I will need to have a closer look at
this. Hopefully prefix_filename() already makes sure everything uses
forward slashes. Or maybe we could improve fspathcmp to see '/' and
'\' the same thing on Windows.
--
Duy
wt->path comes from $GIT_DIR/worktrees/xxx/gitdir, which normally uses
forward slashes, so we should be safe. We already rely on forward
slashes in get_linked_worktree()
quoted
Perhaps git_find_last_dir_sep()?
But this is probably a good thing to do anyway, to be more robust in
future. But it could confuse the reader later on why it's necessary
when backward slashes can't exist in wt->path. I don't know. Maybe
just have a comment that backward slashes can't never appear here?
As this path is read from a file git itself creates, and if we know
that it will always contain forward slashes, then I agree that it
could be potentially confusing to later readers to see
git_find_last_dir_sep(). So, keeping it as-is seems correct.
Not sure if it needs a comment. I reviewed this rather quickly since
(I think) you plan on re-rolling it and I'm far behind on my reviews.
Consequently, I didn't check the existing code, and reviewed only
within the context of the patch itself. If the end result is that it's
clear from reading the code that it will always contain forward
slashes, then a comment would be redundant. You could perhaps mention
in the commit message that the slash will always be forward, which
should satisfy future reviewers and readers of the code once its in
the tree.
There is also a potential problem with find_worktree_by_path(). I was
counting on real_path() to normalize paths and could simply do
strcmp_icase (or its new name, fspathcmp). But real_path() does not
seem to convert unify slashes. I will need to have a closer look at
this. Hopefully prefix_filename() already makes sure everything uses
forward slashes. Or maybe we could improve fspathcmp to see '/' and
'\' the same thing on Windows.
If we look at fspathcmp() as a function which performs whatever magic
is needed to make comparisons work on a platform/filesystem, then it
might indeed be reasonable to enhance it to recognize '/' and '\' as
equivalent (with possible caveats for Windows corner cases).
wt->path comes from $GIT_DIR/worktrees/xxx/gitdir, which normally uses
forward slashes, so we should be safe. We already rely on forward
slashes in get_linked_worktree()
quoted
Perhaps git_find_last_dir_sep()?
But this is probably a good thing to do anyway, to be more robust in
future. But it could confuse the reader later on why it's necessary
when backward slashes can't exist in wt->path. I don't know. Maybe
just have a comment that backward slashes can't never appear here?
As this path is read from a file git itself creates, and if we know
that it will always contain forward slashes, then I agree that it
could be potentially confusing to later readers to see
git_find_last_dir_sep(). So, keeping it as-is seems correct.
Please allow me to disagree. There should not be any assumption that a
path uses forward slashes as directory separator, except when the path is
- a pathspec
- a ref
- a path found in the object database including the index
In particular, everything concerning paths in the file system (including
paths pointing to Git's own files) must not assume forward slashes.
We do convert backslashes to forward slashes in a number of places, but
this is only for cosmetic reasons, not to maintain an invariant.
If we look at fspathcmp() as a function which performs whatever magic
is needed to make comparisons work on a platform/filesystem, then it
might indeed be reasonable to enhance it to recognize '/' and '\' as
equivalent (with possible caveats for Windows corner cases).