From: Tracey Emery Subject: Re: install symbolic links in the work tree To: gameoftrees@openbsd.org Date: Tue, 21 Jul 2020 09:41:42 -0600 On Sun, Jul 19, 2020 at 06:54:41PM +0200, Stefan Sperling wrote: > > Full diff follows. > I think this looks fine. I didn't see anything unusual. There is no breakage in gotweb. As far as I'm concerned, this can be improved in the tree, if need bee. I have just a couple of grammar tweaks and errant tab noted below. > diff refs/heads/master refs/heads/symlink > blob - 2622e5a3f57c01d5c7c55e3ee70f1b92e04329bf > blob + 1bd20230e857c83cb4e2cbd62cec36b1a83b09ab > --- got/got.1 > +++ got/got.1 > @@ -543,6 +543,7 @@ Show the status of each affected file, using the follo > .It \(a~ Ta versioned file is obstructed by a non-regular file > .It ! Ta a missing versioned file was restored > .It # Ta file was not updated because it contains merge conflicts > +.It ? Ta changes destined for an unversioned file were not merged > .El > .Pp > If no > @@ -1168,7 +1169,7 @@ is a directory. > .It Cm rv > Short alias for > .Cm revert . > -.It Cm commit Oo Fl m Ar message Oc Op Ar path ... > +.It Cm commit Oo Fl m Ar message Oc Oo Fl S Oc Op Ar path ... > Create a new commit in the repository from changes in a work tree > and use this commit as the new base commit for the work tree. > If no > @@ -1226,6 +1227,15 @@ Without the > option, > .Cm got commit > opens a temporary file in an editor where a log message can be written. > +.It Fl S > +Allow symbolic links which point somewhere outside of the path space > +managed by > +.Nm . #commas Allow symbolic links, which point somewhere outside of the path space, > +As a precaution, > +when such a symbolic link gets installed in a work tree #commas when such a symbolic link gets install in a work tree, > +.Nm > +may decide to represent the symbolic link as a regular file which contains > +the link's target path, rather than creating an actual symbolic link. > .El > .Pp > .Cm got commit > @@ -1262,6 +1272,7 @@ Show the status of each affected file, using the follo > .It d Ta file's deletion was obstructed by local modifications > .It A Ta new file was added > .It \(a~ Ta changes destined for a non-regular file were not merged > +.It ? Ta changes destined for an unversioned file were not merged > .El > .Pp > The merged changes will appear as local changes in the work tree, which > @@ -1305,6 +1316,7 @@ Show the status of each affected file, using the follo > .It d Ta file's deletion was obstructed by local modifications > .It A Ta new file was added > .It \(a~ Ta changes destined for a non-regular file were not merged > +.It ? Ta changes destined for an unversioned file were not merged > .El > .Pp > The reverse-merged changes will appear as local changes in the work tree, > @@ -1380,6 +1392,7 @@ using the following status codes: > .It d Ta file's deletion was obstructed by local modifications > .It A Ta new file was added > .It \(a~ Ta changes destined for a non-regular file were not merged > +.It ? Ta changes destined for an unversioned file were not merged > .El > .Pp > If merge conflicts occur the rebase operation is interrupted and may > @@ -1509,6 +1522,7 @@ using the following status codes: > .It d Ta file's deletion was obstructed by local modifications > .It A Ta new file was added > .It \(a~ Ta changes destined for a non-regular file were not merged > +.It ? Ta changes destined for an unversioned file were not merged > .El > .Pp > If merge conflicts occur the histedit operation is interrupted and may > @@ -1632,7 +1646,7 @@ or reverted with > .It Cm ig > Short alias for > .Cm integrate . > -.It Cm stage Oo Fl l Oc Oo Fl p Oc Oo Fl F Ar response-script Oc Op Ar path ... > +.It Cm stage Oo Fl l Oc Oo Fl p Oc Oo Fl F Ar response-script Oc Oo Fl S Oc Op Ar path ... > Stage local changes for inclusion in the next commit. > If no > .Ar path > @@ -1708,6 +1722,15 @@ and > responses line-by-line from the specified > .Ar response-script > file instead of prompting interactively. > +.It Fl S > +Allow symbolic links which point somewhere outside of the path space > +managed by > +.Nm . > +As a precaution, > +when such a symbolic link gets installed in a work tree > +.Nm > +may decide to represent the symbolic link as a regular file which contains > +the link's target path, rather than creating an actual symbolic link. Same commas needed above > .El > .Pp > .Cm got stage > blob - f14e875454e17d7c6012447918fec9a697f40c10 > blob + fb2d55929f89f48f959e0ddfdc2fa639fe2c64d0 > --- got/got.c > +++ got/got.c > @@ -3632,7 +3632,53 @@ struct print_diff_arg { > int ignore_whitespace; > }; > > +/* > + * Create a file which contains the target path of a symlink so we can feed > + * it as content to the diff engine. > + */ > static const struct got_error * > +get_symlink_target_file(int *fd, int dirfd, const char *de_name, > + const char *abspath) > +{ > + const struct got_error *err = NULL; > + char target_path[PATH_MAX]; > + ssize_t target_len, outlen; > + > + *fd = -1; > + > + if (dirfd != -1) { > + target_len = readlinkat(dirfd, de_name, target_path, PATH_MAX); > + if (target_len == -1) > + return got_error_from_errno2("readlinkat", abspath); > + } else { > + target_len = readlink(abspath, target_path, PATH_MAX); > + if (target_len == -1) > + return got_error_from_errno2("readlink", abspath); > + } > + > + *fd = got_opentempfd(); > + if (*fd == -1) > + return got_error_from_errno("got_opentempfd"); > + > + outlen = write(*fd, target_path, target_len); > + if (outlen == -1) { > + err = got_error_from_errno("got_opentempfd"); > + goto done; > + } > + > + if (lseek(*fd, 0, SEEK_SET) == -1) { > + err = got_error_from_errno2("lseek", abspath); > + goto done; > + } > +done: > + if (err) { > + close(*fd); > + *fd = -1; > + } > + return err; > +} > + > +static const struct got_error * > print_diff(void *arg, unsigned char status, unsigned char staged_status, > const char *path, struct got_object_id *blob_id, > struct got_object_id *staged_blob_id, struct got_object_id *commit_id, > @@ -3723,14 +3769,28 @@ print_diff(void *arg, unsigned char status, unsigned c > if (dirfd != -1) { > fd = openat(dirfd, de_name, O_RDONLY | O_NOFOLLOW); > if (fd == -1) { > - err = got_error_from_errno2("openat", abspath); > - goto done; > + if (errno != ELOOP) { > + err = got_error_from_errno2("openat", > + abspath); > + goto done; > + } > + err = get_symlink_target_file(&fd, dirfd, > + de_name, abspath); > + if (err) > + goto done; > } > } else { > fd = open(abspath, O_RDONLY | O_NOFOLLOW); > if (fd == -1) { > - err = got_error_from_errno2("open", abspath); > - goto done; > + if (errno != ELOOP) { > + err = got_error_from_errno2("open", > + abspath); > + goto done; > + } > + err = get_symlink_target_file(&fd, dirfd, > + de_name, abspath); > + if (err) > + goto done; > } > } > if (fstat(fd, &sb) == -1) { > @@ -4100,6 +4160,7 @@ cmd_blame(int argc, char *argv[]) > struct got_repository *repo = NULL; > struct got_worktree *worktree = NULL; > char *path, *cwd = NULL, *repo_path = NULL, *in_repo_path = NULL; > + char *link_target = NULL; > struct got_object_id *obj_id = NULL; > struct got_object_id *commit_id = NULL; > struct got_blob_object *blob = NULL; > @@ -4214,16 +4275,23 @@ cmd_blame(int argc, char *argv[]) > goto done; > } > > - error = got_object_id_by_path(&obj_id, repo, commit_id, in_repo_path); > + error = got_object_resolve_symlinks(&link_target, in_repo_path, > + commit_id, repo); > if (error) > goto done; > > + error = got_object_id_by_path(&obj_id, repo, commit_id, > + link_target ? link_target : in_repo_path); > + if (error) > + goto done; > + > error = got_object_get_type(&obj_type, repo, obj_id); > if (error) > goto done; > > if (obj_type != GOT_OBJ_TYPE_BLOB) { > - error = got_error(GOT_ERR_OBJ_TYPE); > + error = got_error_path(link_target ? link_target : in_repo_path, > + GOT_ERR_OBJ_TYPE); > goto done; > } > > @@ -4258,10 +4326,11 @@ cmd_blame(int argc, char *argv[]) > } > bca.repo = repo; > > - error = got_blame(in_repo_path, commit_id, repo, blame_cb, &bca, > - check_cancelled, NULL); > + error = got_blame(link_target ? link_target : in_repo_path, commit_id, > + repo, blame_cb, &bca, check_cancelled, NULL); > done: > free(in_repo_path); > + free(link_target); > free(repo_path); > free(cwd); > free(commit_id); > @@ -6297,7 +6366,7 @@ done: > __dead static void > usage_commit(void) > { > - fprintf(stderr, "usage: %s commit [-m msg] [path ...]\n", > + fprintf(stderr, "usage: %s commit [-m msg] [-S] [path ...]\n", > getprogname()); > exit(1); > } > @@ -6384,16 +6453,20 @@ cmd_commit(int argc, char *argv[]) > struct collect_commit_logmsg_arg cl_arg; > char *gitconfig_path = NULL, *editor = NULL, *author = NULL; > int ch, rebase_in_progress, histedit_in_progress, preserve_logmsg = 0; > + int allow_bad_symlinks = 0; > struct got_pathlist_head paths; > > TAILQ_INIT(&paths); > cl_arg.logmsg_path = NULL; > > - while ((ch = getopt(argc, argv, "m:")) != -1) { > + while ((ch = getopt(argc, argv, "m:S")) != -1) { > switch (ch) { > case 'm': > logmsg = optarg; > break; > + case 'S': > + allow_bad_symlinks = 1; > + break; > default: > usage_commit(); > /* NOTREACHED */ > @@ -6474,7 +6547,8 @@ cmd_commit(int argc, char *argv[]) > } > cl_arg.repo_path = got_repo_get_path(repo); > error = got_worktree_commit(&id, worktree, &paths, author, NULL, > - collect_commit_logmsg, &cl_arg, print_status, NULL, repo); > + allow_bad_symlinks, collect_commit_logmsg, &cl_arg, > + print_status, NULL, repo); > if (error) { > if (error->code != GOT_ERR_COMMIT_MSG_EMPTY && > cl_arg.logmsg_path != NULL) > @@ -8691,7 +8765,7 @@ __dead static void > usage_stage(void) > { > fprintf(stderr, "usage: %s stage [-l] | [-p] [-F response-script] " > - "[file-path ...]\n", > + "[-S] [file-path ...]\n", > getprogname()); > exit(1); > } > @@ -8732,14 +8806,14 @@ cmd_stage(int argc, char *argv[]) > char *cwd = NULL; > struct got_pathlist_head paths; > struct got_pathlist_entry *pe; > - int ch, list_stage = 0, pflag = 0; > + int ch, list_stage = 0, pflag = 0, allow_bad_symlinks = 0; > FILE *patch_script_file = NULL; > const char *patch_script_path = NULL; > struct choose_patch_arg cpa; > > TAILQ_INIT(&paths); > > - while ((ch = getopt(argc, argv, "lpF:")) != -1) { > + while ((ch = getopt(argc, argv, "lpF:S")) != -1) { > switch (ch) { > case 'l': > list_stage = 1; > @@ -8750,6 +8824,9 @@ cmd_stage(int argc, char *argv[]) > case 'F': > patch_script_path = optarg; > break; > + case 'S': > + allow_bad_symlinks = 1; > + break; > default: > usage_stage(); > /* NOTREACHED */ > @@ -8812,7 +8889,8 @@ cmd_stage(int argc, char *argv[]) > cpa.action = "stage"; > error = got_worktree_stage(worktree, &paths, > pflag ? NULL : print_status, NULL, > - pflag ? choose_patch : NULL, &cpa, repo); > + pflag ? choose_patch : NULL, &cpa, > + allow_bad_symlinks, repo); > } > done: > if (patch_script_file && fclose(patch_script_file) == EOF && > blob - 1921d0479484792497498d1b7aedaad701aadc51 > blob + b3805d39c06efa63932243754b6f446fa0218eda > --- include/got_error.h > +++ include/got_error.h > @@ -143,6 +143,7 @@ > #define GOT_ERR_TREE_ENTRY_TYPE 126 > #define GOT_ERR_PARSE_Y_YY 127 > #define GOT_ERR_NO_CONFIG_FILE 128 > +#define GOT_ERR_BAD_SYMLINK 129 > > static const struct got_error { > int code; > @@ -292,6 +293,8 @@ static const struct got_error { > { GOT_ERR_TREE_ENTRY_TYPE, "unexpected tree entry type" }, > { GOT_ERR_PARSE_Y_YY, "yyerror error" }, > { GOT_ERR_NO_CONFIG_FILE, "configuration file doesn't exit" }, > + { GOT_ERR_BAD_SYMLINK, "symbolic link points outside of paths under " > + "version control" }, > }; > > /* > blob - 54c7093a4d0bc68cfd3ef1939b7c80f71106e777 > blob + 3adf5f3dbc4de118965bdc9066822242aff95942 > --- include/got_object.h > +++ include/got_object.h > @@ -212,7 +212,20 @@ struct got_tree_entry *got_tree_entry_get_prev(struct > /* Return non-zero if the specified tree entry is a Git submodule. */ > int got_object_tree_entry_is_submodule(struct got_tree_entry *); > > +/* Return non-zero if the specified tree entry is a symbolic link. */ > +int got_object_tree_entry_is_symlink(struct got_tree_entry *); > + > /* > + * Resolve an in-repository symlink at the specified path in the tree > + * corresponding to the specified commit. If the specified path is not > + * a symlink then set *link_target to NULL. > + * Otherwise, resolve symlinks recursively and return the final link > + * target path. The caller must dispose of it with free(3). > + */ > +const struct got_error *got_object_resolve_symlinks(char **, const char *, > + struct got_object_id *, struct got_repository *); > + > +/* > * Compare two trees and indicate whether the entry at the specified path > * differs between them. The path must not be the root path "/"; the function > * got_object_id_cmp() should be used instead to compare the tree roots. > @@ -254,6 +267,9 @@ const uint8_t *got_object_blob_get_read_buf(struct got > const struct got_error *got_object_blob_read_block(size_t *, > struct got_blob_object *); > > +/* Rewind an open blob's data stream back to the beginning. */ > +void got_object_blob_rewind(struct got_blob_object *); > + > /* > * Read the entire content of a blob and write it to the specified file. > * Flush and rewind the file as well. Indicate the amount of bytes > @@ -263,6 +279,16 @@ const struct got_error *got_object_blob_read_block(siz > */ > const struct got_error *got_object_blob_dump_to_file(size_t *, int *, > off_t **, FILE *, struct got_blob_object *); > + > +/* > + * Read the entire content of a blob into a newly allocated string buffer > + * and terminate it with '\0'. This is intended for blobs which contain a > + * symlink target path. It should not be used to process arbitrary blobs. > + * Use got_object_blob_dump_to_file() or got_tree_entry_get_symlink_target() > + * instead if possible. The caller must dispose of the string with free(3). > + */ > +const struct got_error *got_object_blob_read_to_str(char **, > + struct got_blob_object *); > > /* > * Attempt to open a tag object in a repository. > blob - 0dd838e3937672354147a6c4cddeaadaa34de9c5 > blob + 277c66b0096dcee60d7ae5305877516328376f34 > --- include/got_worktree.h > +++ include/got_worktree.h > @@ -223,10 +223,13 @@ typedef const struct got_error *(*got_worktree_commit_ > * current base commit. > * An author and a non-empty log message must be specified. > * The name of the committer is optional (may be NULL). > + * If a path to be committed contains a symlink which points outside > + * of the path space under version control, raise an error unless > + * committing of such paths is being forced by the caller. > */ > const struct got_error *got_worktree_commit(struct got_object_id **, > struct got_worktree *, struct got_pathlist_head *, const char *, > - const char *, got_worktree_commit_msg_cb, void *, > + const char *, int, got_worktree_commit_msg_cb, void *, > got_worktree_status_cb, void *, struct got_repository *); > > /* Get the path of a commitable worktree item. */ > @@ -424,10 +427,13 @@ const struct got_error *got_worktree_integrate_abort(s > * Stage the specified paths for commit. > * If the patch callback is not NULL, call it to select patch hunks for > * staging. Otherwise, stage the full file content found at each path. > -*/ > + * If a path being staged contains a symlink which points outside > + * of the path space under version control, raise an error unless > + * staging of such paths is being forced by the caller. > + */ > const struct got_error *got_worktree_stage(struct got_worktree *, > struct got_pathlist_head *, got_worktree_status_cb, void *, > - got_worktree_patch_cb, void *, struct got_repository *); > + got_worktree_patch_cb, void *, int, struct got_repository *); > > /* > * Merge staged changes for the specified paths back into the work tree > blob - 152352cad8daa30cc23016c559df4f22a087bd74 > blob + 7e5ee06994a5158bc937ce8307bd19f51d7e0ef5 > --- lib/diff.c > +++ lib/diff.c > @@ -114,16 +114,25 @@ diff_blobs(struct got_blob_object *blob1, struct got_b > > if (outfile) { > char *modestr1 = NULL, *modestr2 = NULL; > + int modebits; > if (mode1 && mode1 != mode2) { > + if (S_ISLNK(mode1)) > + modebits = S_IFLNK; > + else > + modebits = (S_IRWXU | S_IRWXG | S_IRWXO); > if (asprintf(&modestr1, " (mode %o)", > - mode1 & (S_IRWXU | S_IRWXG | S_IRWXO)) == -1) { > + mode1 & modebits) == -1) { > err = got_error_from_errno("asprintf"); > goto done; > } > } > if (mode2 && mode1 != mode2) { > + if (S_ISLNK(mode2)) > + modebits = S_IFLNK; > + else > + modebits = (S_IRWXU | S_IRWXG | S_IRWXO); > if (asprintf(&modestr2, " (mode %o)", > - mode2 & (S_IRWXU | S_IRWXG | S_IRWXO)) == -1) { > + mode2 & modebits) == -1) { > err = got_error_from_errno("asprintf"); > goto done; > } > @@ -550,9 +559,11 @@ diff_entry_old_new(struct got_tree_entry *te1, > if (!id_match) > return diff_modified_tree(&te1->id, &te2->id, > label1, label2, repo, cb, cb_arg, diff_content); > - } else if (S_ISREG(te1->mode) && S_ISREG(te2->mode)) { > + } else if ((S_ISREG(te1->mode) || S_ISLNK(te1->mode)) && > + (S_ISREG(te2->mode) || S_ISLNK(te2->mode))) { > if (!id_match || > - (te1->mode & S_IXUSR) != (te2->mode & S_IXUSR)) { > + ((te1->mode & (S_IFLNK | S_IXUSR))) != > + (te2->mode & (S_IFLNK | S_IXUSR))) { > if (diff_content) > return diff_modified_blob(&te1->id, &te2->id, > label1, label2, te1->mode, te2->mode, > blob - 656685591bdfde81e5346507229c0bd87d2fa18c > blob + 003fe8d9e671d896c1f1db5f6853ad737a7a39b4 > --- lib/fileindex.c > +++ lib/fileindex.c > @@ -53,18 +53,35 @@ struct got_fileindex { > #define GOT_FILEIDX_MAX_ENTRIES INT_MAX > }; > > -uint16_t > -got_fileindex_perms_from_st(struct stat *sb) > +mode_t > +got_fileindex_entry_perms_get(struct got_fileindex_entry *ie) > { > - uint16_t perms = (sb->st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)); > - return (perms << GOT_FILEIDX_MODE_PERMS_SHIFT); > + return ((ie->mode & GOT_FILEIDX_MODE_PERMS) >> > + GOT_FILEIDX_MODE_PERMS_SHIFT); > } > > +static void > +fileindex_entry_perms_set(struct got_fileindex_entry *ie, mode_t mode) > +{ > + ie->mode &= ~GOT_FILEIDX_MODE_PERMS; > + ie->mode |= ((mode << GOT_FILEIDX_MODE_PERMS_SHIFT) & > + GOT_FILEIDX_MODE_PERMS); > +} > + > mode_t > got_fileindex_perms_to_st(struct got_fileindex_entry *ie) > { > - mode_t perms = (ie->mode >> GOT_FILEIDX_MODE_PERMS_SHIFT); > - return (S_IFREG | (perms & (S_IRWXU | S_IRWXG | S_IRWXO))); > + mode_t perms = got_fileindex_entry_perms_get(ie); > + int type = got_fileindex_entry_filetype_get(ie); > + uint32_t ftype; > + > + if (type == GOT_FILEIDX_MODE_REGULAR_FILE || > + type == GOT_FILEIDX_MODE_BAD_SYMLINK) > + ftype = S_IFREG; > + else > + ftype = S_IFLNK; > + > + return (ftype | (perms & (S_IRWXU | S_IRWXG | S_IRWXO))); > } > > const struct got_error * > @@ -95,11 +112,16 @@ got_fileindex_entry_update(struct got_fileindex_entry > ie->uid = sb.st_uid; > ie->gid = sb.st_gid; > ie->size = (sb.st_size & 0xffffffff); > - if (S_ISLNK(sb.st_mode)) > - ie->mode = GOT_FILEIDX_MODE_SYMLINK; > - else > - ie->mode = GOT_FILEIDX_MODE_REGULAR_FILE; > - ie->mode |= got_fileindex_perms_from_st(&sb); > + if (S_ISLNK(sb.st_mode)) { > + got_fileindex_entry_filetype_set(ie, > + GOT_FILEIDX_MODE_SYMLINK); > + fileindex_entry_perms_set(ie, 0); > + } else { > + got_fileindex_entry_filetype_set(ie, > + GOT_FILEIDX_MODE_REGULAR_FILE); > + fileindex_entry_perms_set(ie, > + sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)); > + } > } > > if (blob_sha1) { > @@ -174,6 +196,34 @@ got_fileindex_entry_stage_set(struct got_fileindex_ent > ie->flags &= ~GOT_FILEIDX_F_STAGE; > ie->flags |= ((stage << GOT_FILEIDX_F_STAGE_SHIFT) & > GOT_FILEIDX_F_STAGE); > +} > + > +int > +got_fileindex_entry_filetype_get(struct got_fileindex_entry *ie) > +{ > + return (ie->mode & GOT_FILEIDX_MODE_FILE_TYPE_ONDISK); > +} > + > +void > +got_fileindex_entry_filetype_set(struct got_fileindex_entry *ie, int type) > +{ > + ie->mode &= ~GOT_FILEIDX_MODE_FILE_TYPE_ONDISK; > + ie->mode |= (type & GOT_FILEIDX_MODE_FILE_TYPE_ONDISK); > +} > + > +void > +got_fileindex_entry_staged_filetype_set(struct got_fileindex_entry *ie, int type) > +{ > + ie->mode &= ~GOT_FILEIDX_MODE_FILE_TYPE_STAGED; > + ie->mode |= ((type << GOT_FILEIDX_MODE_FILE_TYPE_STAGED_SHIFT) & > + GOT_FILEIDX_MODE_FILE_TYPE_STAGED); > +} > + > +int > +got_fileindex_entry_staged_filetype_get(struct got_fileindex_entry *ie) > +{ > + return (ie->mode & GOT_FILEIDX_MODE_FILE_TYPE_STAGED) >> > + GOT_FILEIDX_MODE_FILE_TYPE_STAGED_SHIFT; > } > > int > blob - af6efe8eca81db02b8067b47cf97720194c03e52 > blob + f10058320f24affa0531641620b65ac3641a9b6f > --- lib/got_lib_fileindex.h > +++ lib/got_lib_fileindex.h > @@ -37,8 +37,12 @@ struct got_fileindex_entry { > > uint16_t mode; > #define GOT_FILEIDX_MODE_FILE_TYPE 0x000f > +#define GOT_FILEIDX_MODE_FILE_TYPE_ONDISK 0x0003 > +#define GOT_FILEIDX_MODE_FILE_TYPE_STAGED 0x000c > +#define GOT_FILEIDX_MODE_FILE_TYPE_STAGED_SHIFT 2 > #define GOT_FILEIDX_MODE_REGULAR_FILE 1 > #define GOT_FILEIDX_MODE_SYMLINK 2 > +#define GOT_FILEIDX_MODE_BAD_SYMLINK 3 > #define GOT_FILEIDX_MODE_PERMS 0xfff0 > #define GOT_FILEIDX_MODE_PERMS_SHIFT 4 > > @@ -99,6 +103,7 @@ struct got_fileindex_hdr { > uint8_t sha1[SHA1_DIGEST_LENGTH]; /* checksum of above on-disk data */ > }; > > +mode_t got_fileindex_entry_perms_get(struct got_fileindex_entry *); > uint16_t got_fileindex_perms_from_st(struct stat *); > mode_t got_fileindex_perms_to_st(struct got_fileindex_entry *); > > @@ -161,5 +166,9 @@ int got_fileindex_entry_has_commit(struct got_fileinde > int got_fileindex_entry_has_file_on_disk(struct got_fileindex_entry *); > uint32_t got_fileindex_entry_stage_get(const struct got_fileindex_entry *); > void got_fileindex_entry_stage_set(struct got_fileindex_entry *ie, uint32_t); > +int got_fileindex_entry_filetype_get(struct got_fileindex_entry *); > +void got_fileindex_entry_filetype_set(struct got_fileindex_entry *, int); > +void got_fileindex_entry_staged_filetype_set(struct got_fileindex_entry *, int); > +int got_fileindex_entry_staged_filetype_get(struct got_fileindex_entry *); > > void got_fileindex_entry_mark_deleted_from_disk(struct got_fileindex_entry *); > blob - 9a3beff5204483df08335c4ff3dfe9f49015cf50 > blob + b840da129287ae16d17c02686ba0926e3de5196e > --- lib/object.c > +++ lib/object.c > @@ -32,6 +32,7 @@ > #include > #include > #include > +#include > #include > #include > #include > @@ -863,38 +864,69 @@ got_tree_entry_get_id(struct got_tree_entry *te) > } > > const struct got_error * > +got_object_blob_read_to_str(char **s, struct got_blob_object *blob) > +{ > + const struct got_error *err = NULL; > + size_t len, totlen, hdrlen, offset; > + > + *s = NULL; > + > + hdrlen = got_object_blob_get_hdrlen(blob); > + totlen = 0; > + offset = 0; > + do { > + char *p; > + > + err = got_object_blob_read_block(&len, blob); > + if (err) > + return err; > + > + if (len == 0) > + break; > + > + totlen += len - hdrlen; > + p = realloc(*s, totlen + 1); > + if (p == NULL) { > + err = got_error_from_errno("realloc"); > + free(*s); > + *s = NULL; > + return err; > + } > + *s = p; > + /* Skip blob object header first time around. */ > + memcpy(*s + offset, > + got_object_blob_get_read_buf(blob) + hdrlen, len - hdrlen); > + hdrlen = 0; > + offset = totlen; > + } while (len > 0); > + > + (*s)[totlen] = '\0'; > + return NULL; > +} > + > +const struct got_error * > got_tree_entry_get_symlink_target(char **link_target, struct got_tree_entry *te, > struct got_repository *repo) > { > const struct got_error *err = NULL; > struct got_blob_object *blob = NULL; > - size_t len; > > *link_target = NULL; > > - /* S_IFDIR check avoids confusing symlinks with submodules. */ > - if ((te->mode & (S_IFDIR | S_IFLNK)) != S_IFLNK) > + if (!got_object_tree_entry_is_symlink(te)) > return got_error(GOT_ERR_TREE_ENTRY_TYPE); > > err = got_object_open_as_blob(&blob, repo, > got_tree_entry_get_id(te), PATH_MAX); > if (err) > return err; > - errant tab > - err = got_object_blob_read_block(&len, blob); > - if (err) > - goto done; > > - *link_target = malloc(len + 1); > - if (*link_target == NULL) { > - err = got_error_from_errno("malloc"); > - goto done; > + err = got_object_blob_read_to_str(link_target, blob); > + got_object_blob_close(blob); > + if (err) { > + free(*link_target); > + *link_target = NULL; > } > - memcpy(*link_target, got_object_blob_get_read_buf(blob), len); > - (*link_target)[len] = '\0'; > -done: > - if (blob) > - got_object_blob_close(blob); > return err; > } > > @@ -1204,6 +1236,13 @@ got_object_blob_close(struct got_blob_object *blob) > return err; > } > > +void > +got_object_blob_rewind(struct got_blob_object *blob) > +{ > + if (blob->f) > + rewind(blob->f); > +} > + > char * > got_object_blob_id_str(struct got_blob_object *blob, char *buf, size_t size) > { > @@ -1675,6 +1714,14 @@ normalize_mode_for_comparison(mode_t mode) > if (S_ISDIR(mode)) > return mode & S_IFDIR; > > + /* > + * For symlinks, the only relevant bit is the IFLNK bit. > + * This allows us to detect paths changing from a symlinks > + * to a file or directory and vice versa. > + */ > + if (S_ISLNK(mode)) > + return mode & S_IFLNK; > + > /* For files, the only change we care about is the executable bit. */ > return mode & S_IXUSR; > } > @@ -1794,6 +1841,116 @@ int > got_object_tree_entry_is_submodule(struct got_tree_entry *te) > { > return (te->mode & S_IFMT) == (S_IFDIR | S_IFLNK); > +} > + > +int > +got_object_tree_entry_is_symlink(struct got_tree_entry *te) > +{ > + /* S_IFDIR check avoids confusing symlinks with submodules. */ > + return ((te->mode & (S_IFDIR | S_IFLNK)) == S_IFLNK); > +} > + > +static const struct got_error * > +resolve_symlink(char **link_target, const char *path, > + struct got_object_id *commit_id, struct got_repository *repo) > +{ > + const struct got_error *err = NULL; > + char *name, *parent_path = NULL; > + struct got_object_id *tree_obj_id = NULL; > + struct got_tree_object *tree = NULL; > + struct got_tree_entry *te = NULL; > + > + *link_target = NULL; > + tab > + name = basename(path); > + if (name == NULL) > + return got_error_from_errno2("basename", path); > + > + err = got_path_dirname(&parent_path, path); > + if (err) > + return err; > + > + err = got_object_id_by_path(&tree_obj_id, repo, commit_id, > + parent_path); > + if (err) { > + if (err->code == GOT_ERR_NO_TREE_ENTRY) { > + /* Display the complete path in error message. */ > + err = got_error_path(path, err->code); > + } > + goto done; > + } > + > + err = got_object_open_as_tree(&tree, repo, tree_obj_id); > + if (err) > + goto done; > + > + te = got_object_tree_find_entry(tree, name); > + if (te == NULL) { > + err = got_error_path(path, GOT_ERR_NO_TREE_ENTRY); > + goto done; > + } > + > + if (got_object_tree_entry_is_symlink(te)) { > + err = got_tree_entry_get_symlink_target(link_target, te, repo); > + if (err) > + goto done; > + if (!got_path_is_absolute(*link_target)) { > + char *abspath; > + if (asprintf(&abspath, "%s/%s", parent_path, > + *link_target) == -1) { > + err = got_error_from_errno("asprintf"); > + goto done; > + } > + free(*link_target); > + *link_target = malloc(PATH_MAX); > + if (*link_target == NULL) { > + err = got_error_from_errno("malloc"); > + goto done; > + } > + err = got_canonpath(abspath, *link_target, PATH_MAX); > + free(abspath); > + if (err) > + goto done; > + } > + } > +done: > + free(tree_obj_id); > + if (tree) > + got_object_tree_close(tree); > + if (err) { > + free(*link_target); > + *link_target = NULL; > + } > + return err; > +} > + > +const struct got_error * > +got_object_resolve_symlinks(char **link_target, const char *path, > + struct got_object_id *commit_id, struct got_repository *repo) > +{ > + const struct got_error *err = NULL; > + char *next_target = NULL; > + int max_recursion = 40; /* matches Git */ > + > + *link_target = NULL; > + > + do { > + err = resolve_symlink(&next_target, > + *link_target ? *link_target : path, commit_id, repo); > + if (err) > + break; > + if (next_target) { > + free(*link_target); > + if (--max_recursion == 0) { > + err = got_error_path(path, GOT_ERR_RECURSION); > + *link_target = NULL; > + break; > + } > + *link_target = next_target; > + } > + } while (next_target); > + > + return err; > } > > const struct got_error * > blob - fa3eec221ff24961d74441b2e25a3a6186877ecd > blob + ed77449fe0df86c0f53685f3fd20ccfcb57d314e > --- lib/object_create.c > +++ lib/object_create.c > @@ -128,10 +128,15 @@ got_object_blob_create(struct got_object_id **id, cons > SHA1Init(&sha1_ctx); > > fd = open(ondisk_path, O_RDONLY | O_NOFOLLOW); > - if (fd == -1) > - return got_error_from_errno2("open", ondisk_path); > + if (fd == -1) { > + if (errno != ELOOP) > + return got_error_from_errno2("open", ondisk_path); > > - if (fstat(fd, &sb) == -1) { > + if (lstat(ondisk_path, &sb) == -1) { > + err = got_error_from_errno2("lstat", ondisk_path); > + goto done; > + } > + } else if (fstat(fd, &sb) == -1) { > err = got_error_from_errno2("fstat", ondisk_path); > goto done; > } > @@ -156,13 +161,21 @@ got_object_blob_create(struct got_object_id **id, cons > goto done; > } > for (;;) { > - char buf[8192]; > + char buf[PATH_MAX * 8]; > ssize_t inlen; > > - inlen = read(fd, buf, sizeof(buf)); > - if (inlen == -1) { > - err = got_error_from_errno("read"); > - goto done; > + if (S_ISLNK(sb.st_mode)) { > + inlen = readlink(ondisk_path, buf, sizeof(buf)); > + if (inlen == -1) { > + err = got_error_from_errno("readlink"); > + goto done; > + } > + } else { > + inlen = read(fd, buf, sizeof(buf)); > + if (inlen == -1) { > + err = got_error_from_errno("read"); > + goto done; > + } > } > if (inlen == 0) > break; /* EOF */ > @@ -172,6 +185,8 @@ got_object_blob_create(struct got_object_id **id, cons > err = got_ferror(blobfile, GOT_ERR_IO); > goto done; > } > + if (S_ISLNK(sb.st_mode)) > + break; > } > > *id = malloc(sizeof(**id)); > @@ -218,6 +233,8 @@ te_mode2str(char *buf, size_t len, struct got_tree_ent > mode |= S_IXUSR | S_IXGRP | S_IXOTH; > } else if (got_object_tree_entry_is_submodule(te)) > mode = S_IFDIR | S_IFLNK; > + else if (S_ISLNK(te->mode)) > + mode = S_IFLNK; /* Git leaves all the other bits unset. */ > else if (S_ISDIR(te->mode)) > mode = S_IFDIR; /* Git leaves all the other bits unset. */ > else > blob - 7531c15dc53560bdff8f322c2b1f6f3c61d849f4 > blob + 84525c88d4fa50d981704ce73e73905deac9e91a > --- lib/repository.c > +++ lib/repository.c > @@ -1454,7 +1454,12 @@ alloc_added_blob_tree_entry(struct got_tree_entry **ne > goto done; > } > > - (*new_te)->mode = S_IFREG | (mode & ((S_IRWXU | S_IRWXG | S_IRWXO))); > + if (S_ISLNK(mode)) { > + (*new_te)->mode = S_IFLNK; > + } else { > + (*new_te)->mode = S_IFREG; > + (*new_te)->mode |= (mode & (S_IRWXU | S_IRWXG | S_IRWXO)); > + } > memcpy(&(*new_te)->id, blob_id, sizeof((*new_te)->id)); > done: > if (err && *new_te) { > @@ -1601,7 +1606,7 @@ write_tree(struct got_object_id **new_tree_id, const c > err = NULL; > continue; > } > - } else if (de->d_type == DT_REG) { > + } else if (de->d_type == DT_REG || de->d_type == DT_LNK) { > err = import_file(&new_te, de, path_dir, repo); > if (err) > goto done; > @@ -1622,7 +1627,7 @@ write_tree(struct got_object_id **new_tree_id, const c > TAILQ_FOREACH(pe, &paths, entry) { > struct got_tree_entry *te = pe->data; > char *path; > - if (!S_ISREG(te->mode)) > + if (!S_ISREG(te->mode) && !S_ISLNK(te->mode)) > continue; > if (asprintf(&path, "%s/%s", path_dir, pe->path) == -1) { > err = got_error_from_errno("asprintf"); > blob - 985b0d77e0e47f32bdbe60268d8184e7df1a8e47 > blob + 0a072932c20e3ee4dec70243cb7cdc3739d3c8ba > --- lib/worktree.c > +++ lib/worktree.c > @@ -376,9 +376,9 @@ open_worktree(struct got_worktree **worktree, const ch > } > (*worktree)->lockfd = -1; > > - (*worktree)->root_path = strdup(path); > + (*worktree)->root_path = realpath(path, NULL); > if ((*worktree)->root_path == NULL) { > - err = got_error_from_errno("strdup"); > + err = got_error_from_errno2("realpath", path); > goto done; > } > err = read_meta_file(&(*worktree)->repo_path, path_got, > @@ -742,6 +742,8 @@ merge_file(int *local_changes_subsumed, struct got_wor > char *merged_path = NULL, *base_path = NULL; > int overlapcnt = 0; > char *parent; > + char *symlink_path = NULL; > + FILE *symlinkf = NULL; > > *local_changes_subsumed = 0; > > @@ -779,8 +781,45 @@ merge_file(int *local_changes_subsumed, struct got_wor > */ > } > > + /* > + * In order the run a 3-way merge with a symlink we copy the symlink's > + * target path into a temporary file and use that file with diff3. > + */ > + if (S_ISLNK(st_mode)) { > + char target_path[PATH_MAX]; > + ssize_t target_len; > + size_t n; > + > + free(base_path); > + if (asprintf(&base_path, "%s/got-symlink-merge", > + parent) == -1) { > + err = got_error_from_errno("asprintf"); > + base_path = NULL; > + goto done; > + } > + err = got_opentemp_named(&symlink_path, &symlinkf, base_path); > + if (err) > + goto done; > + target_len = readlink(ondisk_path, target_path, > + sizeof(target_path)); > + if (target_len == -1) { > + err = got_error_from_errno2("readlink", ondisk_path); > + goto done; > + } > + n = fwrite(target_path, 1, target_len, symlinkf); > + if (n != target_len) { > + err = got_ferror(symlinkf, GOT_ERR_IO); > + goto done; > + } > + if (fflush(symlinkf) == EOF) { > + err = got_error_from_errno2("fflush", symlink_path); > + goto done; > + } > + } > + > err = got_merge_diff3(&overlapcnt, merged_fd, deriv_path, > - blob_orig_path, ondisk_path, label_deriv, label_orig, NULL); > + blob_orig_path, symlink_path ? symlink_path : ondisk_path, > + label_deriv, label_orig, NULL); > if (err) > goto done; > > @@ -817,6 +856,13 @@ done: > if (merged_path) > unlink(merged_path); > } > + if (symlink_path) { > + if (unlink(symlink_path) == -1 && err == NULL) > + err = got_error_from_errno2("unlink", symlink_path); > + } > + if (symlinkf && fclose(symlinkf) == EOF && err == NULL) > + err = got_error_from_errno2("fclose", symlink_path); > + free(symlink_path); > if (merged_fd != -1 && close(merged_fd) != 0 && err == NULL) > err = got_error_from_errno("close"); > if (f_orig && fclose(f_orig) != 0 && err == NULL) > @@ -830,7 +876,195 @@ done: > return err; > } > > +static const struct got_error * > +update_symlink(const char *ondisk_path, const char *target_path, > + size_t target_len) > +{ > + /* This is not atomic but matches what 'ln -sf' does. */ > + if (unlink(ondisk_path) == -1) > + return got_error_from_errno2("unlink", ondisk_path); > + if (symlink(target_path, ondisk_path) == -1) > + return got_error_from_errno3("symlink", target_path, > + ondisk_path); > + return NULL; > +} > + > /* > + * Overwrite a symlink (or a regular file in case there was a "bad" symlink) > + * in the work tree with a file that contains conflict markers and the > + * conflicting target paths of the original version, a "derived version" > + * of a symlink from an incoming change, and a local version of the symlink. > + * > + * The original versions's target path can be NULL if it is not available, > + * such as if both derived versions added a new symlink at the same path. > + * > + * The incoming derived symlink target is NULL in case the incoming change > + * has deleted this symlink. > + */ > +static const struct got_error * > +install_symlink_conflict(const char *deriv_target, > + struct got_object_id *deriv_base_commit_id, const char *orig_target, > + const char *label_orig, const char *local_target, const char *ondisk_path) > +{ > + const struct got_error *err; > + char *id_str = NULL, *label_deriv = NULL, *path = NULL; > + FILE *f = NULL; > + > + err = got_object_id_str(&id_str, deriv_base_commit_id); > + if (err) > + return got_error_from_errno("asprintf"); > + > + if (asprintf(&label_deriv, "%s: commit %s", > + GOT_MERGE_LABEL_MERGED, id_str) == -1) { > + err = got_error_from_errno("asprintf"); > + goto done; > + } > + > + err = got_opentemp_named(&path, &f, "got-symlink-conflict"); > + if (err) > + goto done; > + > + if (fprintf(f, "%s %s\n%s\n%s%s%s%s%s\n%s\n%s\n", > + GOT_DIFF_CONFLICT_MARKER_BEGIN, label_deriv, > + deriv_target ? deriv_target : "(symlink was deleted)", > + orig_target ? label_orig : "", > + orig_target ? "\n" : "", > + orig_target ? orig_target : "", > + orig_target ? "\n" : "", > + GOT_DIFF_CONFLICT_MARKER_SEP, > + local_target, GOT_DIFF_CONFLICT_MARKER_END) < 0) { > + err = got_error_from_errno2("fprintf", path); > + goto done; > + } > + > + if (unlink(ondisk_path) == -1) { > + err = got_error_from_errno2("unlink", ondisk_path); > + goto done; > + } > + if (rename(path, ondisk_path) == -1) { > + err = got_error_from_errno3("rename", path, ondisk_path); > + goto done; > + } > + if (chmod(ondisk_path, GOT_DEFAULT_FILE_MODE) == -1) { > + err = got_error_from_errno2("chmod", ondisk_path); > + goto done; > + } > +done: > + if (f != NULL && fclose(f) == EOF && err == NULL) > + err = got_error_from_errno2("fclose", path); > + free(path); > + free(id_str); > + free(label_deriv); > + return err; > +} > + > +/* forward declaration */ > +static const struct got_error * > +merge_blob(int *, struct got_worktree *, struct got_blob_object *, > + const char *, const char *, uint16_t, const char *, > + struct got_blob_object *, struct got_object_id *, > + struct got_repository *, got_worktree_checkout_cb, void *); > + > +/* > + * Merge a symlink into the work tree, where blob_orig acts as the common > + * ancestor, deriv_target is the link target of the first derived version, > + * and the symlink on disk acts as the second derived version. > + * Assume that contents of both blobs represent symlinks. > + */ > +static const struct got_error * > +merge_symlink(struct got_worktree *worktree, > + struct got_blob_object *blob_orig, const char *ondisk_path, > + const char *path, const char *label_orig, const char *deriv_target, > + struct got_object_id *deriv_base_commit_id, struct got_repository *repo, > + got_worktree_checkout_cb progress_cb, void *progress_arg) > +{ > + const struct got_error *err = NULL; > + char *ancestor_target = NULL; > + struct stat sb; > + ssize_t ondisk_len, deriv_len; > + char ondisk_target[PATH_MAX]; > + int have_local_change = 0; > + int have_incoming_change = 0; > + > + if (lstat(ondisk_path, &sb) == -1) > + return got_error_from_errno2("lstat", ondisk_path); > + > + ondisk_len = readlink(ondisk_path, ondisk_target, > + sizeof(ondisk_target)); > + if (ondisk_len == -1) { > + err = got_error_from_errno2("readlink", > + ondisk_path); > + goto done; > + } > + ondisk_target[ondisk_len] = '\0'; > + > + if (blob_orig) { > + err = got_object_blob_read_to_str(&ancestor_target, blob_orig); > + if (err) > + goto done; > + } > + > + if (ancestor_target == NULL || > + (ondisk_len != strlen(ancestor_target) || > + memcmp(ondisk_target, ancestor_target, ondisk_len) != 0)) > + have_local_change = 1; > + > + deriv_len = strlen(deriv_target); > + if (ancestor_target == NULL || > + (deriv_len != strlen(ancestor_target) || > + memcmp(deriv_target, ancestor_target, deriv_len) != 0)) > + have_incoming_change = 1; > + > + if (!have_local_change && !have_incoming_change) { > + if (ancestor_target) { > + /* Both sides made the same change. */ > + err = (*progress_cb)(progress_arg, GOT_STATUS_MERGE, > + path); > + } else if (deriv_len == ondisk_len && > + memcmp(ondisk_target, deriv_target, deriv_len) == 0) { > + /* Both sides added the same symlink. */ > + err = (*progress_cb)(progress_arg, GOT_STATUS_MERGE, > + path); > + } else { > + /* Both sides added symlinks which don't match. */ > + err = install_symlink_conflict(deriv_target, > + deriv_base_commit_id, ancestor_target, > + label_orig, ondisk_target, ondisk_path); > + if (err) > + goto done; > + err = (*progress_cb)(progress_arg, GOT_STATUS_CONFLICT, > + path); > + } > + } else if (!have_local_change && have_incoming_change) { > + /* Apply the incoming change. */ > + err = update_symlink(ondisk_path, deriv_target, > + strlen(deriv_target)); > + if (err) > + goto done; > + err = (*progress_cb)(progress_arg, GOT_STATUS_MERGE, path); > + } else if (have_local_change && have_incoming_change) { > + if (deriv_len == ondisk_len && > + memcmp(deriv_target, ondisk_target, deriv_len) == 0) { > + /* Both sides made the same change. */ > + err = (*progress_cb)(progress_arg, GOT_STATUS_MERGE, > + path); > + } else { > + err = install_symlink_conflict(deriv_target, > + deriv_base_commit_id, ancestor_target, label_orig, > + ondisk_target, ondisk_path); > + if (err) > + goto done; > + err = (*progress_cb)(progress_arg, GOT_STATUS_CONFLICT, > + path); > + } > + } > + > +done: > + free(ancestor_target); > + return err; > +} > + > +/* > * Perform a 3-way merge where blob_orig acts as the common ancestor, > * blob_deriv acts as the first derived version, and the file on disk > * acts as the second derived version. > @@ -895,13 +1129,15 @@ done: > } > > static const struct got_error * > -create_fileindex_entry(struct got_fileindex *fileindex, > - struct got_object_id *base_commit_id, const char *ondisk_path, > - const char *path, struct got_object_id *blob_id) > +create_fileindex_entry(struct got_fileindex_entry **new_iep, > + struct got_fileindex *fileindex, struct got_object_id *base_commit_id, > + const char *ondisk_path, const char *path, struct got_object_id *blob_id) > { > const struct got_error *err = NULL; > struct got_fileindex_entry *new_ie; > > + *new_iep = NULL; > + > err = got_fileindex_entry_alloc(&new_ie, path); > if (err) > return err; > @@ -915,6 +1151,8 @@ create_fileindex_entry(struct got_fileindex *fileindex > done: > if (err) > got_fileindex_entry_free(new_ie); > + else > + *new_iep = new_ie; > return err; > } > > @@ -935,14 +1173,252 @@ get_ondisk_perms(int executable, mode_t st_mode) > return (st_mode & ~(S_IXUSR | S_IXGRP | S_IXOTH)); > } > > +/* forward declaration */ > static const struct got_error * > install_blob(struct got_worktree *worktree, const char *ondisk_path, > const char *path, mode_t te_mode, mode_t st_mode, > struct got_blob_object *blob, int restoring_missing_file, > - int reverting_versioned_file, struct got_repository *repo, > + int reverting_versioned_file, int installing_bad_symlink, > + int path_is_unversioned, struct got_repository *repo, > + got_worktree_checkout_cb progress_cb, void *progress_arg); > + > +/* > + * This function assumes that the provided symlink target points at a > + * safe location in the work tree! > + */ > +static const struct got_error * > +replace_existing_symlink(const char *ondisk_path, const char *target_path, > + size_t target_len) > +{ > + const struct got_error *err = NULL; > + ssize_t elen; > + char etarget[PATH_MAX]; > + int fd; > + > + /* > + * "Bad" symlinks (those pointing outside the work tree or into the > + * .got directory) are installed in the work tree as a regular file > + * which contains the bad symlink target path. > + * The new symlink target has already been checked for safety by our > + * caller. If we can successfully open a regular file then we simply > + * replace this file with a symlink below. > + */ > + fd = open(ondisk_path, O_RDWR | O_EXCL | O_NOFOLLOW); > + if (fd == -1) { > + if (errno != ELOOP) > + return got_error_from_errno2("open", ondisk_path); > + > + /* We are updating an existing on-disk symlink. */ > + elen = readlink(ondisk_path, etarget, sizeof(etarget)); > + if (elen == -1) > + return got_error_from_errno2("readlink", ondisk_path); > + > + if (elen == target_len && > + memcmp(etarget, target_path, target_len) == 0) > + return NULL; /* nothing to do */ > + } > + > + err = update_symlink(ondisk_path, target_path, target_len); > + if (fd != -1 && close(fd) == -1 && err == NULL) > + err = got_error_from_errno2("close", ondisk_path); > + return err; > +} > + > +static const struct got_error * > +is_bad_symlink_target(int *is_bad_symlink, const char *target_path, > + size_t target_len, const char *ondisk_path, const char *wtroot_path) > +{ > + const struct got_error *err = NULL; > + char canonpath[PATH_MAX]; > + char *path_got = NULL; > + > + *is_bad_symlink = 0; > + > + if (target_len >= sizeof(canonpath)) { > + *is_bad_symlink = 1; > + return NULL; > + } > + > + /* > + * We do not use realpath(3) to resolve the symlink's target > + * path because we don't want to resolve symlinks recursively. > + * Instead we make the path absolute and then canonicalize it. > + * Relative symlink target lookup should begin at the directory > + * in which the blob object is being installed. > + */ > + if (!got_path_is_absolute(target_path)) { > + char *abspath; > + char *parent = dirname(ondisk_path); > + if (parent == NULL) > + return got_error_from_errno2("dirname", ondisk_path); > + if (asprintf(&abspath, "%s/%s", parent, target_path) == -1) > + return got_error_from_errno("asprintf"); > + if (strlen(abspath) >= sizeof(canonpath)) { > + err = got_error_path(abspath, GOT_ERR_BAD_PATH); > + free(abspath); > + return err; > + } > + err = got_canonpath(abspath, canonpath, sizeof(canonpath)); > + free(abspath); > + if (err) > + return err; > + } else { > + err = got_canonpath(target_path, canonpath, sizeof(canonpath)); > + if (err) > + return err; > + } > + > + /* Only allow symlinks pointing at paths within the work tree. */ > + if (!got_path_is_child(canonpath, wtroot_path, strlen(wtroot_path))) { > + *is_bad_symlink = 1; > + return NULL; > + } > + > + /* Do not allow symlinks pointing into the .got directory. */ > + if (asprintf(&path_got, "%s/%s", wtroot_path, > + GOT_WORKTREE_GOT_DIR) == -1) > + return got_error_from_errno("asprintf"); > + if (got_path_is_child(canonpath, path_got, strlen(path_got))) > + *is_bad_symlink = 1; > + > + free(path_got); > + return NULL; > +} > + > +static const struct got_error * > +install_symlink(int *is_bad_symlink, struct got_worktree *worktree, > + const char *ondisk_path, const char *path, struct got_blob_object *blob, > + int restoring_missing_file, int reverting_versioned_file, > + int path_is_unversioned, struct got_repository *repo, > got_worktree_checkout_cb progress_cb, void *progress_arg) > { > const struct got_error *err = NULL; > + char target_path[PATH_MAX]; > + size_t len, target_len = 0; > + char *path_got = NULL; > + const uint8_t *buf = got_object_blob_get_read_buf(blob); > + size_t hdrlen = got_object_blob_get_hdrlen(blob); > + > + *is_bad_symlink = 0; > + > + /* > + * Blob object content specifies the target path of the link. > + * If a symbolic link cannot be installed we instead create > + * a regular file which contains the link target path stored > + * in the blob object. > + */ > + do { > + err = got_object_blob_read_block(&len, blob); > + if (len + target_len >= sizeof(target_path)) { > + /* Path too long; install as a regular file. */ > + *is_bad_symlink = 1; > + got_object_blob_rewind(blob); > + return install_blob(worktree, ondisk_path, path, > + GOT_DEFAULT_FILE_MODE, GOT_DEFAULT_FILE_MODE, blob, > + restoring_missing_file, reverting_versioned_file, > + 1, path_is_unversioned, repo, progress_cb, > + progress_arg); > + } > + if (len > 0) { > + /* Skip blob object header first time around. */ > + memcpy(target_path + target_len, buf + hdrlen, > + len - hdrlen); > + target_len += len - hdrlen; > + hdrlen = 0; > + } > + } while (len != 0); > + target_path[target_len] = '\0'; > + > + err = is_bad_symlink_target(is_bad_symlink, target_path, target_len, > + ondisk_path, worktree->root_path); > + if (err) > + return err; > + > + if (*is_bad_symlink) { > + /* install as a regular file */ > + *is_bad_symlink = 1; > + got_object_blob_rewind(blob); > + err = install_blob(worktree, ondisk_path, path, > + GOT_DEFAULT_FILE_MODE, GOT_DEFAULT_FILE_MODE, blob, > + restoring_missing_file, reverting_versioned_file, 1, > + path_is_unversioned, repo, progress_cb, progress_arg); > + goto done; > + } > + > + if (symlink(target_path, ondisk_path) == -1) { > + if (errno == EEXIST) { > + if (path_is_unversioned) { > + err = (*progress_cb)(progress_arg, > + GOT_STATUS_UNVERSIONED, path); > + goto done; > + } > + err = replace_existing_symlink(ondisk_path, > + target_path, target_len); > + if (err) > + goto done; > + if (progress_cb) { > + err = (*progress_cb)(progress_arg, > + reverting_versioned_file ? > + GOT_STATUS_REVERT : GOT_STATUS_UPDATE, > + path); > + } > + goto done; /* Nothing else to do. */ > + } > + > + if (errno == ENOENT) { > + char *parent = dirname(ondisk_path); > + if (parent == NULL) { > + err = got_error_from_errno2("dirname", > + ondisk_path); > + goto done; > + } > + err = add_dir_on_disk(worktree, parent); > + if (err) > + goto done; > + /* > + * Retry, and fall through to error handling > + * below if this second attempt fails. > + */ > + if (symlink(target_path, ondisk_path) != -1) { > + err = NULL; /* success */ > + goto done; > + } > + } > + > + /* Handle errors from first or second creation attempt. */ > + if (errno == ENAMETOOLONG) { > + /* bad target path; install as a regular file */ > + *is_bad_symlink = 1; > + got_object_blob_rewind(blob); > + err = install_blob(worktree, ondisk_path, path, > + GOT_DEFAULT_FILE_MODE, GOT_DEFAULT_FILE_MODE, blob, > + restoring_missing_file, reverting_versioned_file, 1, > + path_is_unversioned, repo, > + progress_cb, progress_arg); > + } else if (errno == ENOTDIR) { > + err = got_error_path(ondisk_path, > + GOT_ERR_FILE_OBSTRUCTED); > + } else { > + err = got_error_from_errno3("symlink", > + target_path, ondisk_path); > + } > + } else if (progress_cb) > + err = (*progress_cb)(progress_arg, reverting_versioned_file ? > + GOT_STATUS_REVERT : GOT_STATUS_ADD, path); > +done: > + free(path_got); > + return err; > +} > + > +static const struct got_error * > +install_blob(struct got_worktree *worktree, const char *ondisk_path, > + const char *path, mode_t te_mode, mode_t st_mode, > + struct got_blob_object *blob, int restoring_missing_file, > + int reverting_versioned_file, int installing_bad_symlink, > + int path_is_unversioned, struct got_repository *repo, > + got_worktree_checkout_cb progress_cb, void *progress_arg) > +{ > + const struct got_error *err = NULL; > int fd = -1; > size_t len, hdrlen; > int update = 0; > @@ -965,7 +1441,12 @@ install_blob(struct got_worktree *worktree, const char > return got_error_from_errno2("open", > ondisk_path); > } else if (errno == EEXIST) { > - if (!S_ISREG(st_mode)) { > + if (path_is_unversioned) { > + err = (*progress_cb)(progress_arg, > + GOT_STATUS_UNVERSIONED, path); > + goto done; > + } > + if (!S_ISREG(st_mode) && !installing_bad_symlink) { > /* TODO file is obstructed; do something */ > err = got_error_path(ondisk_path, > GOT_ERR_FILE_OBSTRUCTED); > @@ -981,15 +1462,19 @@ install_blob(struct got_worktree *worktree, const char > return got_error_from_errno2("open", ondisk_path); > } > > - if (restoring_missing_file) > - err = (*progress_cb)(progress_arg, GOT_STATUS_MISSING, path); > - else if (reverting_versioned_file) > - err = (*progress_cb)(progress_arg, GOT_STATUS_REVERT, path); > - else > - err = (*progress_cb)(progress_arg, > - update ? GOT_STATUS_UPDATE : GOT_STATUS_ADD, path); > - if (err) > - goto done; > + if (progress_cb) { > + if (restoring_missing_file) > + err = (*progress_cb)(progress_arg, GOT_STATUS_MISSING, > + path); > + else if (reverting_versioned_file) > + err = (*progress_cb)(progress_arg, GOT_STATUS_REVERT, > + path); > + else > + err = (*progress_cb)(progress_arg, > + update ? GOT_STATUS_UPDATE : GOT_STATUS_ADD, path); > + if (err) > + goto done; > + } > > hdrlen = got_object_blob_get_hdrlen(blob); > do { > @@ -1108,6 +1593,59 @@ get_staged_status(struct got_fileindex_entry *ie) > } > > static const struct got_error * > +get_symlink_modification_status(unsigned char *status, > + struct got_fileindex_entry *ie, const char *abspath, > + int dirfd, const char *de_name, struct got_blob_object *blob) > +{ > + const struct got_error *err = NULL; > + char target_path[PATH_MAX]; > + char etarget[PATH_MAX]; > + ssize_t elen; > + size_t len, target_len = 0; > + const uint8_t *buf = got_object_blob_get_read_buf(blob); > + size_t hdrlen = got_object_blob_get_hdrlen(blob); > + > + *status = GOT_STATUS_NO_CHANGE; > + > + /* Blob object content specifies the target path of the link. */ > + do { > + err = got_object_blob_read_block(&len, blob); > + if (err) > + return err; > + if (len + target_len >= sizeof(target_path)) { > + /* > + * Should not happen. The blob contents were OK > + * when this symlink was installed. > + */ > + return got_error(GOT_ERR_NO_SPACE); > + } > + if (len > 0) { > + /* Skip blob object header first time around. */ > + memcpy(target_path + target_len, buf + hdrlen, > + len - hdrlen); > + target_len += len - hdrlen; > + hdrlen = 0; > + } > + } while (len != 0); > + target_path[target_len] = '\0'; > + > + if (dirfd != -1) { > + elen = readlinkat(dirfd, de_name, etarget, sizeof(etarget)); > + if (elen == -1) > + return got_error_from_errno2("readlinkat", abspath); > + } else { > + elen = readlink(abspath, etarget, sizeof(etarget)); > + if (elen == -1) > + return got_error_from_errno2("readlink", abspath); > + } > + > + if (elen != target_len || memcmp(etarget, target_path, target_len) != 0) > + *status = GOT_STATUS_MODIFY; > + > + return NULL; > +} > + > +static const struct got_error * > get_file_status(unsigned char *status, struct stat *sb, > struct got_fileindex_entry *ie, const char *abspath, > int dirfd, const char *de_name, struct got_repository *repo) > @@ -1143,9 +1681,12 @@ get_file_status(unsigned char *status, struct stat *sb > } > } else { > fd = open(abspath, O_RDONLY | O_NOFOLLOW); > - if (fd == -1 && errno != ENOENT) > + if (fd == -1 && errno != ENOENT && errno != ELOOP) > return got_error_from_errno2("open", abspath); > - if (fd == -1 || fstat(fd, sb) == -1) { > + else if (fd == -1 && errno == ELOOP) { > + if (lstat(abspath, sb) == -1) > + return got_error_from_errno2("lstat", abspath); > + } else if (fd == -1 || fstat(fd, sb) == -1) { > if (errno == ENOENT) { > if (got_fileindex_entry_has_file_on_disk(ie)) > *status = GOT_STATUS_MISSING; > @@ -1158,7 +1699,7 @@ get_file_status(unsigned char *status, struct stat *sb > } > } > > - if (!S_ISREG(sb->st_mode)) { > + if (!S_ISREG(sb->st_mode) && !S_ISLNK(sb->st_mode)) { > *status = GOT_STATUS_OBSTRUCTED; > goto done; > } > @@ -1175,6 +1716,12 @@ get_file_status(unsigned char *status, struct stat *sb > if (!stat_info_differs(ie, sb)) > goto done; > > + if (S_ISLNK(sb->st_mode) && > + got_fileindex_entry_filetype_get(ie) != GOT_FILEIDX_MODE_SYMLINK) { > + *status = GOT_STATUS_MODIFY; > + goto done; > + } > + > if (staged_status == GOT_STATUS_MODIFY || > staged_status == GOT_STATUS_ADD) > memcpy(id.sha1, ie->staged_blob_sha1, sizeof(id.sha1)); > @@ -1185,6 +1732,12 @@ get_file_status(unsigned char *status, struct stat *sb > if (err) > goto done; > > + if (S_ISLNK(sb->st_mode)) { > + err = get_symlink_modification_status(status, ie, > + abspath, dirfd, de_name, blob); > + goto done; > + } > + > if (dirfd != -1) { > fd = openat(dirfd, de_name, O_RDONLY | O_NOFOLLOW); > if (fd == -1) { > @@ -1289,8 +1842,10 @@ update_blob(struct got_worktree *worktree, > goto done; > if (status == GOT_STATUS_MISSING || status == GOT_STATUS_DELETE) > sb.st_mode = got_fileindex_perms_to_st(ie); > - } else > + } else { > sb.st_mode = GOT_DEFAULT_FILE_MODE; > + status = GOT_STATUS_UNVERSIONED; > + } > > if (status == GOT_STATUS_OBSTRUCTED) { > err = (*progress_cb)(progress_arg, status, path); > @@ -1351,10 +1906,21 @@ update_blob(struct got_worktree *worktree, > goto done; > } > } > - err = merge_blob(&update_timestamps, worktree, blob2, > - ondisk_path, path, sb.st_mode, label_orig, blob, > - worktree->base_commit_id, repo, > - progress_cb, progress_arg); > + if (S_ISLNK(te->mode) && S_ISLNK(sb.st_mode)) { > + char *link_target; > + err = got_object_blob_read_to_str(&link_target, blob); > + if (err) > + goto done; > + err = merge_symlink(worktree, blob2, ondisk_path, path, > + label_orig, link_target, worktree->base_commit_id, > + repo, progress_cb, progress_arg); > + free(link_target); > + } else { > + err = merge_blob(&update_timestamps, worktree, blob2, > + ondisk_path, path, sb.st_mode, label_orig, blob, > + worktree->base_commit_id, repo, > + progress_cb, progress_arg); > + } > free(label_orig); > if (blob2) > got_object_blob_close(blob2); > @@ -1380,21 +1946,38 @@ update_blob(struct got_worktree *worktree, > if (err) > goto done; > } else { > - err = install_blob(worktree, ondisk_path, path, te->mode, > - sb.st_mode, blob, status == GOT_STATUS_MISSING, 0, > - repo, progress_cb, progress_arg); > + int is_bad_symlink = 0; > + if (S_ISLNK(te->mode)) { > + err = install_symlink(&is_bad_symlink, worktree, > + ondisk_path, path, blob, > + status == GOT_STATUS_MISSING, 0, > + status == GOT_STATUS_UNVERSIONED, repo, > + progress_cb, progress_arg); > + } else { > + err = install_blob(worktree, ondisk_path, path, > + te->mode, sb.st_mode, blob, > + status == GOT_STATUS_MISSING, 0, 0, > + status == GOT_STATUS_UNVERSIONED, repo, > + progress_cb, progress_arg); > + } > if (err) > goto done; > + > if (ie) { > err = got_fileindex_entry_update(ie, ondisk_path, > blob->id.sha1, worktree->base_commit_id->sha1, 1); > } else { > - err = create_fileindex_entry(fileindex, > + err = create_fileindex_entry(&ie, fileindex, > worktree->base_commit_id, ondisk_path, path, > &blob->id); > } > if (err) > goto done; > + > + if (is_bad_symlink) { > + got_fileindex_entry_filetype_set(ie, > + GOT_FILEIDX_MODE_BAD_SYMLINK); > + } > } > got_object_blob_close(blob); > done: > @@ -1451,6 +2034,25 @@ delete_blob(struct got_worktree *worktree, struct got_ > if (err) > goto done; > > + if (S_ISLNK(sb.st_mode) && status != GOT_STATUS_NO_CHANGE) { > + char ondisk_target[PATH_MAX]; > + ssize_t ondisk_len = readlink(ondisk_path, ondisk_target, > + sizeof(ondisk_target)); > + if (ondisk_len == -1) { > + err = got_error_from_errno2("readlink", ondisk_path); > + goto done; > + } > + ondisk_target[ondisk_len] = '\0'; > + err = install_symlink_conflict(NULL, worktree->base_commit_id, > + NULL, NULL, /* XXX pass common ancestor info? */ > + ondisk_target, ondisk_path); > + if (err) > + goto done; > + err = (*progress_cb)(progress_arg, GOT_STATUS_CONFLICT, > + ie->path); > + goto done; > + } > + > if (status == GOT_STATUS_MODIFY || status == GOT_STATUS_CONFLICT || > status == GOT_STATUS_ADD) { > err = (*progress_cb)(progress_arg, GOT_STATUS_MERGE, ie->path); > @@ -2113,9 +2715,21 @@ merge_file_cb(void *arg, struct got_blob_object *blob1 > goto done; > } > > - err = merge_blob(&local_changes_subsumed, a->worktree, blob1, > - ondisk_path, path2, sb.st_mode, a->label_orig, blob2, > - a->commit_id2, repo, a->progress_cb, a->progress_arg); > + if (S_ISLNK(mode1) && S_ISLNK(mode2)) { > + char *link_target2; > + err = got_object_blob_read_to_str(&link_target2, blob2); > + if (err) > + goto done; > + err = merge_symlink(a->worktree, blob1, ondisk_path, > + path2, a->label_orig, link_target2, a->commit_id2, > + repo, a->progress_cb, a->progress_arg); > + free(link_target2); > + } else { > + err = merge_blob(&local_changes_subsumed, a->worktree, > + blob1, ondisk_path, path2, sb.st_mode, > + a->label_orig, blob2, a->commit_id2, repo, > + a->progress_cb, a->progress_arg); > + } > } else if (blob1) { > ie = got_fileindex_entry_get(a->fileindex, path1, > strlen(path1)); > @@ -2188,11 +2802,29 @@ merge_file_cb(void *arg, struct got_blob_object *blob1 > status, path2); > goto done; > } > - err = merge_blob(&local_changes_subsumed, a->worktree, > - NULL, ondisk_path, path2, sb.st_mode, > - a->label_orig, blob2, a->commit_id2, repo, > - a->progress_cb, > - a->progress_arg); > + if (S_ISLNK(mode2) && S_ISLNK(sb.st_mode)) { > + char *link_target2; > + err = got_object_blob_read_to_str(&link_target2, > + blob2); > + if (err) > + goto done; > + err = merge_symlink(a->worktree, NULL, > + ondisk_path, path2, a->label_orig, > + link_target2, a->commit_id2, repo, > + a->progress_cb, a->progress_arg); > + free(link_target2); > + } else if (S_ISREG(sb.st_mode)) { > + err = merge_blob(&local_changes_subsumed, > + a->worktree, NULL, ondisk_path, path2, > + sb.st_mode, a->label_orig, blob2, > + a->commit_id2, repo, a->progress_cb, > + a->progress_arg); > + } else { > + err = got_error_path(ondisk_path, > + GOT_ERR_FILE_OBSTRUCTED); > + } > + if (err) > + goto done; > if (status == GOT_STATUS_DELETE) { > err = got_fileindex_entry_update(ie, > ondisk_path, blob2->id.sha1, > @@ -2201,12 +2833,17 @@ merge_file_cb(void *arg, struct got_blob_object *blob1 > goto done; > } > } else { > + int is_bad_symlink = 0; > sb.st_mode = GOT_DEFAULT_FILE_MODE; > - err = install_blob(a->worktree, ondisk_path, path2, > - /* XXX get this from parent tree! */ > - GOT_DEFAULT_FILE_MODE, > - sb.st_mode, blob2, 0, 0, repo, > - a->progress_cb, a->progress_arg); > + if (S_ISLNK(mode2)) { > + err = install_symlink(&is_bad_symlink, > + a->worktree, ondisk_path, path2, blob2, 0, > + 0, 1, repo, a->progress_cb, a->progress_arg); > + } else { > + err = install_blob(a->worktree, ondisk_path, path2, > + mode2, sb.st_mode, blob2, 0, 0, 0, 1, repo, > + a->progress_cb, a->progress_arg); > + } > if (err) > goto done; > err = got_fileindex_entry_alloc(&ie, path2); > @@ -2223,6 +2860,10 @@ merge_file_cb(void *arg, struct got_blob_object *blob1 > got_fileindex_entry_free(ie); > goto done; > } > + if (is_bad_symlink) { > + got_fileindex_entry_filetype_set(ie, > + GOT_FILEIDX_MODE_BAD_SYMLINK); > + } > } > } > done: > @@ -2675,10 +3316,6 @@ status_new(void *arg, struct dirent *de, const char *p > if (a->cancel_cb && a->cancel_cb(a->cancel_arg)) > return got_error(GOT_ERR_CANCELLED); > > - /* XXX ignore symlinks for now */ > - if (de->d_type == DT_LNK) > - return NULL; > - > if (parent_path[0]) { > if (asprintf(&path, "%s/%s", parent_path, de->d_name) == -1) > return got_error_from_errno("asprintf"); > @@ -2737,7 +3374,7 @@ void *status_arg, struct got_repository *repo, int rep > return NULL; > } > > - if (S_ISREG(sb.st_mode)) > + if (S_ISREG(sb.st_mode) || S_ISLNK(sb.st_mode)) > return (*status_cb)(status_arg, GOT_STATUS_UNVERSIONED, > GOT_STATUS_NO_CHANGE, path, NULL, NULL, NULL, -1, NULL); > > @@ -2813,7 +3450,8 @@ worktree_status(struct got_worktree *worktree, const c > > fd = open(ondisk_path, O_RDONLY | O_NOFOLLOW | O_DIRECTORY); > if (fd == -1) { > - if (errno != ENOTDIR && errno != ENOENT && errno != EACCES) > + if (errno != ENOTDIR && errno != ENOENT && errno != EACCES && > + errno != ELOOP) > err = got_error_from_errno2("open", ondisk_path); > else > err = report_single_file_status(path, ondisk_path, > @@ -2883,24 +3521,61 @@ got_worktree_resolve_path(char **wt_path, struct got_w > const char *arg) > { > const struct got_error *err = NULL; > - char *resolved, *cwd = NULL, *path = NULL; > + char *resolved = NULL, *cwd = NULL, *path = NULL; > size_t len; > + struct stat sb; > > *wt_path = NULL; > > - resolved = realpath(arg, NULL); > - if (resolved == NULL) { > - if (errno != ENOENT) > - return got_error_from_errno2("realpath", arg); > - cwd = getcwd(NULL, 0); > - if (cwd == NULL) > - return got_error_from_errno("getcwd"); > - if (asprintf(&resolved, "%s/%s", cwd, arg) == -1) { > - err = got_error_from_errno("asprintf"); > + cwd = getcwd(NULL, 0); > + if (cwd == NULL) > + return got_error_from_errno("getcwd"); > + > + if (lstat(arg, &sb) == -1) { > + if (errno != ENOENT) { > + err = got_error_from_errno2("lstat", arg); > goto done; > } > } > + if (S_ISLNK(sb.st_mode)) { > + /* > + * We cannot use realpath(3) with symlinks since we want to > + * operate on the symlink itself. > + * But we can make the path absolute, assuming it is relative > + * to the current working directory, and then canonicalize it. > + */ > + char *abspath = NULL; > + char canonpath[PATH_MAX]; > + if (!got_path_is_absolute(arg)) { > + if (asprintf(&abspath, "%s/%s", cwd, arg) == -1) { > + err = got_error_from_errno("asprintf"); > + goto done; > + } > > + } > + err = got_canonpath(abspath ? abspath : arg, canonpath, > + sizeof(canonpath)); > + if (err) > + goto done; > + resolved = strdup(canonpath); > + if (resolved == NULL) { > + err = got_error_from_errno("strdup"); > + goto done; > + } > + } else { > + resolved = realpath(arg, NULL); > + if (resolved == NULL) { > + if (errno != ENOENT) { > + err = got_error_from_errno2("realpath", arg); > + goto done; > + } > + if (asprintf(&resolved, "%s/%s", cwd, arg) == -1) { > + err = got_error_from_errno("asprintf"); > + goto done; > + } > + } > + } > + > if (strncmp(got_worktree_get_root_path(worktree), resolved, > strlen(got_worktree_get_root_path(worktree)))) { > err = got_error_path(resolved, GOT_ERR_BAD_PATH); > @@ -3403,6 +4078,8 @@ create_patched_content(char **path_outfile, int revers > struct got_blob_object *blob = NULL; > FILE *f1 = NULL, *f2 = NULL, *outfile = NULL; > int fd2 = -1; > + char link_target[PATH_MAX]; > + ssize_t link_len = 0; > char *path1 = NULL, *id_str = NULL; > struct stat sb1, sb2; > struct got_diff_changes *changes = NULL; > @@ -3421,27 +4098,62 @@ create_patched_content(char **path_outfile, int revers > if (dirfd2 != -1) { > fd2 = openat(dirfd2, de_name2, O_RDONLY | O_NOFOLLOW); > if (fd2 == -1) { > - err = got_error_from_errno2("openat", path2); > - goto done; > + if (errno != ELOOP) { > + err = got_error_from_errno2("openat", path2); > + goto done; > + } > + link_len = readlinkat(dirfd2, de_name2, > + link_target, sizeof(link_target)); > + if (link_len == -1) > + return got_error_from_errno2("readlinkat", path2); > + sb2.st_mode = S_IFLNK; > + sb2.st_size = link_len; > } > } else { > fd2 = open(path2, O_RDONLY | O_NOFOLLOW); > if (fd2 == -1) { > - err = got_error_from_errno2("open", path2); > - goto done; > + if (errno != ELOOP) { > + err = got_error_from_errno2("open", path2); > + goto done; > + } > + link_len = readlink(path2, link_target, > + sizeof(link_target)); > + if (link_len == -1) > + return got_error_from_errno2("readlink", path2); > + sb2.st_mode = S_IFLNK; > + sb2.st_size = link_len; > } > } > - if (fstat(fd2, &sb2) == -1) { > - err = got_error_from_errno2("fstat", path2); > - goto done; > - } > + if (fd2 != -1) { > + if (fstat(fd2, &sb2) == -1) { > + err = got_error_from_errno2("fstat", path2); > + goto done; > + } > > - f2 = fdopen(fd2, "r"); > - if (f2 == NULL) { > - err = got_error_from_errno2("fdopen", path2); > - goto done; > + f2 = fdopen(fd2, "r"); > + if (f2 == NULL) { > + err = got_error_from_errno2("fdopen", path2); > + goto done; > + } > + fd2 = -1; > + } else { > + size_t n; > + f2 = got_opentemp(); > + if (f2 == NULL) { > + err = got_error_from_errno2("got_opentemp", path2); > + goto done; > + } > + n = fwrite(link_target, 1, link_len, f2); > + if (n != link_len) { > + err = got_ferror(f2, GOT_ERR_IO); > + goto done; > + } > + if (fflush(f2) == EOF) { > + err = got_error_from_errno("fflush"); > + goto done; > + } > + rewind(f2); > } > - fd2 = -1; > > err = got_object_open_as_blob(&blob, repo, blob_id, 8192); > if (err) > @@ -3495,9 +4207,11 @@ create_patched_content(char **path_outfile, int revers > if (err) > goto done; > > - if (chmod(*path_outfile, sb2.st_mode) == -1) { > - err = got_error_from_errno2("chmod", path2); > - goto done; > + if (!S_ISLNK(sb2.st_mode)) { > + if (chmod(*path_outfile, sb2.st_mode) == -1) { > + err = got_error_from_errno2("chmod", path2); > + goto done; > + } > } > } > done: > @@ -3672,21 +4386,44 @@ revert_file(void *arg, unsigned char status, unsigned > > if (a->patch_cb && (status == GOT_STATUS_MODIFY || > status == GOT_STATUS_CONFLICT)) { > + int is_bad_symlink = 0; > err = create_patched_content(&path_content, 1, &id, > ondisk_path, dirfd, de_name, ie->path, a->repo, > a->patch_cb, a->patch_arg); > if (err || path_content == NULL) > break; > - if (rename(path_content, ondisk_path) == -1) { > - err = got_error_from_errno3("rename", > - path_content, ondisk_path); > - goto done; > + if (te && S_ISLNK(te->mode)) { > + if (unlink(path_content) == -1) { > + err = got_error_from_errno2("unlink", > + path_content); > + break; > + } > + err = install_symlink(&is_bad_symlink, > + a->worktree, ondisk_path, ie->path, > + blob, 0, 1, 0, a->repo, > + a->progress_cb, a->progress_arg); > + } else { > + if (rename(path_content, ondisk_path) == -1) { > + err = got_error_from_errno3("rename", > + path_content, ondisk_path); > + goto done; > + } > } > } else { > - err = install_blob(a->worktree, ondisk_path, ie->path, > - te ? te->mode : GOT_DEFAULT_FILE_MODE, > - got_fileindex_perms_to_st(ie), blob, 0, 1, > - a->repo, a->progress_cb, a->progress_arg); > + int is_bad_symlink = 0; > + if (te && S_ISLNK(te->mode)) { > + err = install_symlink(&is_bad_symlink, > + a->worktree, ondisk_path, ie->path, > + blob, 0, 1, 0, a->repo, > + a->progress_cb, a->progress_arg); > + } else { > + err = install_blob(a->worktree, ondisk_path, > + ie->path, > + te ? te->mode : GOT_DEFAULT_FILE_MODE, > + got_fileindex_perms_to_st(ie), blob, > + 0, 1, 0, 0, a->repo, > + a->progress_cb, a->progress_arg); > + } > if (err) > goto done; > if (status == GOT_STATUS_DELETE || > @@ -3697,6 +4434,10 @@ revert_file(void *arg, unsigned char status, unsigned > if (err) > goto done; > } > + if (is_bad_symlink) { > + got_fileindex_entry_filetype_set(ie, > + GOT_FILEIDX_MODE_BAD_SYMLINK); > + } > } > break; > } > @@ -3781,7 +4522,9 @@ struct collect_commitables_arg { > struct got_pathlist_head *commitable_paths; > struct got_repository *repo; > struct got_worktree *worktree; > + struct got_fileindex *fileindex; > int have_staged_files; > + int allow_bad_symlinks; > }; > > static const struct got_error * > @@ -3838,9 +4581,26 @@ collect_commitables(void *arg, unsigned char status, > err = got_error_from_errno("asprintf"); > goto done; > } > - if (status == GOT_STATUS_DELETE || staged_status == GOT_STATUS_DELETE) { > - sb.st_mode = GOT_DEFAULT_FILE_MODE; > - } else { > + > + if (staged_status == GOT_STATUS_ADD || > + staged_status == GOT_STATUS_MODIFY) { > + struct got_fileindex_entry *ie; > + ie = got_fileindex_entry_get(a->fileindex, path, strlen(path)); > + switch (got_fileindex_entry_staged_filetype_get(ie)) { > + case GOT_FILEIDX_MODE_REGULAR_FILE: > + case GOT_FILEIDX_MODE_BAD_SYMLINK: > + ct->mode = S_IFREG; > + break; > + case GOT_FILEIDX_MODE_SYMLINK: > + ct->mode = S_IFLNK; > + break; > + default: > + err = got_error_path(path, GOT_ERR_BAD_FILETYPE); > + goto done; > + } > + ct->mode |= got_fileindex_entry_perms_get(ie); > + } else if (status != GOT_STATUS_DELETE && > + staged_status != GOT_STATUS_DELETE) { > if (dirfd != -1) { > if (fstatat(dirfd, de_name, &sb, > AT_SYMLINK_NOFOLLOW) == -1) { > @@ -3862,6 +4622,30 @@ collect_commitables(void *arg, unsigned char status, > goto done; > } > > + if (S_ISLNK(ct->mode) && staged_status == GOT_STATUS_NO_CHANGE && > + status == GOT_STATUS_ADD && !a->allow_bad_symlinks) { > + int is_bad_symlink; > + char target_path[PATH_MAX]; > + ssize_t target_len; > + target_len = readlink(ct->ondisk_path, target_path, > + sizeof(target_path)); > + if (target_len == -1) { > + err = got_error_from_errno2("readlink", > + ct->ondisk_path); > + goto done; > + } > + err = is_bad_symlink_target(&is_bad_symlink, target_path, > + target_len, ct->ondisk_path, a->worktree->root_path); > + if (err) > + goto done; > + if (is_bad_symlink) { > + err = got_error_path(ct->ondisk_path, > + GOT_ERR_BAD_SYMLINK); > + goto done; > + } > + } > + > + > ct->status = status; > ct->staged_status = staged_status; > ct->blob_id = NULL; /* will be filled in when blob gets created */ > @@ -3955,6 +4739,9 @@ match_ct_parent_path(int *match, struct got_commitable > static mode_t > get_ct_file_mode(struct got_commitable *ct) > { > + if (S_ISLNK(ct->mode)) > + return S_IFLNK; > + > return S_IFREG | (ct->mode & ((S_IRWXU | S_IRWXG | S_IRWXO))); > } > > @@ -4630,7 +5417,7 @@ check_non_staged_files(struct got_fileindex *fileindex > const struct got_error * > got_worktree_commit(struct got_object_id **new_commit_id, > struct got_worktree *worktree, struct got_pathlist_head *paths, > - const char *author, const char *committer, > + const char *author, const char *committer, int allow_bad_symlinks, > got_worktree_commit_msg_cb commit_msg_cb, void *commit_arg, > got_worktree_status_cb status_cb, void *status_arg, > struct got_repository *repo) > @@ -4677,8 +5464,10 @@ got_worktree_commit(struct got_object_id **new_commit_ > > cc_arg.commitable_paths = &commitable_paths; > cc_arg.worktree = worktree; > + cc_arg.fileindex = fileindex; > cc_arg.repo = repo; > cc_arg.have_staged_files = have_staged_files; > + cc_arg.allow_bad_symlinks = allow_bad_symlinks; > TAILQ_FOREACH(pe, paths, entry) { > err = worktree_status(worktree, pe->path, fileindex, repo, > collect_commitables, &cc_arg, NULL, NULL, 0, 0); > @@ -6212,6 +7001,7 @@ struct stage_path_arg { > got_worktree_patch_cb patch_cb; > void *patch_arg; > int staged_something; > + int allow_bad_symlinks; > }; > > static const struct got_error * > @@ -6226,6 +7016,7 @@ stage_path(void *arg, unsigned char status, > char *ondisk_path = NULL, *path_content = NULL; > uint32_t stage; > struct got_object_id *new_staged_blob_id = NULL; > + struct stat sb; > > if (status == GOT_STATUS_UNVERSIONED) > return NULL; > @@ -6241,6 +7032,11 @@ stage_path(void *arg, unsigned char status, > switch (status) { > case GOT_STATUS_ADD: > case GOT_STATUS_MODIFY: > + /* XXX could sb.st_mode be passed in by our caller? */ > + if (lstat(ondisk_path, &sb) == -1) { > + err = got_error_from_errno2("lstat", ondisk_path); > + break; > + } > if (a->patch_cb) { > if (status == GOT_STATUS_ADD) { > int choice = GOT_PATCH_CHOICE_NONE; > @@ -6270,6 +7066,39 @@ stage_path(void *arg, unsigned char status, > else > stage = GOT_FILEIDX_STAGE_MODIFY; > got_fileindex_entry_stage_set(ie, stage); > + if (S_ISLNK(sb.st_mode)) { > + int is_bad_symlink = 0; > + if (!a->allow_bad_symlinks) { > + char target_path[PATH_MAX]; > + ssize_t target_len; > + target_len = readlink(ondisk_path, target_path, > + sizeof(target_path)); > + if (target_len == -1) { > + err = got_error_from_errno2("readlink", > + ondisk_path); > + break; > + } > + err = is_bad_symlink_target(&is_bad_symlink, > + target_path, target_len, ondisk_path, > + a->worktree->root_path); > + if (err) > + break; > + if (is_bad_symlink) { > + err = got_error_path(ondisk_path, > + GOT_ERR_BAD_SYMLINK); > + break; > + } > + } > + if (is_bad_symlink) > + got_fileindex_entry_staged_filetype_set(ie, > + GOT_FILEIDX_MODE_BAD_SYMLINK); > + else > + got_fileindex_entry_staged_filetype_set(ie, > + GOT_FILEIDX_MODE_SYMLINK); > + } else { > + got_fileindex_entry_staged_filetype_set(ie, > + GOT_FILEIDX_MODE_REGULAR_FILE); > + } > a->staged_something = 1; > if (a->status_cb == NULL) > break; > @@ -6328,7 +7157,7 @@ got_worktree_stage(struct got_worktree *worktree, > struct got_pathlist_head *paths, > got_worktree_status_cb status_cb, void *status_arg, > got_worktree_patch_cb patch_cb, void *patch_arg, > - struct got_repository *repo) > + int allow_bad_symlinks, struct got_repository *repo) > { > const struct got_error *err = NULL, *sync_err, *unlockerr; > struct got_pathlist_entry *pe; > @@ -6379,6 +7208,7 @@ got_worktree_stage(struct got_worktree *worktree, > spa.status_cb = status_cb; > spa.status_arg = status_arg; > spa.staged_something = 0; > + spa.allow_bad_symlinks = allow_bad_symlinks; > TAILQ_FOREACH(pe, paths, entry) { > err = worktree_status(worktree, pe->path, fileindex, repo, > stage_path, &spa, NULL, NULL, 0, 0); > @@ -6541,7 +7371,7 @@ done: > free(*path_unstaged_content); > *path_unstaged_content = NULL; > } > - if (err || !have_rejected_content) { > + if (err || !have_content || !have_rejected_content) { > if (*path_new_staged_content && > unlink(*path_new_staged_content) == -1 && err == NULL) > err = got_error_from_errno2("unlink", > @@ -6562,6 +7392,97 @@ done: > } > > static const struct got_error * > +unstage_hunks(struct got_object_id *staged_blob_id, > + struct got_blob_object *blob_base, > + struct got_object_id *blob_id, struct got_fileindex_entry *ie, > + const char *ondisk_path, const char *label_orig, > + struct got_worktree *worktree, struct got_repository *repo, > + got_worktree_patch_cb patch_cb, void *patch_arg, > + got_worktree_checkout_cb progress_cb, void *progress_arg) > +{ > + const struct got_error *err = NULL; > + char *path_unstaged_content = NULL; > + char *path_new_staged_content = NULL; > + struct got_object_id *new_staged_blob_id = NULL; > + FILE *f = NULL; > + struct stat sb; > + > + err = create_unstaged_content(&path_unstaged_content, > + &path_new_staged_content, blob_id, staged_blob_id, > + ie->path, repo, patch_cb, patch_arg); > + if (err) > + return err; > + > + if (path_unstaged_content == NULL) > + return NULL; > + > + if (path_new_staged_content) { > + err = got_object_blob_create(&new_staged_blob_id, > + path_new_staged_content, repo); > + if (err) > + goto done; > + } > + > + f = fopen(path_unstaged_content, "r"); > + if (f == NULL) { > + err = got_error_from_errno2("fopen", > + path_unstaged_content); > + goto done; > + } > + if (fstat(fileno(f), &sb) == -1) { > + err = got_error_from_errno2("fstat", path_unstaged_content); > + goto done; > + } > + if (got_fileindex_entry_staged_filetype_get(ie) == > + GOT_FILEIDX_MODE_SYMLINK && sb.st_size < PATH_MAX) { > + char link_target[PATH_MAX]; > + size_t r; > + r = fread(link_target, 1, sizeof(link_target), f); > + if (r == 0 && ferror(f)) { > + err = got_error_from_errno("fread"); > + goto done; > + } > + if (r >= sizeof(link_target)) { /* should not happen */ > + err = got_error(GOT_ERR_NO_SPACE); > + goto done; > + } > + link_target[r] = '\0'; > + err = merge_symlink(worktree, blob_base, > + ondisk_path, ie->path, label_orig, link_target, > + worktree->base_commit_id, repo, progress_cb, > + progress_arg); > + } else { > + int local_changes_subsumed; > + err = merge_file(&local_changes_subsumed, worktree, > + blob_base, ondisk_path, ie->path, > + got_fileindex_perms_to_st(ie), > + path_unstaged_content, label_orig, "unstaged", > + repo, progress_cb, progress_arg); > + } > + if (err) > + goto done; > + > + if (new_staged_blob_id) { > + memcpy(ie->staged_blob_sha1, new_staged_blob_id->sha1, > + SHA1_DIGEST_LENGTH); > + } else > + got_fileindex_entry_stage_set(ie, GOT_FILEIDX_STAGE_NONE); > +done: > + free(new_staged_blob_id); > + if (path_unstaged_content && > + unlink(path_unstaged_content) == -1 && err == NULL) > + err = got_error_from_errno2("unlink", path_unstaged_content); > + if (path_new_staged_content && > + unlink(path_new_staged_content) == -1 && err == NULL) > + err = got_error_from_errno2("unlink", path_new_staged_content); > + if (f && fclose(f) != 0 && err == NULL) > + err = got_error_from_errno2("fclose", path_unstaged_content); > + free(path_unstaged_content); > + free(path_new_staged_content); > + return err; > +} > + > +static const struct got_error * > unstage_path(void *arg, unsigned char status, > unsigned char staged_status, const char *relpath, > struct got_object_id *blob_id, struct got_object_id *staged_blob_id, > @@ -6571,8 +7492,7 @@ unstage_path(void *arg, unsigned char status, > struct unstage_path_arg *a = arg; > struct got_fileindex_entry *ie; > struct got_blob_object *blob_base = NULL, *blob_staged = NULL; > - char *ondisk_path = NULL, *path_unstaged_content = NULL; > - char *path_new_staged_content = NULL; > + char *ondisk_path = NULL; > char *id_str = NULL, *label_orig = NULL; > int local_changes_subsumed; > struct stat sb; > @@ -6618,34 +7538,11 @@ unstage_path(void *arg, unsigned char status, > if (choice != GOT_PATCH_CHOICE_YES) > break; > } else { > - err = create_unstaged_content( > - &path_unstaged_content, > - &path_new_staged_content, blob_id, > - staged_blob_id, ie->path, a->repo, > - a->patch_cb, a->patch_arg); > - if (err || path_unstaged_content == NULL) > - break; > - if (path_new_staged_content) { > - err = got_object_blob_create( > - &staged_blob_id, > - path_new_staged_content, > - a->repo); > - if (err) > - break; > - memcpy(ie->staged_blob_sha1, > - staged_blob_id->sha1, > - SHA1_DIGEST_LENGTH); > - } > - err = merge_file(&local_changes_subsumed, > - a->worktree, blob_base, ondisk_path, > - relpath, got_fileindex_perms_to_st(ie), > - path_unstaged_content, label_orig, > - "unstaged", a->repo, a->progress_cb, > - a->progress_arg); > - if (err == NULL && > - path_new_staged_content == NULL) > - got_fileindex_entry_stage_set(ie, > - GOT_FILEIDX_STAGE_NONE); > + err = unstage_hunks(staged_blob_id, > + blob_base, blob_id, ie, ondisk_path, > + label_orig, a->worktree, a->repo, > + a->patch_cb, a->patch_arg, > + a->progress_cb, a->progress_arg); > break; /* Done with this file. */ > } > } > @@ -6653,11 +7550,43 @@ unstage_path(void *arg, unsigned char status, > staged_blob_id, 8192); > if (err) > break; > - err = merge_blob(&local_changes_subsumed, a->worktree, > - blob_base, ondisk_path, relpath, > - got_fileindex_perms_to_st(ie), label_orig, blob_staged, > - commit_id ? commit_id : a->worktree->base_commit_id, > - a->repo, a->progress_cb, a->progress_arg); > + switch (got_fileindex_entry_staged_filetype_get(ie)) { > + case GOT_FILEIDX_MODE_BAD_SYMLINK: > + case GOT_FILEIDX_MODE_REGULAR_FILE: > + err = merge_blob(&local_changes_subsumed, a->worktree, > + blob_base, ondisk_path, relpath, > + got_fileindex_perms_to_st(ie), label_orig, > + blob_staged, commit_id ? commit_id : > + a->worktree->base_commit_id, a->repo, > + a->progress_cb, a->progress_arg); > + break; > + case GOT_FILEIDX_MODE_SYMLINK: > + if (S_ISLNK(got_fileindex_perms_to_st(ie))) { > + char *staged_target; > + err = got_object_blob_read_to_str( > + &staged_target, blob_staged); > + if (err) > + goto done; > + err = merge_symlink(a->worktree, blob_base, > + ondisk_path, relpath, label_orig, > + staged_target, commit_id ? commit_id : > + a->worktree->base_commit_id, > + a->repo, a->progress_cb, a->progress_arg); > + free(staged_target); > + } else { > + err = merge_blob(&local_changes_subsumed, > + a->worktree, blob_base, ondisk_path, > + relpath, got_fileindex_perms_to_st(ie), > + label_orig, blob_staged, > + commit_id ? commit_id : > + a->worktree->base_commit_id, a->repo, > + a->progress_cb, a->progress_arg); > + } > + break; > + default: > + err = got_error_path(relpath, GOT_ERR_BAD_FILETYPE); > + break; > + } > if (err == NULL) > got_fileindex_entry_stage_set(ie, > GOT_FILEIDX_STAGE_NONE); > @@ -6686,14 +7615,6 @@ unstage_path(void *arg, unsigned char status, > } > done: > free(ondisk_path); > - if (path_unstaged_content && > - unlink(path_unstaged_content) == -1 && err == NULL) > - err = got_error_from_errno2("unlink", path_unstaged_content); > - if (path_new_staged_content && > - unlink(path_new_staged_content) == -1 && err == NULL) > - err = got_error_from_errno2("unlink", path_new_staged_content); > - free(path_unstaged_content); > - free(path_new_staged_content); > if (blob_base) > got_object_blob_close(blob_base); > if (blob_staged) > blob - 030ece4e122124a89fa7523bf23c419719dcb231 > blob + 789676b33649267773f4b3b3f09800cc68d7bb1b > --- regress/cmdline/add.sh > +++ regress/cmdline/add.sh > @@ -296,6 +296,71 @@ function test_add_clashes_with_submodule { > test_done "$testroot" "$ret" > } > > +function test_add_symlink { > + local testroot=`test_init add_symlink` > + > + got checkout $testroot/repo $testroot/wt > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && ln -s alpha alpha.link) > + (cd $testroot/wt && ln -s epsilon epsilon.link) > + (cd $testroot/wt && ln -s /etc/passwd passwd.link) > + (cd $testroot/wt && ln -s ../beta epsilon/beta.link) > + (cd $testroot/wt && ln -s nonexistent nonexistent.link) > + > + echo "A alpha.link" > $testroot/stdout.expected > + (cd $testroot/wt && got add alpha.link > $testroot/stdout) > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + echo "A epsilon.link" > $testroot/stdout.expected > + (cd $testroot/wt && got add epsilon.link > $testroot/stdout) > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + echo "A passwd.link" > $testroot/stdout.expected > + (cd $testroot/wt && got add passwd.link > $testroot/stdout) > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + echo "A epsilon/beta.link" > $testroot/stdout.expected > + (cd $testroot/wt && got add epsilon/beta.link > $testroot/stdout) > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + echo "A nonexistent.link" > $testroot/stdout.expected > + (cd $testroot/wt && got add nonexistent.link > $testroot/stdout) > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + fi > + test_done "$testroot" "$ret" > +} > + > run_test test_add_basic > run_test test_double_add > run_test test_add_multiple > @@ -303,3 +368,4 @@ run_test test_add_file_in_new_subdir > run_test test_add_deleted > run_test test_add_directory > run_test test_add_clashes_with_submodule > +run_test test_add_symlink > blob - 9e0475af05cac63a02818ca77164c2355cf2428e > blob + 171849c46606421d957f9db7406193faf834319f > --- regress/cmdline/blame.sh > +++ regress/cmdline/blame.sh > @@ -762,6 +762,126 @@ function test_blame_submodule { > test_done "$testroot" "$ret" > } > > +function test_blame_symlink { > + local testroot=`test_init blame_symlink` > + local commit_id0=`git_show_head $testroot/repo` > + local short_commit0=`trim_obj_id 32 $commit_id0` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + > + local commit_id1=`git_show_head $testroot/repo` > + local short_commit1=`trim_obj_id 32 $commit_id1` > + local author_time=`git_show_author_time $testroot/repo` > + > + # got blame dereferences symlink to a regular file > + got blame -r $testroot/repo alpha.link > $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "blame command failed unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + d=`date -r $author_time +"%G-%m-%d"` > + echo "1) $short_commit0 $d $GOT_AUTHOR_8 alpha" \ > + > $testroot/stdout.expected > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" -a "$xfail" == "" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "1" > + return 1 > + fi > + > + # got blame dereferences symlink with relative path > + got blame -r $testroot/repo epsilon/beta.link > $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "blame command failed unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + d=`date -r $author_time +"%G-%m-%d"` > + echo "1) $short_commit0 $d $GOT_AUTHOR_8 beta" \ > + > $testroot/stdout.expected > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" -a "$xfail" == "" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "1" > + return 1 > + fi > + > + got blame -r $testroot/repo epsilon.link > $testroot/stdout \ > + 2> $testroot/stderr > + ret="$?" > + if [ "$ret" == "0" ]; then > + echo "blame command succeeded unexpectedly" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + # blame dereferences symlink to a directory > + echo "got: /epsilon: wrong type of object" > $testroot/stderr.expected > + cmp -s $testroot/stderr.expected $testroot/stderr > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stderr.expected $testroot/stderr > + test_done "$testroot" "1" > + return 1 > + fi > + > + # got blame fails if symlink target does not exist in repo > + got blame -r $testroot/repo passwd.link > $testroot/stdout \ > + 2> $testroot/stderr > + ret="$?" > + if [ "$ret" == "0" ]; then > + echo "blame command succeeded unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + echo "got: /etc/passwd: no such entry found in tree" \ > + > $testroot/stderr.expected > + cmp -s $testroot/stderr.expected $testroot/stderr > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stderr.expected $testroot/stderr > + test_done "$testroot" "1" > + return 1 > + fi > + > + got blame -r $testroot/repo nonexistent.link > $testroot/stdout \ > + 2> $testroot/stderr > + ret="$?" > + if [ "$ret" == "0" ]; then > + echo "blame command succeeded unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + echo "got: /nonexistent: no such entry found in tree" \ > + > $testroot/stderr.expected > + cmp -s $testroot/stderr.expected $testroot/stderr > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stderr.expected $testroot/stderr > + test_done "$testroot" "1" > + return 1 > + fi > + > + test_done "$testroot" "$ret" > +} > + > run_test test_blame_basic > run_test test_blame_tag > run_test test_blame_file_single_line > @@ -773,3 +893,4 @@ run_test test_blame_commit_subsumed > run_test test_blame_blame_h > run_test test_blame_added_on_branch > run_test test_blame_submodule > +run_test test_blame_symlink > blob - 3c88697b0b4d38195808b7f3667068dc92dc3b5f > blob + 57a5971b240539d837c4ce91d72ff249c5e70bee > --- regress/cmdline/cat.sh > +++ regress/cmdline/cat.sh > @@ -259,7 +259,84 @@ function test_cat_submodule_of_same_repo { > test_done "$testroot" "$ret" > } > > +function test_cat_symlink { > + local testroot=`test_init cat_symlink` > + local commit_id=`git_show_head $testroot/repo` > + local author_time=`git_show_author_time $testroot/repo` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + > + local alpha_link_id=`got tree -r $testroot/repo -i | grep 'alpha.link@ -> alpha$' | cut -d' ' -f 1` > + local epsilon_link_id=`got tree -r $testroot/repo -i | grep 'epsilon.link@ -> epsilon$' | cut -d' ' -f 1` > + local passwd_link_id=`got tree -r $testroot/repo -i | grep 'passwd.link@ -> /etc/passwd$' | cut -d' ' -f 1` > + local epsilon_beta_link_id=`got tree -r $testroot/repo -i epsilon | grep 'beta.link@ -> ../beta$' | cut -d' ' -f 1` > + local nonexistent_link_id=`got tree -r $testroot/repo -i | grep 'nonexistent.link@ -> nonexistent$' | cut -d' ' -f 1` > + > + # cat symlink to regular file > + echo -n "alpha" > $testroot/stdout.expected > + got cat -r $testroot/repo $alpha_link_id > $testroot/stdout > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + # cat symlink with relative path to regular file > + echo -n "../beta" > $testroot/stdout.expected > + got cat -r $testroot/repo $epsilon_beta_link_id > $testroot/stdout > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + # cat symlink to a tree > + echo -n "epsilon" > $testroot/stdout.expected > + got cat -r $testroot/repo $epsilon_link_id > $testroot/stdout > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + # cat symlink to paths which don't exist in repository > + echo -n "/etc/passwd" > $testroot/stdout.expected > + got cat -r $testroot/repo $passwd_link_id > $testroot/stdout > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + echo -n "nonexistent" > $testroot/stdout.expected > + got cat -r $testroot/repo $nonexistent_link_id > $testroot/stdout > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + test_done "$testroot" "$ret" > +} > + > run_test test_cat_basic > run_test test_cat_path > run_test test_cat_submodule > run_test test_cat_submodule_of_same_repo > +run_test test_cat_symlink > blob - abba81a65d3c964e736faa7f7c27c92fd429bb1f > blob + 2ca82b6e47ed2d8f5e0de764be0ee241eb36cba3 > --- regress/cmdline/checkout.sh > +++ regress/cmdline/checkout.sh > @@ -397,10 +397,10 @@ function test_checkout_into_nonempty_dir { > return 1 > fi > > - echo "U $testroot/wt/alpha" > $testroot/stdout.expected > - echo "U $testroot/wt/beta" >> $testroot/stdout.expected > - echo "U $testroot/wt/epsilon/zeta" >> $testroot/stdout.expected > - echo "U $testroot/wt/gamma/delta" >> $testroot/stdout.expected > + echo "? $testroot/wt/alpha" > $testroot/stdout.expected > + echo "? $testroot/wt/beta" >> $testroot/stdout.expected > + echo "? $testroot/wt/epsilon/zeta" >> $testroot/stdout.expected > + echo "? $testroot/wt/gamma/delta" >> $testroot/stdout.expected > echo "Now shut up and hack" >> $testroot/stdout.expected > > got checkout -E $testroot/repo $testroot/wt > $testroot/stdout > @@ -489,7 +489,7 @@ function test_checkout_into_nonempty_dir { > if [ "$ret" != "0" ]; then > diff -u $testroot/content.expected $testroot/content > test_done "$testroot" "$ret" > - return > + return 1 > fi > > echo 'M alpha' > $testroot/stdout.expected > @@ -503,6 +503,259 @@ function test_checkout_into_nonempty_dir { > test_done "$testroot" "$ret" > } > > +function test_checkout_symlink { > + local testroot=`test_init checkout_symlink` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s passwd.link passwd2.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && ln -s .got/foo dotgotfoo.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + > + got checkout $testroot/repo $testroot/wt > $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "got checkout failed unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + echo "A $testroot/wt/alpha" > $testroot/stdout.expected > + echo "A $testroot/wt/alpha.link" >> $testroot/stdout.expected > + echo "A $testroot/wt/beta" >> $testroot/stdout.expected > + echo "A $testroot/wt/dotgotfoo.link" >> $testroot/stdout.expected > + echo "A $testroot/wt/epsilon/beta.link" >> $testroot/stdout.expected > + echo "A $testroot/wt/epsilon/zeta" >> $testroot/stdout.expected > + echo "A $testroot/wt/epsilon.link" >> $testroot/stdout.expected > + echo "A $testroot/wt/gamma/delta" >> $testroot/stdout.expected > + echo "A $testroot/wt/nonexistent.link" >> $testroot/stdout.expected > + echo "A $testroot/wt/passwd.link" >> $testroot/stdout.expected > + echo "A $testroot/wt/passwd2.link" >> $testroot/stdout.expected > + echo "Now shut up and hack" >> $testroot/stdout.expected > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/alpha.link ]; then > + echo "alpha.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/alpha.link > $testroot/stdout > + echo "alpha" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/epsilon.link ]; then > + echo "epsilon.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon.link > $testroot/stdout > + echo "epsilon" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/passwd.link ]; then > + echo -n "passwd.link symlink points outside of work tree: " >&2 > + readlink $testroot/wt/passwd.link >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo -n "/etc/passwd" > $testroot/content.expected > + cp $testroot/wt/passwd.link $testroot/content > + > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/passwd2.link ]; then > + echo "passwd2.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/passwd2.link > $testroot/stdout > + echo "passwd.link" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon/beta.link > $testroot/stdout > + echo "../beta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + readlink $testroot/wt/nonexistent.link > $testroot/stdout > + echo "nonexistent" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/dotgotfoo.link ]; then > + echo -n "dotgotfoo.link symlink points into .got dir: " >&2 > + readlink $testroot/wt/dotgotfoo.link >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo -n ".got/foo" > $testroot/content.expected > + cp $testroot/wt/dotgotfoo.link $testroot/content > + > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + fi > + test_done "$testroot" "$ret" > +} > + > +function test_checkout_symlink_relative_wtpath { > + local testroot=`test_init checkout_symlink_with_wtpath` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && ln -s .got/foo dotgotfoo.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + > + (cd $testroot && got checkout $testroot/repo wt > /dev/null) > + ret="$?" > + if [ "$ret" != "0" ]; then > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/alpha.link ]; then > + echo "alpha.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/alpha.link > $testroot/stdout > + echo "alpha" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/epsilon.link ]; then > + echo "epsilon.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon.link > $testroot/stdout > + echo "epsilon" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/passwd.link ]; then > + echo -n "passwd.link symlink points outside of work tree: " >&2 > + readlink $testroot/wt/passwd.link >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo -n "/etc/passwd" > $testroot/content.expected > + cp $testroot/wt/passwd.link $testroot/content > + > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon/beta.link > $testroot/stdout > + echo "../beta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + readlink $testroot/wt/nonexistent.link > $testroot/stdout > + echo "nonexistent" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/dotgotfoo.link ]; then > + echo -n "dotgotfoo.link symlink points into .got dir: " >&2 > + readlink $testroot/wt/dotgotfoo.link >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo -n ".got/foo" > $testroot/content.expected > + cp $testroot/wt/dotgotfoo.link $testroot/content > + > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + fi > + test_done "$testroot" "$ret" > +} > + > run_test test_checkout_basic > run_test test_checkout_dir_exists > run_test test_checkout_dir_not_empty > @@ -512,3 +765,5 @@ run_test test_checkout_tag > run_test test_checkout_ignores_submodules > run_test test_checkout_read_only > run_test test_checkout_into_nonempty_dir > +run_test test_checkout_symlink > +run_test test_checkout_symlink_relative_wtpath > blob - 15366064a879e3cb8137061a6408c3504af03578 > blob + cb7dd89554f0b55182721b03135d3a63add638f9 > --- regress/cmdline/cherrypick.sh > +++ regress/cmdline/cherrypick.sh > @@ -345,9 +345,398 @@ function test_cherrypick_conflict_wt_file_vs_repo_subm > test_done "$testroot" "$ret" > } > > +function test_cherrypick_modified_symlinks { > + local testroot=`test_init cherrypick_modified_symlinks` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + local commit_id1=`git_show_head $testroot/repo` > + > + got branch -r $testroot/repo foo > + > + got checkout -b foo $testroot/repo $testroot/wt > /dev/null > + > + (cd $testroot/repo && ln -sf beta alpha.link) > + (cd $testroot/repo && ln -sfh gamma epsilon.link) > + (cd $testroot/repo && ln -sf ../gamma/delta epsilon/beta.link) > + (cd $testroot/repo && ln -sf .got/bar $testroot/repo/dotgotfoo.link) > + (cd $testroot/repo && git rm -q nonexistent.link) > + (cd $testroot/repo && ln -sf epsilon/zeta zeta.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "change symlinks" > + local commit_id2=`git_show_head $testroot/repo` > + > + (cd $testroot/wt && got cherrypick $commit_id2 > $testroot/stdout) > + > + echo "G alpha.link" > $testroot/stdout.expected > + echo "G epsilon/beta.link" >> $testroot/stdout.expected > + echo "A dotgotfoo.link" >> $testroot/stdout.expected > + echo "G epsilon.link" >> $testroot/stdout.expected > + echo "D nonexistent.link" >> $testroot/stdout.expected > + echo "A zeta.link" >> $testroot/stdout.expected > + echo "Merged commit $commit_id2" >> $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/alpha.link ]; then > + echo "alpha.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/alpha.link > $testroot/stdout > + echo "beta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/epsilon.link ]; then > + echo "epsilon.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon.link > $testroot/stdout > + echo "gamma" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/passwd.link ]; then > + echo -n "passwd.link symlink points outside of work tree: " >&2 > + readlink $testroot/wt/passwd.link >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo -n "/etc/passwd" > $testroot/content.expected > + cp $testroot/wt/passwd.link $testroot/content > + > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon/beta.link > $testroot/stdout > + echo "../gamma/delta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/nonexistent.link ]; then > + echo -n "nonexistent.link still exists on disk: " >&2 > + readlink $testroot/wt/nonexistent.link >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + test_done "$testroot" "0" > +} > + > +function test_cherrypick_symlink_conflicts { > + local testroot=`test_init cherrypick_symlink_conflicts` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && ln -sf epsilon/zeta zeta.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + local commit_id1=`git_show_head $testroot/repo` > + > + (cd $testroot/repo && ln -sf beta alpha.link) > + (cd $testroot/repo && ln -sf beta boo.link) > + (cd $testroot/repo && ln -sfh gamma epsilon.link) > + (cd $testroot/repo && ln -sf ../gamma/delta epsilon/beta.link) > + echo 'this is regular file foo' > $testroot/repo/dotgotfoo.link > + (cd $testroot/repo && ln -sf .got/bar dotgotbar.link) > + (cd $testroot/repo && git rm -q nonexistent.link) > + (cd $testroot/repo && ln -sf gamma/delta zeta.link) > + (cd $testroot/repo && ln -sf alpha new.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "change symlinks" > + local commit_id2=`git_show_head $testroot/repo` > + > + got branch -r $testroot/repo -c $commit_id1 foo > + got checkout -b foo $testroot/repo $testroot/wt > /dev/null > + > + # modified symlink to file A vs modified symlink to file B > + (cd $testroot/wt && ln -sf gamma/delta alpha.link) > + # modified symlink to dir A vs modified symlink to file B > + (cd $testroot/wt && ln -sfh beta epsilon.link) > + # modeified symlink to file A vs modified symlink to dir B > + (cd $testroot/wt && ln -sfh ../gamma epsilon/beta.link) > + # added regular file A vs added bad symlink to file A > + (cd $testroot/wt && ln -sf .got/bar dotgotfoo.link) > + (cd $testroot/wt && got add dotgotfoo.link > /dev/null) > + # added bad symlink to file A vs added regular file A > + echo 'this is regular file bar' > $testroot/wt/dotgotbar.link > + (cd $testroot/wt && got add dotgotbar.link > /dev/null) > + # added symlink to file A vs unversioned file A > + echo 'this is unversioned file boo' > $testroot/wt/boo.link > + # removed symlink to non-existent file A vs modified symlink > + # to nonexistent file B > + (cd $testroot/wt && ln -sf nonexistent2 nonexistent.link) > + # modified symlink to file A vs removed symlink to file A > + (cd $testroot/wt && got rm zeta.link > /dev/null) > + # added symlink to file A vs added symlink to file B > + (cd $testroot/wt && ln -sf beta new.link) > + (cd $testroot/wt && got add new.link > /dev/null) > + (cd $testroot/wt && got commit -S -m "change symlinks on foo" \ > + > /dev/null) > + > + (cd $testroot/wt && got update >/dev/null) > + (cd $testroot/wt && got cherrypick $commit_id2 > $testroot/stdout) > + > + echo -n > $testroot/stdout.expected > + echo "C alpha.link" >> $testroot/stdout.expected > + echo "C epsilon/beta.link" >> $testroot/stdout.expected > + echo "? boo.link" >> $testroot/stdout.expected > + echo "C epsilon.link" >> $testroot/stdout.expected > + echo "C dotgotbar.link" >> $testroot/stdout.expected > + echo "C dotgotfoo.link" >> $testroot/stdout.expected > + echo "D nonexistent.link" >> $testroot/stdout.expected > + echo "! zeta.link" >> $testroot/stdout.expected > + echo "C new.link" >> $testroot/stdout.expected > + echo "Merged commit $commit_id2" >> $testroot/stdout.expected > + echo "Files with new merge conflicts: 6" >> $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/alpha.link ]; then > + echo "alpha.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "<<<<<<< merged change: commit $commit_id2" \ > + > $testroot/content.expected > + echo "beta" >> $testroot/content.expected > + echo "3-way merge base: commit $commit_id1" \ > + >> $testroot/content.expected > + echo "alpha" >> $testroot/content.expected > + echo "=======" >> $testroot/content.expected > + echo "gamma/delta" >> $testroot/content.expected > + echo '>>>>>>>' >> $testroot/content.expected > + echo -n "" >> $testroot/content.expected > + > + cp $testroot/wt/alpha.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/boo.link ]; then > + echo "boo.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "this is unversioned file boo" > $testroot/content.expected > + cp $testroot/wt/boo.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/epsilon.link ]; then > + echo "epsilon.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "<<<<<<< merged change: commit $commit_id2" \ > + > $testroot/content.expected > + echo "gamma" >> $testroot/content.expected > + echo "3-way merge base: commit $commit_id1" \ > + >> $testroot/content.expected > + echo "epsilon" >> $testroot/content.expected > + echo "=======" >> $testroot/content.expected > + echo "beta" >> $testroot/content.expected > + echo '>>>>>>>' >> $testroot/content.expected > + echo -n "" >> $testroot/content.expected > + > + cp $testroot/wt/epsilon.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/passwd.link ]; then > + echo -n "passwd.link symlink points outside of work tree: " >&2 > + readlink $testroot/wt/passwd.link >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo -n "/etc/passwd" > $testroot/content.expected > + cp $testroot/wt/passwd.link $testroot/content > + > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/epsilon/beta.link ]; then > + echo "epsilon/beta.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "<<<<<<< merged change: commit $commit_id2" \ > + > $testroot/content.expected > + echo "../gamma/delta" >> $testroot/content.expected > + echo "3-way merge base: commit $commit_id1" \ > + >> $testroot/content.expected > + echo "../beta" >> $testroot/content.expected > + echo "=======" >> $testroot/content.expected > + echo "../gamma" >> $testroot/content.expected > + echo '>>>>>>>' >> $testroot/content.expected > + echo -n "" >> $testroot/content.expected > + > + cp $testroot/wt/epsilon/beta.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/nonexistent.link ]; then > + echo -n "nonexistent.link still exists on disk: " >&2 > + readlink $testroot/wt/nonexistent.link >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + if [ -h $testroot/wt/dotgotfoo.link ]; then > + echo "dotgotfoo.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "<<<<<<< merged change: commit $commit_id2" \ > + > $testroot/content.expected > + echo "this is regular file foo" >> $testroot/content.expected > + echo "=======" >> $testroot/content.expected > + echo -n ".got/bar" >> $testroot/content.expected > + echo '>>>>>>>' >> $testroot/content.expected > + cp $testroot/wt/dotgotfoo.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/dotgotbar.link ]; then > + echo "dotgotbar.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + echo "<<<<<<< merged change: commit $commit_id2" \ > + > $testroot/content.expected > + echo -n ".got/bar" >> $testroot/content.expected > + echo "=======" >> $testroot/content.expected > + echo "this is regular file bar" >> $testroot/content.expected > + echo '>>>>>>>' >> $testroot/content.expected > + echo -n "" >> $testroot/content.expected > + cp $testroot/wt/dotgotbar.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/new.link ]; then > + echo "new.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "<<<<<<< merged change: commit $commit_id2" \ > + > $testroot/content.expected > + echo "alpha" >> $testroot/content.expected > + echo "=======" >> $testroot/content.expected > + echo "beta" >> $testroot/content.expected > + echo '>>>>>>>' >> $testroot/content.expected > + echo -n "" >> $testroot/content.expected > + > + cp $testroot/wt/new.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + echo "A dotgotfoo.link" > $testroot/stdout.expected > + echo "M new.link" >> $testroot/stdout.expected > + echo "D nonexistent.link" >> $testroot/stdout.expected > + (cd $testroot/wt && got status > $testroot/stdout) > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + test_done "$testroot" "0" > +} > + > run_test test_cherrypick_basic > run_test test_cherrypick_root_commit > run_test test_cherrypick_into_work_tree_with_conflicts > run_test test_cherrypick_modified_submodule > run_test test_cherrypick_added_submodule > run_test test_cherrypick_conflict_wt_file_vs_repo_submodule > +run_test test_cherrypick_modified_symlinks > +run_test test_cherrypick_symlink_conflicts > blob - 190d9e7cedd9ad2b17f54f971ebcf4cc3acd7c3f > blob + b877e38e53b5003239de90bd1bd31223db2fd177 > --- regress/cmdline/commit.sh > +++ regress/cmdline/commit.sh > @@ -902,6 +902,334 @@ function test_commit_with_unrelated_submodule { > test_done "$testroot" "$ret" > } > > +function check_symlinks { > + local wtpath="$1" > + if ! [ -h $wtpath/alpha.link ]; then > + echo "alpha.link is not a symlink" > + return 1 > + fi > + > + readlink $wtpath/alpha.link > $testroot/stdout > + echo "alpha" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + return 1 > + fi > + > + if ! [ -h $wtpath/epsilon.link ]; then > + echo "epsilon.link is not a symlink" > + return 1 > + fi > + > + readlink $wtpath/epsilon.link > $testroot/stdout > + echo "epsilon" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + return 1 > + fi > + > + if [ -h $wtpath/passwd.link ]; then > + echo -n "passwd.link is a symlink and points outside of work tree: " >&2 > + readlink $wtpath/passwd.link >&2 > + return 1 > + fi > + > + echo -n "/etc/passwd" > $testroot/content.expected > + cp $wtpath/passwd.link $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "cp command failed unexpectedly" >&2 > + return 1 > + fi > + > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + return 1 > + fi > + > + readlink $wtpath/epsilon/beta.link > $testroot/stdout > + echo "../beta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + return 1 > + fi > + > + readlink $wtpath/nonexistent.link > $testroot/stdout > + echo "nonexistent" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + return 1 > + fi > + > + return 0 > +} > + > +function test_commit_symlink { > + local testroot=`test_init commit_symlink` > + > + got checkout $testroot/repo $testroot/wt > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && ln -s alpha alpha.link) > + (cd $testroot/wt && ln -s epsilon epsilon.link) > + (cd $testroot/wt && ln -s /etc/passwd passwd.link) > + (cd $testroot/wt && ln -s ../beta epsilon/beta.link) > + (cd $testroot/wt && ln -s nonexistent nonexistent.link) > + (cd $testroot/wt && got add alpha.link epsilon.link passwd.link \ > + epsilon/beta.link nonexistent.link > /dev/null) > + > + (cd $testroot/wt && got commit -m 'test commit_symlink' \ > + > $testroot/stdout 2> $testroot/stderr) > + ret="$?" > + if [ "$ret" == "0" ]; then > + echo "got commit succeeded unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + echo -n "got: $testroot/wt/passwd.link: " > $testroot/stderr.expected > + echo "symbolic link points outside of paths under version control" \ > + >> $testroot/stderr.expected > + cmp -s $testroot/stderr.expected $testroot/stderr > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stderr.expected $testroot/stderr > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && got commit -S -m 'test commit_symlink' \ > + > $testroot/stdout) > + > + local head_rev=`git_show_head $testroot/repo` > + echo "A alpha.link" > $testroot/stdout.expected > + echo "A epsilon.link" >> $testroot/stdout.expected > + echo "A nonexistent.link" >> $testroot/stdout.expected > + echo "A passwd.link" >> $testroot/stdout.expected > + echo "A epsilon/beta.link" >> $testroot/stdout.expected > + echo "Created commit $head_rev" >> $testroot/stdout.expected > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + # verify created in-repository tree > + got checkout $testroot/repo $testroot/wt2 > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + test_done "$testroot" "$ret" > + return 1 > + fi > + check_symlinks $testroot/wt2 > + ret="$?" > + if [ "$ret" != "0" ]; then > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/passwd.link ]; then > + echo 'passwd.link is not a symlink' >&2 > + test_done "$testroot" 1 > + return 1 > + fi > + > + # 'got update' should reinstall passwd.link as a regular file > + (cd $testroot/wt && got update > /dev/null) > + check_symlinks $testroot/wt > + ret="$?" > + if [ "$ret" != "0" ]; then > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && ln -sf beta alpha.link) > + (cd $testroot/wt && ln -sfh gamma epsilon.link) > + rm $testroot/wt/epsilon/beta.link > + echo "this is a regular file" > $testroot/wt/epsilon/beta.link > + (cd $testroot/wt && ln -sf .got/bar dotgotbar.link) > + (cd $testroot/wt && got add dotgotbar.link > /dev/null) > + (cd $testroot/wt && got rm nonexistent.link > /dev/null) > + (cd $testroot/wt && ln -sf gamma/delta zeta.link) > + (cd $testroot/wt && ln -sf alpha new.link) > + (cd $testroot/wt && got add new.link > /dev/null) > + > + (cd $testroot/wt && got commit -m 'test commit_symlink' \ > + > $testroot/stdout 2> $testroot/stderr) > + ret="$?" > + if [ "$ret" == "0" ]; then > + echo "got commit succeeded unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + echo -n "got: $testroot/wt/dotgotbar.link: " > $testroot/stderr.expected > + echo "symbolic link points outside of paths under version control" \ > + >> $testroot/stderr.expected > + cmp -s $testroot/stderr.expected $testroot/stderr > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stderr.expected $testroot/stderr > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && got commit -S -m 'test commit_symlink' \ > + > $testroot/stdout) > + > + local head_rev=`git_show_head $testroot/repo` > + echo "A dotgotbar.link" > $testroot/stdout.expected > + echo "A new.link" >> $testroot/stdout.expected > + echo "M alpha.link" >> $testroot/stdout.expected > + echo "M epsilon/beta.link" >> $testroot/stdout.expected > + echo "M epsilon.link" >> $testroot/stdout.expected > + echo "D nonexistent.link" >> $testroot/stdout.expected > + echo "Created commit $head_rev" >> $testroot/stdout.expected > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + got tree -r $testroot/repo -c $head_rev -R > $testroot/stdout > + cat > $testroot/stdout.expected < +alpha > +alpha.link@ -> beta > +beta > +dotgotbar.link@ -> .got/bar > +epsilon/ > +epsilon/beta.link > +epsilon/zeta > +epsilon.link@ -> gamma > +gamma/ > +gamma/delta > +new.link@ -> alpha > +passwd.link@ -> /etc/passwd > +EOF > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + fi > + test_done "$testroot" "$ret" > +} > + > +function test_commit_fix_bad_symlink { > + local testroot=`test_init commit_fix_bad_symlink` > + > + got checkout $testroot/repo $testroot/wt > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "got checkout failed unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && ln -s /etc/passwd passwd.link) > + (cd $testroot/wt && got add passwd.link > /dev/null) > + > + (cd $testroot/wt && got commit -S -m 'commit bad symlink' \ > + > $testroot/stdout) > + > + if ! [ -h $testroot/wt/passwd.link ]; then > + echo 'passwd.link is not a symlink' >&2 > + test_done "$testroot" 1 > + return 1 > + fi > + (cd $testroot/wt && got update >/dev/null) > + if [ -h $testroot/wt/passwd.link ]; then > + echo "passwd.link is a symlink but should be a regular file" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + # create another work tree which will contain the "bad" symlink > + got checkout $testroot/repo $testroot/wt2 > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "got checkout failed unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + # change "bad" symlink back into a "good" symlink > + (cd $testroot/wt && ln -sfh alpha passwd.link) > + > + (cd $testroot/wt && got commit -m 'fix bad symlink' \ > + > $testroot/stdout) > + > + local head_rev=`git_show_head $testroot/repo` > + echo "M passwd.link" > $testroot/stdout.expected > + echo "Created commit $head_rev" >> $testroot/stdout.expected > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/passwd.link ]; then > + echo 'passwd.link is not a symlink' >&2 > + test_done "$testroot" 1 > + return 1 > + fi > + > + readlink $testroot/wt/passwd.link > $testroot/stdout > + echo "alpha" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + return 1 > + fi > + > + # Update the other work tree; the bad symlink should be fixed > + (cd $testroot/wt2 && got update > /dev/null) > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "got checkout failed unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt2/passwd.link ]; then > + echo 'passwd.link is not a symlink' >&2 > + test_done "$testroot" 1 > + return 1 > + fi > + > + readlink $testroot/wt2/passwd.link > $testroot/stdout > + echo "alpha" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + return 1 > + fi > + > + test_done "$testroot" "0" > +} > + > run_test test_commit_basic > run_test test_commit_new_subdir > run_test test_commit_subdir > @@ -922,3 +1250,5 @@ run_test test_commit_gitconfig_author > run_test test_commit_xbit_change > run_test test_commit_normalizes_filemodes > run_test test_commit_with_unrelated_submodule > +run_test test_commit_symlink > +run_test test_commit_fix_bad_symlink > blob - 5104375e287f0e845051a912863028d3e6a4d86c > blob + bc744fcb7fa87573ecc329d19d9b5140d79d7444 > --- regress/cmdline/common.sh > +++ regress/cmdline/common.sh > @@ -156,7 +156,8 @@ function get_blob_id > tree_path="$2" > filename="$3" > > - got tree -r $repo -i $tree_path | grep ${filename}$ | cut -d' ' -f 1 > + got tree -r $repo -i $tree_path | grep "[0-9a-f] ${filename}$" | \ > + cut -d' ' -f 1 > } > > function test_init > blob - a3f99c8779d3bd933ae0f0f14741563fad6794a7 > blob + dbc39580319a7e28e9adc23ded3e53d643d3d6e0 > --- regress/cmdline/diff.sh > +++ regress/cmdline/diff.sh > @@ -362,9 +362,232 @@ function test_diff_submodule_of_same_repo { > test_done "$testroot" "$ret" > } > > +function test_diff_symlinks_in_work_tree { > + local testroot=`test_init diff_symlinks_in_work_tree` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && ln -s .got/foo dotgotfoo.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + local commit_id1=`git_show_head $testroot/repo` > + > + got checkout $testroot/repo $testroot/wt > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && ln -sf beta alpha.link) > + (cd $testroot/wt && ln -sfh gamma epsilon.link) > + (cd $testroot/wt && ln -sf ../gamma/delta epsilon/beta.link) > + echo -n '.got/bar' > $testroot/wt/dotgotfoo.link > + (cd $testroot/wt && got rm nonexistent.link > /dev/null) > + (cd $testroot/wt && ln -sf epsilon/zeta zeta.link) > + (cd $testroot/wt && got add zeta.link > /dev/null) > + (cd $testroot/wt && got diff > $testroot/stdout) > + > + echo "diff $commit_id1 $testroot/wt" > $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id1 -i | \ > + grep 'alpha.link@ -> alpha$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo 'file + alpha.link' >> $testroot/stdout.expected > + echo '--- alpha.link' >> $testroot/stdout.expected > + echo '+++ alpha.link' >> $testroot/stdout.expected > + echo '@@ -1 +1 @@' >> $testroot/stdout.expected > + echo '-alpha' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo '+beta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id1 -i | \ > + grep 'dotgotfoo.link@ -> .got/foo$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo 'file + dotgotfoo.link' >> $testroot/stdout.expected > + echo '--- dotgotfoo.link' >> $testroot/stdout.expected > + echo '+++ dotgotfoo.link' >> $testroot/stdout.expected > + echo '@@ -1 +1 @@' >> $testroot/stdout.expected > + echo '-.got/foo' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo '+.got/bar' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id1 -i epsilon | \ > + grep 'beta.link@ -> ../beta$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo 'file + epsilon/beta.link' >> $testroot/stdout.expected > + echo '--- epsilon/beta.link' >> $testroot/stdout.expected > + echo '+++ epsilon/beta.link' >> $testroot/stdout.expected > + echo '@@ -1 +1 @@' >> $testroot/stdout.expected > + echo '-../beta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo '+../gamma/delta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id1 -i | \ > + grep 'epsilon.link@ -> epsilon$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo 'file + epsilon.link' >> $testroot/stdout.expected > + echo '--- epsilon.link' >> $testroot/stdout.expected > + echo '+++ epsilon.link' >> $testroot/stdout.expected > + echo '@@ -1 +1 @@' >> $testroot/stdout.expected > + echo '-epsilon' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo '+gamma' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id1 -i | \ > + grep 'nonexistent.link@ -> nonexistent$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo 'file + /dev/null' >> $testroot/stdout.expected > + echo '--- nonexistent.link' >> $testroot/stdout.expected > + echo '+++ nonexistent.link' >> $testroot/stdout.expected > + echo '@@ -1 +0,0 @@' >> $testroot/stdout.expected > + echo '-nonexistent' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo 'blob - /dev/null' >> $testroot/stdout.expected > + echo 'file + zeta.link' >> $testroot/stdout.expected > + echo '--- zeta.link' >> $testroot/stdout.expected > + echo '+++ zeta.link' >> $testroot/stdout.expected > + echo '@@ -0,0 +1 @@' >> $testroot/stdout.expected > + echo '+epsilon/zeta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + fi > + test_done "$testroot" "$ret" > +} > + > +function test_diff_symlinks_in_repo { > + local testroot=`test_init diff_symlinks_in_repo` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && ln -s .got/foo dotgotfoo.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + local commit_id1=`git_show_head $testroot/repo` > + > + (cd $testroot/repo && ln -sf beta alpha.link) > + (cd $testroot/repo && ln -sfh gamma epsilon.link) > + (cd $testroot/repo && ln -sf ../gamma/delta epsilon/beta.link) > + (cd $testroot/repo && ln -sf .got/bar $testroot/repo/dotgotfoo.link) > + (cd $testroot/repo && git rm -q nonexistent.link) > + (cd $testroot/repo && ln -sf epsilon/zeta zeta.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "change symlinks" > + local commit_id2=`git_show_head $testroot/repo` > + > + got diff -r $testroot/repo $commit_id1 $commit_id2 > $testroot/stdout > + > + echo "diff $commit_id1 $commit_id2" > $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id1 -i | \ > + grep 'alpha.link@ -> alpha$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id2 -i | \ > + grep 'alpha.link@ -> beta$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo '--- alpha.link' >> $testroot/stdout.expected > + echo '+++ alpha.link' >> $testroot/stdout.expected > + echo '@@ -1 +1 @@' >> $testroot/stdout.expected > + echo '-alpha' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo '+beta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id1 -i | \ > + grep 'dotgotfoo.link@ -> .got/foo$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id2 -i | \ > + grep 'dotgotfoo.link@ -> .got/bar$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo '--- dotgotfoo.link' >> $testroot/stdout.expected > + echo '+++ dotgotfoo.link' >> $testroot/stdout.expected > + echo '@@ -1 +1 @@' >> $testroot/stdout.expected > + echo '-.got/foo' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo '+.got/bar' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id1 -i epsilon | \ > + grep 'beta.link@ -> ../beta$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id2 -i epsilon | \ > + grep 'beta.link@ -> ../gamma/delta$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo '--- epsilon/beta.link' >> $testroot/stdout.expected > + echo '+++ epsilon/beta.link' >> $testroot/stdout.expected > + echo '@@ -1 +1 @@' >> $testroot/stdout.expected > + echo '-../beta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo '+../gamma/delta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id1 -i | \ > + grep 'epsilon.link@ -> epsilon$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id2 -i | \ > + grep 'epsilon.link@ -> gamma$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo '--- epsilon.link' >> $testroot/stdout.expected > + echo '+++ epsilon.link' >> $testroot/stdout.expected > + echo '@@ -1 +1 @@' >> $testroot/stdout.expected > + echo '-epsilon' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo '+gamma' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id1 -i | \ > + grep 'nonexistent.link@ -> nonexistent$' | \ > + cut -d' ' -f 1 | sed -e 's/$/ (mode 120000)/' \ > + >> $testroot/stdout.expected > + echo 'blob + /dev/null' >> $testroot/stdout.expected > + echo '--- nonexistent.link' >> $testroot/stdout.expected > + echo '+++ /dev/null' >> $testroot/stdout.expected > + echo '@@ -1 +0,0 @@' >> $testroot/stdout.expected > + echo '-nonexistent' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo 'blob - /dev/null' >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -c $commit_id2 -i | \ > + grep 'zeta.link@ -> epsilon/zeta$' | \ > + cut -d' ' -f 1 | sed -e 's/$/ (mode 120000)/' \ > + >> $testroot/stdout.expected > + echo '--- /dev/null' >> $testroot/stdout.expected > + echo '+++ zeta.link' >> $testroot/stdout.expected > + echo '@@ -0,0 +1 @@' >> $testroot/stdout.expected > + echo '+epsilon/zeta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + fi > + test_done "$testroot" "$ret" > +} > + > run_test test_diff_basic > run_test test_diff_shows_conflict > run_test test_diff_tag > run_test test_diff_lightweight_tag > run_test test_diff_ignore_whitespace > run_test test_diff_submodule_of_same_repo > +run_test test_diff_symlinks_in_work_tree > +run_test test_diff_symlinks_in_repo > blob - a5da67ca967c91f73507031c7e592c80f772a371 > blob + 82df005b32ffe2f974cea0f4b8d19ac3638ce010 > --- regress/cmdline/import.sh > +++ regress/cmdline/import.sh > @@ -241,7 +241,58 @@ function test_import_empty_dir { > test_done "$testroot" "$ret" > } > > +function test_import_symlink { > + local testname=import_symlink > + local testroot=`mktemp -p /tmp -d got-test-$testname-XXXXXXXX` > + > + got init $testroot/repo > + > + mkdir $testroot/tree > + echo 'this is file alpha' > $testroot/tree/alpha > + ln -s alpha $testroot/tree/alpha.link > + > + got import -m 'init' -r $testroot/repo $testroot/tree \ > + > $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + local head_commit=`git_show_head $testroot/repo` > + echo "A $testroot/tree/alpha" > $testroot/stdout.expected > + echo "A $testroot/tree/alpha.link" >> $testroot/stdout.expected > + echo "Created branch refs/heads/main with commit $head_commit" \ > + >> $testroot/stdout.expected > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + id_alpha=`get_blob_id $testroot/repo "" alpha` > + id_alpha_link=$(got tree -r $testroot/repo -i | grep 'alpha.link@ -> alpha$' | cut -d' ' -f 1) > + tree_id=`(cd $testroot/repo && got cat $head_commit | \ > + grep ^tree | cut -d ' ' -f 2)` > + > + got tree -i -r $testroot/repo -c $head_commit > $testroot/stdout > + > + echo "$id_alpha alpha" > $testroot/stdout.expected > + echo "$id_alpha_link alpha.link@ -> alpha" >> $testroot/stdout.expected > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + fi > + test_done "$testroot" "$ret" > +} > + > run_test test_import_basic > run_test test_import_requires_new_branch > run_test test_import_ignores > run_test test_import_empty_dir > +run_test test_import_symlink > blob - 21b308c15e0b334333d5c674b394de80c25a671d > blob + 0ee9bb50011d761f484a2b1996a32492bebab27e > --- regress/cmdline/revert.sh > +++ regress/cmdline/revert.sh > @@ -1053,6 +1053,436 @@ function test_revert_deleted_subtree { > test_done "$testroot" "$ret" > } > > +function test_revert_symlink { > + local testroot=`test_init revert_symlink` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && ln -sf epsilon/zeta zeta.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + local commit_id1=`git_show_head $testroot/repo` > + > + got checkout $testroot/repo $testroot/wt > /dev/null > + > + # symlink to file A now points to file B > + (cd $testroot/wt && ln -sf gamma/delta alpha.link) > + # symlink to a directory A now points to file B > + (cd $testroot/wt && ln -sfh beta epsilon.link) > + # "bad" symlink now contains a different target path > + echo "foo" > $testroot/wt/passwd.link > + # relative symlink to directory A now points to relative directory B > + (cd $testroot/wt && ln -sfh ../gamma epsilon/beta.link) > + # an unversioned symlink > + (cd $testroot/wt && ln -sf .got/foo dotgotfoo.link) > + # symlink to file A now points to non-existent file B > + (cd $testroot/wt && ln -sf nonexistent2 nonexistent.link) > + # removed symlink > + (cd $testroot/wt && got rm zeta.link > /dev/null) > + # added symlink > + (cd $testroot/wt && ln -sf beta new.link) > + (cd $testroot/wt && got add new.link > /dev/null) > + > + (cd $testroot/wt && got revert alpha.link epsilon.link \ > + passwd.link epsilon/beta.link dotgotfoo.link \ > + nonexistent.link zeta.link new.link > $testroot/stdout) > + > + cat > $testroot/stdout.expected < +R alpha.link > +R epsilon.link > +R passwd.link > +R epsilon/beta.link > +R nonexistent.link > +R zeta.link > +R new.link > +EOF > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/alpha.link ]; then > + echo "alpha.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/alpha.link > $testroot/stdout > + echo "alpha" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/epsilon.link ]; then > + echo "epsilon.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon.link > $testroot/stdout > + echo "epsilon" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/passwd.link ]; then > + echo "passwd.link should not be a symlink" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo -n "/etc/passwd" > $testroot/content.expected > + cp $testroot/wt/passwd.link $testroot/content > + > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon/beta.link > $testroot/stdout > + echo "../beta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + readlink $testroot/wt/nonexistent.link > $testroot/stdout > + echo "nonexistent" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/dotgotfoo.link ]; then > + echo "dotgotfoo.link is not a symlink " >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + readlink $testroot/wt/dotgotfoo.link > $testroot/stdout > + echo ".got/foo" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/zeta.link ]; then > + echo -n "zeta.link is not a symlink" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/zeta.link > $testroot/stdout > + echo "epsilon/zeta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/new.link ]; then > + echo -n "new.link is not a symlink" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + (cd $testroot/wt && got status > $testroot/stdout) > + echo "? dotgotfoo.link" > $testroot/stdout.expected > + echo "? new.link" >> $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + return 1 > + fi > + test_done "$testroot" "$ret" > +} > + > +function test_revert_patch_symlink { > + local testroot=`test_init revert_patch_symlink` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && ln -sf epsilon/zeta zeta.link) > + (cd $testroot/repo && ln -sf epsilon/zeta zeta2.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + local commit_id1=`git_show_head $testroot/repo` > + > + got checkout $testroot/repo $testroot/wt > /dev/null > + > + # symlink to file A now points to file B > + (cd $testroot/wt && ln -sf gamma/delta alpha.link) > + # symlink to a directory A now points to file B > + (cd $testroot/wt && ln -sfh beta epsilon.link) > + # "bad" symlink now contains a different target path > + echo "foo" > $testroot/wt/passwd.link > + # relative symlink to directory A now points to relative directory B > + (cd $testroot/wt && ln -sfh ../gamma epsilon/beta.link) > + # an unversioned symlink > + (cd $testroot/wt && ln -sf .got/foo dotgotfoo.link) > + # symlink to file A now points to non-existent file B > + (cd $testroot/wt && ln -sf nonexistent2 nonexistent.link) > + # removed symlink > + (cd $testroot/wt && got rm zeta.link > /dev/null) > + (cd $testroot/wt && got rm zeta2.link > /dev/null) > + # added symlink > + (cd $testroot/wt && ln -sf beta new.link) > + (cd $testroot/wt && got add new.link > /dev/null) > + (cd $testroot/wt && ln -sf beta zeta3.link) > + (cd $testroot/wt && got add zeta3.link > /dev/null) > + > + printf "y\nn\ny\nn\ny\ny\nn\ny\ny\n" > $testroot/patchscript > + (cd $testroot/wt && got revert -F $testroot/patchscript -p -R . \ > + > $testroot/stdout) > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "got revert command failed unexpectedly" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + cat > $testroot/stdout.expected < +----------------------------------------------- > +@@ -1 +1 @@ > +-alpha > +\ No newline at end of file > ++gamma/delta > +\ No newline at end of file > +----------------------------------------------- > +M alpha.link (change 1 of 1) > +revert this change? [y/n/q] y > +R alpha.link > +----------------------------------------------- > +@@ -1 +1 @@ > +-../beta > +\ No newline at end of file > ++../gamma > +\ No newline at end of file > +----------------------------------------------- > +M epsilon/beta.link (change 1 of 1) > +revert this change? [y/n/q] n > +----------------------------------------------- > +@@ -1 +1 @@ > +-epsilon > +\ No newline at end of file > ++beta > +\ No newline at end of file > +----------------------------------------------- > +M epsilon.link (change 1 of 1) > +revert this change? [y/n/q] y > +R epsilon.link > +A new.link > +revert this addition? [y/n] n > +----------------------------------------------- > +@@ -1 +1 @@ > +-nonexistent > +\ No newline at end of file > ++nonexistent2 > +\ No newline at end of file > +----------------------------------------------- > +M nonexistent.link (change 1 of 1) > +revert this change? [y/n/q] y > +R nonexistent.link > +----------------------------------------------- > +@@ -1 +1 @@ > +-/etc/passwd > +\ No newline at end of file > ++foo > +----------------------------------------------- > +M passwd.link (change 1 of 1) > +revert this change? [y/n/q] y > +R passwd.link > +D zeta.link > +revert this deletion? [y/n] n > +D zeta2.link > +revert this deletion? [y/n] y > +R zeta2.link > +A zeta3.link > +revert this addition? [y/n] y > +R zeta3.link > +EOF > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/alpha.link ]; then > + echo "alpha.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/alpha.link > $testroot/stdout > + echo "alpha" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/epsilon.link ]; then > + echo "epsilon.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon.link > $testroot/stdout > + echo "epsilon" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/passwd.link ]; then > + echo "passwd.link should not be a symlink" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo -n "/etc/passwd" > $testroot/content.expected > + cp $testroot/wt/passwd.link $testroot/content > + > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon/beta.link > $testroot/stdout > + echo "../gamma" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + readlink $testroot/wt/nonexistent.link > $testroot/stdout > + echo "nonexistent" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/dotgotfoo.link ]; then > + echo "dotgotfoo.link is not a symlink " >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + readlink $testroot/wt/dotgotfoo.link > $testroot/stdout > + echo ".got/foo" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + > + if [ -e $testroot/wt/zeta.link ]; then > + echo -n "zeta.link should not exist on disk" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/zeta2.link ]; then > + echo -n "zeta2.link is not a symlink" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/zeta2.link > $testroot/stdout > + echo "epsilon/zeta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/zeta3.link ]; then > + echo -n "zeta3.link is not a symlink" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/zeta3.link > $testroot/stdout > + echo "beta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/new.link ]; then > + echo -n "new.link is not a symlink" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + (cd $testroot/wt && got status > $testroot/stdout) > + echo "? dotgotfoo.link" > $testroot/stdout.expected > + echo "M epsilon/beta.link" >> $testroot/stdout.expected > + echo "A new.link" >> $testroot/stdout.expected > + echo "D zeta.link" >> $testroot/stdout.expected > + echo "? zeta3.link" >> $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + return 1 > + fi > + test_done "$testroot" "$ret" > +} > + > run_test test_revert_basic > run_test test_revert_rm > run_test test_revert_add > @@ -1067,3 +1497,5 @@ run_test test_revert_patch_removed > run_test test_revert_patch_one_change > run_test test_revert_added_subtree > run_test test_revert_deleted_subtree > +run_test test_revert_symlink > +run_test test_revert_patch_symlink > blob - d96dfe3602ef3a7ee2a20198083c20e833f9567a > blob + 2ecf3848716083ed82686978dfb7da32ea4ea299 > --- regress/cmdline/rm.sh > +++ regress/cmdline/rm.sh > @@ -404,6 +404,40 @@ function test_rm_subtree { > test_done "$testroot" "$ret" > } > > +function test_rm_symlink { > + local testroot=`test_init rm_symlink` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + > + got checkout $testroot/repo $testroot/wt > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + echo 'D alpha.link' > $testroot/stdout.expected > + echo 'D epsilon.link' >> $testroot/stdout.expected > + echo 'D passwd.link' >> $testroot/stdout.expected > + echo 'D epsilon/beta.link' >> $testroot/stdout.expected > + echo 'D nonexistent.link' >> $testroot/stdout.expected > + (cd $testroot/wt && got rm alpha.link epsilon.link passwd.link \ > + epsilon/beta.link nonexistent.link > $testroot/stdout) > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + fi > + test_done "$testroot" "$ret" > +} > + > run_test test_rm_basic > run_test test_rm_with_local_mods > run_test test_double_rm > @@ -411,3 +445,4 @@ run_test test_rm_and_add_elsewhere > run_test test_rm_directory > run_test test_rm_directory_keep_files > run_test test_rm_subtree > +run_test test_rm_symlink > blob - a537bcbfccf7f2f23f1ffd8cf0407deef0991edd > blob + 21af58910aa46d20189bd009ad4c9a6bf02d4453 > --- regress/cmdline/stage.sh > +++ regress/cmdline/stage.sh > @@ -2353,6 +2353,611 @@ EOF > > } > > +function test_stage_symlink { > + local testroot=`test_init stage_symlink` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + local head_commit=`git_show_head $testroot/repo` > + > + got checkout $testroot/repo $testroot/wt > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && ln -sf beta alpha.link) > + (cd $testroot/wt && ln -sfh gamma epsilon.link) > + (cd $testroot/wt && ln -sf ../gamma/delta epsilon/beta.link) > + echo 'this is regular file foo' > $testroot/wt/dotgotfoo.link > + (cd $testroot/wt && got add dotgotfoo.link > /dev/null) > + (cd $testroot/wt && ln -sf .got/bar dotgotbar.link) > + (cd $testroot/wt && got add dotgotbar.link > /dev/null) > + (cd $testroot/wt && got rm nonexistent.link > /dev/null) > + (cd $testroot/wt && ln -sf gamma/delta zeta.link) > + (cd $testroot/wt && got add zeta.link > /dev/null) > + > + (cd $testroot/wt && got stage > $testroot/stdout 2> $testroot/stderr) > + ret="$?" > + if [ "$ret" == "0" ]; then > + echo "got stage succeeded unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + echo -n "got: $testroot/wt/dotgotbar.link: " > $testroot/stderr.expected > + echo "symbolic link points outside of paths under version control" \ > + >> $testroot/stderr.expected > + cmp -s $testroot/stderr.expected $testroot/stderr > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stderr.expected $testroot/stderr > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && got stage -S > $testroot/stdout) > + > + cat > $testroot/stdout.expected < + M alpha.link > + A dotgotbar.link > + A dotgotfoo.link > + M epsilon/beta.link > + M epsilon.link > + D nonexistent.link > + A zeta.link > +EOF > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + rm $testroot/wt/alpha.link > + echo 'this is regular file alpha.link' > $testroot/wt/alpha.link > + > + (cd $testroot/wt && got diff -s > $testroot/stdout) > + > + echo "diff $head_commit $testroot/wt (staged changes)" \ > + > $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -i | grep 'alpha.link@ -> alpha$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + (cd $testroot/wt && got stage -l alpha.link) | cut -d' ' -f 1 \ > + >> $testroot/stdout.expected > + echo '--- alpha.link' >> $testroot/stdout.expected > + echo '+++ alpha.link' >> $testroot/stdout.expected > + echo '@@ -1 +1 @@' >> $testroot/stdout.expected > + echo '-alpha' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo '+beta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo 'blob - /dev/null' >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + (cd $testroot/wt && got stage -l dotgotbar.link) | cut -d' ' -f 1 \ > + >> $testroot/stdout.expected > + echo '--- /dev/null' >> $testroot/stdout.expected > + echo '+++ dotgotbar.link' >> $testroot/stdout.expected > + echo '@@ -0,0 +1 @@' >> $testroot/stdout.expected > + echo '+.got/bar' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo 'blob - /dev/null' >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + (cd $testroot/wt && got stage -l dotgotfoo.link) | cut -d' ' -f 1 \ > + >> $testroot/stdout.expected > + echo '--- /dev/null' >> $testroot/stdout.expected > + echo '+++ dotgotfoo.link' >> $testroot/stdout.expected > + echo '@@ -0,0 +1 @@' >> $testroot/stdout.expected > + echo '+this is regular file foo' >> $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -i epsilon | grep 'beta.link@ -> ../beta$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + (cd $testroot/wt && got stage -l epsilon/beta.link) | cut -d' ' -f 1 \ > + >> $testroot/stdout.expected > + echo '--- epsilon/beta.link' >> $testroot/stdout.expected > + echo '+++ epsilon/beta.link' >> $testroot/stdout.expected > + echo '@@ -1 +1 @@' >> $testroot/stdout.expected > + echo '-../beta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo '+../gamma/delta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -i | grep 'epsilon.link@ -> epsilon$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + (cd $testroot/wt && got stage -l epsilon.link) | cut -d' ' -f 1 \ > + >> $testroot/stdout.expected > + echo '--- epsilon.link' >> $testroot/stdout.expected > + echo '+++ epsilon.link' >> $testroot/stdout.expected > + echo '@@ -1 +1 @@' >> $testroot/stdout.expected > + echo '-epsilon' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo '+gamma' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -i | grep 'nonexistent.link@ -> nonexistent$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo 'blob + /dev/null' >> $testroot/stdout.expected > + echo '--- nonexistent.link' >> $testroot/stdout.expected > + echo '+++ /dev/null' >> $testroot/stdout.expected > + echo '@@ -1 +0,0 @@' >> $testroot/stdout.expected > + echo '-nonexistent' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo 'blob - /dev/null' >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + (cd $testroot/wt && got stage -l zeta.link) | cut -d' ' -f 1 \ > + >> $testroot/stdout.expected > + echo '--- /dev/null' >> $testroot/stdout.expected > + echo '+++ zeta.link' >> $testroot/stdout.expected > + echo '@@ -0,0 +1 @@' >> $testroot/stdout.expected > + echo '+gamma/delta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && got commit -m "staged symlink" \ > + > $testroot/stdout) > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "got commit command failed unexpectedly" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + local commit_id=`git_show_head $testroot/repo` > + echo "A dotgotbar.link" > $testroot/stdout.expected > + echo "A dotgotfoo.link" >> $testroot/stdout.expected > + echo "A zeta.link" >> $testroot/stdout.expected > + echo "M alpha.link" >> $testroot/stdout.expected > + echo "M epsilon/beta.link" >> $testroot/stdout.expected > + echo "M epsilon.link" >> $testroot/stdout.expected > + echo "D nonexistent.link" >> $testroot/stdout.expected > + echo "Created commit $commit_id" >> $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + got tree -r $testroot/repo -c $commit_id > $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "got tree command failed unexpectedly" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + cat > $testroot/stdout.expected < +alpha > +alpha.link@ -> beta > +beta > +dotgotbar.link@ -> .got/bar > +dotgotfoo.link > +epsilon/ > +epsilon.link@ -> gamma > +gamma/ > +passwd.link@ -> /etc/passwd > +zeta.link@ -> gamma/delta > +EOF > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + return 1 > + fi > + > + if [ -h $testroot/wt/alpha.link ]; then > + echo "alpha.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo 'this is regular file alpha.link' > $testroot/content.expected > + cp $testroot/wt/alpha.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/dotgotbar.link ]; then > + echo "dotgotbar.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + (cd $testroot/wt && got update > /dev/null) > + if [ -h $testroot/wt/dotgotbar.link ]; then > + echo "dotgotbar.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + echo -n ".got/bar" > $testroot/content.expected > + cp $testroot/wt/dotgotbar.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/dotgotfoo.link ]; then > + echo "dotgotfoo.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + echo "this is regular file foo" > $testroot/content.expected > + cp $testroot/wt/dotgotfoo.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/epsilon.link ]; then > + echo "epsilon.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon.link > $testroot/stdout > + echo "gamma" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/passwd.link ]; then > + echo "passwd.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + echo -n "/etc/passwd" > $testroot/content.expected > + cp $testroot/wt/passwd.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/zeta.link ]; then > + echo "zeta.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/zeta.link > $testroot/stdout > + echo "gamma/delta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + test_done "$testroot" "0" > +} > + > +function test_stage_patch_symlink { > + local testroot=`test_init stage_patch_symlink` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + local head_commit=`git_show_head $testroot/repo` > + > + got checkout $testroot/repo $testroot/wt > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && ln -sf beta alpha.link) > + (cd $testroot/wt && ln -sfh gamma epsilon.link) > + (cd $testroot/wt && ln -sf ../gamma/delta epsilon/beta.link) > + echo 'this is regular file foo' > $testroot/wt/dotgotfoo.link > + (cd $testroot/wt && got add dotgotfoo.link > /dev/null) > + (cd $testroot/wt && ln -sf .got/bar dotgotbar.link) > + (cd $testroot/wt && got add dotgotbar.link > /dev/null) > + (cd $testroot/wt && got rm nonexistent.link > /dev/null) > + (cd $testroot/wt && ln -sf gamma/delta zeta.link) > + (cd $testroot/wt && got add zeta.link > /dev/null) > + > + printf "y\nn\ny\nn\ny\ny\ny" > $testroot/patchscript > + (cd $testroot/wt && got stage -F $testroot/patchscript -p \ > + > $testroot/stdout) > + > + cat > $testroot/stdout.expected < +----------------------------------------------- > +@@ -1 +1 @@ > +-alpha > +\ No newline at end of file > ++beta > +\ No newline at end of file > +----------------------------------------------- > +M alpha.link (change 1 of 1) > +stage this change? [y/n/q] y > +A dotgotbar.link > +stage this addition? [y/n] n > +A dotgotfoo.link > +stage this addition? [y/n] y > +----------------------------------------------- > +@@ -1 +1 @@ > +-../beta > +\ No newline at end of file > ++../gamma/delta > +\ No newline at end of file > +----------------------------------------------- > +M epsilon/beta.link (change 1 of 1) > +stage this change? [y/n/q] n > +----------------------------------------------- > +@@ -1 +1 @@ > +-epsilon > +\ No newline at end of file > ++gamma > +\ No newline at end of file > +----------------------------------------------- > +M epsilon.link (change 1 of 1) > +stage this change? [y/n/q] y > +D nonexistent.link > +stage this deletion? [y/n] y > +A zeta.link > +stage this addition? [y/n] y > +EOF > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + rm $testroot/wt/alpha.link > + echo 'this is regular file alpha.link' > $testroot/wt/alpha.link > + > + (cd $testroot/wt && got diff -s > $testroot/stdout) > + > + echo "diff $head_commit $testroot/wt (staged changes)" \ > + > $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -i | grep 'alpha.link@ -> alpha$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + (cd $testroot/wt && got stage -l alpha.link) | cut -d' ' -f 1 \ > + >> $testroot/stdout.expected > + echo '--- alpha.link' >> $testroot/stdout.expected > + echo '+++ alpha.link' >> $testroot/stdout.expected > + echo '@@ -1 +1 @@' >> $testroot/stdout.expected > + echo '-alpha' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo '+beta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo 'blob - /dev/null' >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + (cd $testroot/wt && got stage -l dotgotfoo.link) | cut -d' ' -f 1 \ > + >> $testroot/stdout.expected > + echo '--- /dev/null' >> $testroot/stdout.expected > + echo '+++ dotgotfoo.link' >> $testroot/stdout.expected > + echo '@@ -0,0 +1 @@' >> $testroot/stdout.expected > + echo '+this is regular file foo' >> $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -i | grep 'epsilon.link@ -> epsilon$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + (cd $testroot/wt && got stage -l epsilon.link) | cut -d' ' -f 1 \ > + >> $testroot/stdout.expected > + echo '--- epsilon.link' >> $testroot/stdout.expected > + echo '+++ epsilon.link' >> $testroot/stdout.expected > + echo '@@ -1 +1 @@' >> $testroot/stdout.expected > + echo '-epsilon' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo '+gamma' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo -n 'blob - ' >> $testroot/stdout.expected > + got tree -r $testroot/repo -i | grep 'nonexistent.link@ -> nonexistent$' | \ > + cut -d' ' -f 1 >> $testroot/stdout.expected > + echo 'blob + /dev/null' >> $testroot/stdout.expected > + echo '--- nonexistent.link' >> $testroot/stdout.expected > + echo '+++ /dev/null' >> $testroot/stdout.expected > + echo '@@ -1 +0,0 @@' >> $testroot/stdout.expected > + echo '-nonexistent' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + echo 'blob - /dev/null' >> $testroot/stdout.expected > + echo -n 'blob + ' >> $testroot/stdout.expected > + (cd $testroot/wt && got stage -l zeta.link) | cut -d' ' -f 1 \ > + >> $testroot/stdout.expected > + echo '--- /dev/null' >> $testroot/stdout.expected > + echo '+++ zeta.link' >> $testroot/stdout.expected > + echo '@@ -0,0 +1 @@' >> $testroot/stdout.expected > + echo '+gamma/delta' >> $testroot/stdout.expected > + echo '\ No newline at end of file' >> $testroot/stdout.expected > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && got commit -m "staged symlink" \ > + > $testroot/stdout) > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "got commit command failed unexpectedly" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + local commit_id=`git_show_head $testroot/repo` > + echo "A dotgotfoo.link" > $testroot/stdout.expected > + echo "A zeta.link" >> $testroot/stdout.expected > + echo "M alpha.link" >> $testroot/stdout.expected > + echo "M epsilon.link" >> $testroot/stdout.expected > + echo "D nonexistent.link" >> $testroot/stdout.expected > + echo "Created commit $commit_id" >> $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + got tree -r $testroot/repo -c $commit_id > $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "got tree command failed unexpectedly" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + cat > $testroot/stdout.expected < +alpha > +alpha.link@ -> beta > +beta > +dotgotfoo.link > +epsilon/ > +epsilon.link@ -> gamma > +gamma/ > +passwd.link@ -> /etc/passwd > +zeta.link@ -> gamma/delta > +EOF > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + return 1 > + fi > + > + if [ -h $testroot/wt/alpha.link ]; then > + echo "alpha.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo 'this is regular file alpha.link' > $testroot/content.expected > + cp $testroot/wt/alpha.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/dotgotbar.link ]; then > + echo "dotgotbar.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + readlink $testroot/wt/dotgotbar.link > $testroot/stdout > + echo ".got/bar" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/dotgotfoo.link ]; then > + echo "dotgotfoo.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + echo "this is regular file foo" > $testroot/content.expected > + cp $testroot/wt/dotgotfoo.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/epsilon.link ]; then > + echo "epsilon.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon.link > $testroot/stdout > + echo "gamma" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/passwd.link ]; then > + echo "passwd.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + echo -n "/etc/passwd" > $testroot/content.expected > + cp $testroot/wt/passwd.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/zeta.link ]; then > + echo "zeta.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/zeta.link > $testroot/stdout > + echo "gamma/delta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + test_done "$testroot" "0" > +} > + > run_test test_stage_basic > run_test test_stage_no_changes > run_test test_stage_unversioned > @@ -2380,3 +2985,5 @@ run_test test_stage_patch_removed > run_test test_stage_patch_removed_twice > run_test test_stage_patch_quit > run_test test_stage_patch_incomplete_script > +run_test test_stage_symlink > +run_test test_stage_patch_symlink > blob - bf27874f97a3459375523f37a2f63d9a83168936 > blob + d510c15f15bfe869139ef654fac21eb2100b2a51 > --- regress/cmdline/status.sh > +++ regress/cmdline/status.sh > @@ -258,12 +258,14 @@ function test_status_unversioned_subdirs { > test_done "$testroot" "$ret" > } > > -# 'got status' ignores symlinks at present; this might change eventually > -function test_status_ignores_symlink { > - local testroot=`test_init status_ignores_symlink 1` > +function test_status_symlink { > + local testroot=`test_init status_symlink` > > mkdir $testroot/repo/ramdisk/ > touch $testroot/repo/ramdisk/Makefile > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > (cd $testroot/repo && git add .) > git_commit $testroot/repo -m "first commit" > > @@ -276,7 +278,7 @@ function test_status_ignores_symlink { > > ln -s /usr/obj/distrib/i386/ramdisk $testroot/wt/ramdisk/obj > > - echo -n > $testroot/stdout.expected > + echo "? ramdisk/obj" > $testroot/stdout.expected > > (cd $testroot/wt && got status > $testroot/stdout) > > @@ -284,7 +286,33 @@ function test_status_ignores_symlink { > ret="$?" > if [ "$ret" != "0" ]; then > diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > fi > + > + (cd $testroot/wt && ln -sf beta alpha.link) > + (cd $testroot/wt && ln -sfh gamma epsilon.link) > + > + (cd $testroot/wt && ln -s /etc/passwd passwd.link) > + (cd $testroot/wt && ln -s ../beta epsilon/beta.link) > + (cd $testroot/wt && got add passwd.link epsilon/beta.link > /dev/null) > + > + (cd $testroot/wt && got rm nonexistent.link > /dev/null) > + > + echo 'M alpha.link' > $testroot/stdout.expected > + echo 'A epsilon/beta.link' >> $testroot/stdout.expected > + echo 'M epsilon.link' >> $testroot/stdout.expected > + echo 'D nonexistent.link' >> $testroot/stdout.expected > + echo 'A passwd.link' >> $testroot/stdout.expected > + echo "? ramdisk/obj" >> $testroot/stdout.expected > + > + (cd $testroot/wt && got status > $testroot/stdout) > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + fi > test_done "$testroot" "$ret" > } > > @@ -612,7 +640,7 @@ run_test test_status_subdir_no_mods2 > run_test test_status_obstructed > run_test test_status_shows_local_mods_after_update > run_test test_status_unversioned_subdirs > -run_test test_status_ignores_symlink > +run_test test_status_symlink > run_test test_status_shows_no_mods_after_complete_merge > run_test test_status_shows_conflict > run_test test_status_empty_dir > blob - ef2a91d696b6b381bc506f6ed946a1bfa9412d17 > blob + 3cd20d37557eceb704929d98cb9c43eab91e88eb > --- regress/cmdline/unstage.sh > +++ regress/cmdline/unstage.sh > @@ -953,6 +953,470 @@ EOF > test_done "$testroot" "$ret" > } > > +function test_unstage_symlink { > + local testroot=`test_init unstage_symlink` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + local head_commit=`git_show_head $testroot/repo` > + > + got checkout $testroot/repo $testroot/wt > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && ln -sf beta alpha.link) > + (cd $testroot/wt && ln -sfh gamma epsilon.link) > + (cd $testroot/wt && ln -sf ../gamma/delta epsilon/beta.link) > + echo 'this is regular file foo' > $testroot/wt/dotgotfoo.link > + (cd $testroot/wt && got add dotgotfoo.link > /dev/null) > + (cd $testroot/wt && ln -sf .got/bar dotgotbar.link) > + (cd $testroot/wt && got add dotgotbar.link > /dev/null) > + (cd $testroot/wt && got rm nonexistent.link > /dev/null) > + (cd $testroot/wt && ln -sf gamma/delta zeta.link) > + (cd $testroot/wt && got add zeta.link > /dev/null) > + > + (cd $testroot/wt && got stage -S > /dev/null) > + > + (cd $testroot/wt && got status > $testroot/stdout) > + cat > $testroot/stdout.expected < + M alpha.link > + A dotgotbar.link > + A dotgotfoo.link > + M epsilon/beta.link > + M epsilon.link > + D nonexistent.link > + A zeta.link > +EOF > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/wt && got unstage > $testroot/stdout) > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "got unstage command failed unexpectedly" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + cat > $testroot/stdout.expected < +G alpha.link > +G dotgotbar.link > +G dotgotfoo.link > +G epsilon/beta.link > +G epsilon.link > +D nonexistent.link > +G zeta.link > +EOF > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/alpha.link ]; then > + echo "alpha.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/alpha.link > $testroot/stdout > + echo "beta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/epsilon.link ]; then > + echo "epsilon.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon.link > $testroot/stdout > + echo "gamma" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/epsilon/beta.link ]; then > + echo "epsilon/beta.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon/beta.link > $testroot/stdout > + echo "../gamma/delta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -f $testroot/wt/dotgotfoo.link ]; then > + echo "dotgotfoo.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "this is regular file foo" > $testroot/content.expected > + cp $testroot/wt/dotgotfoo.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + # bad symlinks are allowed as-is for commit and stage/unstage > + if [ ! -h $testroot/wt/dotgotbar.link ]; then > + echo "dotgotbar.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/dotgotbar.link > $testroot/stdout > + echo ".got/bar" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -e $testroot/wt/nonexistent.link ]; then > + echo "nonexistent.link exists on disk" > + test_done "$testroot" "1" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/zeta.link ]; then > + echo "zeta.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/zeta.link > $testroot/stdout > + echo "gamma/delta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + test_done "$testroot" "0" > +} > + > +function test_unstage_patch_symlink { > + local testroot=`test_init unstage_patch_symlink` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && ln -sf epsilon/zeta zeta.link) > + (cd $testroot/repo && ln -sf epsilon/zeta zeta2.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + local commit_id1=`git_show_head $testroot/repo` > + > + got checkout $testroot/repo $testroot/wt > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + # symlink to file A now points to file B > + (cd $testroot/wt && ln -sf gamma/delta alpha.link) > + # symlink to a directory A now points to file B > + (cd $testroot/wt && ln -sfh beta epsilon.link) > + # "bad" symlink now contains a different target path > + echo "foo" > $testroot/wt/passwd.link > + # relative symlink to directory A now points to relative directory B > + (cd $testroot/wt && ln -sfh ../gamma epsilon/beta.link) > + # an unversioned symlink > + (cd $testroot/wt && ln -sf .got/foo dotgotfoo.link) > + # symlink to file A now points to non-existent file B > + (cd $testroot/wt && ln -sf nonexistent2 nonexistent.link) > + # removed symlink > + (cd $testroot/wt && got rm zeta.link > /dev/null) > + (cd $testroot/wt && got rm zeta2.link > /dev/null) > + # added symlink > + (cd $testroot/wt && ln -sf beta new.link) > + (cd $testroot/wt && got add new.link > /dev/null) > + (cd $testroot/wt && ln -sf beta zeta3.link) > + (cd $testroot/wt && got add zeta3.link > /dev/null) > + > + (cd $testroot/wt && got stage -S > /dev/null) > + > + (cd $testroot/wt && got status > $testroot/stdout) > + cat > $testroot/stdout.expected < + M alpha.link > +? dotgotfoo.link > + M epsilon/beta.link > + M epsilon.link > + A new.link > + M nonexistent.link > + M passwd.link > + D zeta.link > + D zeta2.link > + A zeta3.link > +EOF > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + printf "y\nn\ny\nn\ny\ny\nn\ny\ny\n" > $testroot/patchscript > + (cd $testroot/wt && got unstage -F $testroot/patchscript -p \ > + > $testroot/stdout) > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "got unstage command failed unexpectedly" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + cat > $testroot/stdout.expected < +----------------------------------------------- > +@@ -1 +1 @@ > +-alpha > +\ No newline at end of file > ++gamma/delta > +\ No newline at end of file > +----------------------------------------------- > +M alpha.link (change 1 of 1) > +unstage this change? [y/n/q] y > +G alpha.link > +----------------------------------------------- > +@@ -1 +1 @@ > +-../beta > +\ No newline at end of file > ++../gamma > +\ No newline at end of file > +----------------------------------------------- > +M epsilon/beta.link (change 1 of 1) > +unstage this change? [y/n/q] n > +----------------------------------------------- > +@@ -1 +1 @@ > +-epsilon > +\ No newline at end of file > ++beta > +\ No newline at end of file > +----------------------------------------------- > +M epsilon.link (change 1 of 1) > +unstage this change? [y/n/q] y > +G epsilon.link > +A new.link > +unstage this addition? [y/n] n > +----------------------------------------------- > +@@ -1 +1 @@ > +-nonexistent > +\ No newline at end of file > ++nonexistent2 > +\ No newline at end of file > +----------------------------------------------- > +M nonexistent.link (change 1 of 1) > +unstage this change? [y/n/q] y > +G nonexistent.link > +----------------------------------------------- > +@@ -1 +1 @@ > +-/etc/passwd > +\ No newline at end of file > ++foo > +----------------------------------------------- > +M passwd.link (change 1 of 1) > +unstage this change? [y/n/q] y > +G passwd.link > +D zeta.link > +unstage this deletion? [y/n] n > +D zeta2.link > +unstage this deletion? [y/n] y > +D zeta2.link > +A zeta3.link > +unstage this addition? [y/n] y > +G zeta3.link > +EOF > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/alpha.link ]; then > + echo "alpha.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/alpha.link > $testroot/stdout > + echo "gamma/delta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/epsilon.link ]; then > + echo "epsilon.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon.link > $testroot/stdout > + echo "beta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/passwd.link ]; then > + echo "passwd.link should not be a symlink" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "foo" > $testroot/content.expected > + cp $testroot/wt/passwd.link $testroot/content > + > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon/beta.link > $testroot/stdout > + echo "../gamma" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + readlink $testroot/wt/nonexistent.link > $testroot/stdout > + echo "nonexistent2" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/dotgotfoo.link ]; then > + echo "dotgotfoo.link is not a symlink " >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + readlink $testroot/wt/dotgotfoo.link > $testroot/stdout > + echo ".got/foo" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + > + if [ -e $testroot/wt/zeta.link ]; then > + echo -n "zeta.link should not exist on disk" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + if [ -e $testroot/wt/zeta2.link ]; then > + echo -n "zeta2.link exists on disk" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/zeta3.link ]; then > + echo -n "zeta3.link is not a symlink" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/zeta3.link > $testroot/stdout > + echo "beta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ ! -h $testroot/wt/new.link ]; then > + echo -n "new.link is not a symlink" >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + (cd $testroot/wt && got status > $testroot/stdout) > + echo "M alpha.link" > $testroot/stdout.expected > + echo "? dotgotfoo.link" >> $testroot/stdout.expected > + echo " M epsilon/beta.link" >> $testroot/stdout.expected > + echo "M epsilon.link" >> $testroot/stdout.expected > + echo " A new.link" >> $testroot/stdout.expected > + echo "M nonexistent.link" >> $testroot/stdout.expected > + echo "M passwd.link" >> $testroot/stdout.expected > + echo " D zeta.link" >> $testroot/stdout.expected > + echo "D zeta2.link" >> $testroot/stdout.expected > + echo "A zeta3.link" >> $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + return 1 > + fi > + test_done "$testroot" "$ret" > +} > + > run_test test_unstage_basic > run_test test_unstage_unversioned > run_test test_unstage_nonexistent > @@ -960,3 +1424,5 @@ run_test test_unstage_patch > run_test test_unstage_patch_added > run_test test_unstage_patch_removed > run_test test_unstage_patch_quit > +run_test test_unstage_symlink > +run_test test_unstage_patch_symlink > blob - 60a1a2ab40ff159845a71f43bd69ec3145dfa795 > blob + 80b89ada1f0b16289f55b21fccf251a40d0bd17b > --- regress/cmdline/update.sh > +++ regress/cmdline/update.sh > @@ -1825,7 +1825,435 @@ function test_update_conflict_wt_file_vs_repo_submodul > test_done "$testroot" "$ret" > } > > +function test_update_adds_symlink { > + local testroot=`test_init update_adds_symlink` > > + got checkout $testroot/repo $testroot/wt > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "checkout failed unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + > + echo "A alpha.link" > $testroot/stdout.expected > + echo "A epsilon/beta.link" >> $testroot/stdout.expected > + echo "A epsilon.link" >> $testroot/stdout.expected > + echo "A nonexistent.link" >> $testroot/stdout.expected > + echo "A passwd.link" >> $testroot/stdout.expected > + echo -n "Updated to commit " >> $testroot/stdout.expected > + git_show_head $testroot/repo >> $testroot/stdout.expected > + echo >> $testroot/stdout.expected > + > + (cd $testroot/wt && got update > $testroot/stdout) > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/alpha.link ]; then > + echo "alpha.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/alpha.link > $testroot/stdout > + echo "alpha" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if ! [ -h $testroot/wt/epsilon.link ]; then > + echo "epsilon.link is not a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon.link > $testroot/stdout > + echo "epsilon" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/passwd.link ]; then > + echo -n "passwd.link symlink points outside of work tree: " >&2 > + readlink $testroot/wt/passwd.link >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo -n "/etc/passwd" > $testroot/content.expected > + cp $testroot/wt/passwd.link $testroot/content > + > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + readlink $testroot/wt/epsilon/beta.link > $testroot/stdout > + echo "../beta" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + readlink $testroot/wt/nonexistent.link > $testroot/stdout > + echo "nonexistent" > $testroot/stdout.expected > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + fi > + test_done "$testroot" "$ret" > +} > + > +function test_update_deletes_symlink { > + local testroot=`test_init update_deletes_symlink` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlink" > + > + got checkout $testroot/repo $testroot/wt > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "checkout failed unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/repo && git rm -q alpha.link) > + git_commit $testroot/repo -m "delete symlink" > + > + echo "D alpha.link" > $testroot/stdout.expected > + echo -n "Updated to commit " >> $testroot/stdout.expected > + git_show_head $testroot/repo >> $testroot/stdout.expected > + echo >> $testroot/stdout.expected > + > + (cd $testroot/wt && got update > $testroot/stdout) > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -e $testroot/wt/alpha.link ]; then > + echo "alpha.link still exists on disk" > + test_done "$testroot" "1" > + return 1 > + fi > + > + test_done "$testroot" "0" > +} > + > +function test_update_symlink_conflicts { > + local testroot=`test_init update_symlink_conflicts` > + > + (cd $testroot/repo && ln -s alpha alpha.link) > + (cd $testroot/repo && ln -s epsilon epsilon.link) > + (cd $testroot/repo && ln -s /etc/passwd passwd.link) > + (cd $testroot/repo && ln -s ../beta epsilon/beta.link) > + (cd $testroot/repo && ln -s nonexistent nonexistent.link) > + (cd $testroot/repo && ln -sf epsilon/zeta zeta.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "add symlinks" > + local commit_id1=`git_show_head $testroot/repo` > + > + got checkout $testroot/repo $testroot/wt > /dev/null > + ret="$?" > + if [ "$ret" != "0" ]; then > + echo "checkout failed unexpectedly" >&2 > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + (cd $testroot/repo && ln -sf beta alpha.link) > + (cd $testroot/repo && ln -sfh gamma epsilon.link) > + (cd $testroot/repo && ln -sf ../gamma/delta epsilon/beta.link) > + echo 'this is regular file foo' > $testroot/repo/dotgotfoo.link > + (cd $testroot/repo && ln -sf .got/bar dotgotbar.link) > + (cd $testroot/repo && git rm -q nonexistent.link) > + (cd $testroot/repo && ln -sf gamma/delta zeta.link) > + (cd $testroot/repo && ln -sf alpha new.link) > + (cd $testroot/repo && git add .) > + git_commit $testroot/repo -m "change symlinks" > + local commit_id2=`git_show_head $testroot/repo` > + > + # modified symlink to file A vs modified symlink to file B > + (cd $testroot/wt && ln -sf gamma/delta alpha.link) > + # modified symlink to dir A vs modified symlink to file B > + (cd $testroot/wt && ln -sfh beta epsilon.link) > + # modeified symlink to file A vs modified symlink to dir B > + (cd $testroot/wt && ln -sfh ../gamma epsilon/beta.link) > + # added regular file A vs added bad symlink to file A > + (cd $testroot/wt && ln -sf .got/bar dotgotfoo.link) > + (cd $testroot/wt && got add dotgotfoo.link > /dev/null) > + # added bad symlink to file A vs added regular file A > + echo 'this is regular file bar' > $testroot/wt/dotgotbar.link > + (cd $testroot/wt && got add dotgotbar.link > /dev/null) > + # removed symlink to non-existent file A vs modified symlink > + # to nonexistent file B > + (cd $testroot/wt && ln -sf nonexistent2 nonexistent.link) > + # modified symlink to file A vs removed symlink to file A > + (cd $testroot/wt && got rm zeta.link > /dev/null) > + # added symlink to file A vs added symlink to file B > + (cd $testroot/wt && ln -sf beta new.link) > + (cd $testroot/wt && got add new.link > /dev/null) > + > + (cd $testroot/wt && got update > $testroot/stdout) > + > + echo "C alpha.link" >> $testroot/stdout.expected > + echo "C dotgotbar.link" >> $testroot/stdout.expected > + echo "C dotgotfoo.link" >> $testroot/stdout.expected > + echo "C epsilon/beta.link" >> $testroot/stdout.expected > + echo "C epsilon.link" >> $testroot/stdout.expected > + echo "C new.link" >> $testroot/stdout.expected > + echo "C nonexistent.link" >> $testroot/stdout.expected > + echo "G zeta.link" >> $testroot/stdout.expected > + echo -n "Updated to commit " >> $testroot/stdout.expected > + git_show_head $testroot/repo >> $testroot/stdout.expected > + echo >> $testroot/stdout.expected > + echo "Files with new merge conflicts: 7" >> $testroot/stdout.expected > + > + cmp -s $testroot/stdout.expected $testroot/stdout > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/alpha.link ]; then > + echo "alpha.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "<<<<<<< merged change: commit $commit_id2" \ > + > $testroot/content.expected > + echo "beta" >> $testroot/content.expected > + echo "3-way merge base: commit $commit_id1" \ > + >> $testroot/content.expected > + echo "alpha" >> $testroot/content.expected > + echo "=======" >> $testroot/content.expected > + echo "gamma/delta" >> $testroot/content.expected > + echo '>>>>>>>' >> $testroot/content.expected > + echo -n "" >> $testroot/content.expected > + > + cp $testroot/wt/alpha.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/epsilon.link ]; then > + echo "epsilon.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "<<<<<<< merged change: commit $commit_id2" \ > + > $testroot/content.expected > + echo "gamma" >> $testroot/content.expected > + echo "3-way merge base: commit $commit_id1" \ > + >> $testroot/content.expected > + echo "epsilon" >> $testroot/content.expected > + echo "=======" >> $testroot/content.expected > + echo "beta" >> $testroot/content.expected > + echo '>>>>>>>' >> $testroot/content.expected > + echo -n "" >> $testroot/content.expected > + > + cp $testroot/wt/epsilon.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/passwd.link ]; then > + echo -n "passwd.link symlink points outside of work tree: " >&2 > + readlink $testroot/wt/passwd.link >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo -n "/etc/passwd" > $testroot/content.expected > + cp $testroot/wt/passwd.link $testroot/content > + > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/epsilon/beta.link ]; then > + echo "epsilon/beta.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "<<<<<<< merged change: commit $commit_id2" \ > + > $testroot/content.expected > + echo "../gamma/delta" >> $testroot/content.expected > + echo "3-way merge base: commit $commit_id1" \ > + >> $testroot/content.expected > + echo "../beta" >> $testroot/content.expected > + echo "=======" >> $testroot/content.expected > + echo "../gamma" >> $testroot/content.expected > + echo '>>>>>>>' >> $testroot/content.expected > + echo -n "" >> $testroot/content.expected > + > + cp $testroot/wt/epsilon/beta.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/nonexistent.link ]; then > + echo -n "nonexistent.link still exists on disk: " >&2 > + readlink $testroot/wt/nonexistent.link >&2 > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "<<<<<<< merged change: commit $commit_id2" \ > + > $testroot/content.expected > + echo "(symlink was deleted)" >> $testroot/content.expected > + echo "=======" >> $testroot/content.expected > + echo "nonexistent2" >> $testroot/content.expected > + echo '>>>>>>>' >> $testroot/content.expected > + echo -n "" >> $testroot/content.expected > + > + cp $testroot/wt/nonexistent.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/dotgotfoo.link ]; then > + echo "dotgotfoo.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "<<<<<<< merged change: commit $commit_id2" \ > + > $testroot/content.expected > + echo "this is regular file foo" >> $testroot/content.expected > + echo "=======" >> $testroot/content.expected > + echo -n ".got/bar" >> $testroot/content.expected > + echo '>>>>>>>' >> $testroot/content.expected > + echo -n "" >> $testroot/content.expected > + > + cp $testroot/wt/dotgotfoo.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/dotgotbar.link ]; then > + echo "dotgotbar.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + echo "<<<<<<< merged change: commit $commit_id2" \ > + > $testroot/content.expected > + echo -n ".got/bar" >> $testroot/content.expected > + echo "=======" >> $testroot/content.expected > + echo "this is regular file bar" >> $testroot/content.expected > + echo '>>>>>>>' >> $testroot/content.expected > + echo -n "" >> $testroot/content.expected > + > + cp $testroot/wt/dotgotbar.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + if [ -h $testroot/wt/new.link ]; then > + echo "new.link is a symlink" > + test_done "$testroot" "1" > + return 1 > + fi > + > + echo "<<<<<<< merged change: commit $commit_id2" \ > + > $testroot/content.expected > + echo "alpha" >> $testroot/content.expected > + echo "=======" >> $testroot/content.expected > + echo "beta" >> $testroot/content.expected > + echo '>>>>>>>' >> $testroot/content.expected > + echo -n "" >> $testroot/content.expected > + > + cp $testroot/wt/new.link $testroot/content > + cmp -s $testroot/content.expected $testroot/content > + ret="$?" > + if [ "$ret" != "0" ]; then > + diff -u $testroot/content.expected $testroot/content > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + echo "A dotgotfoo.link" > $testroot/stdout.expected > + echo "M new.link" >> $testroot/stdout.expected > + echo "D nonexistent.link" >> $testroot/stdout.expected > + (cd $testroot/wt && got status > $testroot/stdout) > + if [ "$ret" != "0" ]; then > + diff -u $testroot/stdout.expected $testroot/stdout > + test_done "$testroot" "$ret" > + return 1 > + fi > + > + test_done "$testroot" "0" > + > +} > + > run_test test_update_basic > run_test test_update_adds_file > run_test test_update_deletes_file > @@ -1861,3 +2289,6 @@ run_test test_update_preserves_conflicted_file > run_test test_update_modified_submodules > run_test test_update_adds_submodule > run_test test_update_conflict_wt_file_vs_repo_submodule > +run_test test_update_adds_symlink > +run_test test_update_deletes_symlink > +run_test test_update_symlink_conflicts > blob - 37110e98ad018c151bf660c62fd637570da2df3a > blob + 47ddd48ca251a920a6562265a2746e94e9df8ad5 > --- tog/tog.c > +++ tog/tog.c > @@ -4499,6 +4499,7 @@ cmd_blame(int argc, char *argv[]) > struct got_reflist_head refs; > struct got_worktree *worktree = NULL; > char *cwd = NULL, *repo_path = NULL, *in_repo_path = NULL; > + char *link_target = NULL; > struct got_object_id *commit_id = NULL; > char *commit_id_str = NULL; > int ch; > @@ -4594,9 +4595,16 @@ cmd_blame(int argc, char *argv[]) > error = got_error_from_errno("view_open"); > goto done; > } > - error = open_blame_view(view, in_repo_path, commit_id, &refs, repo); > + > + error = got_object_resolve_symlinks(&link_target, in_repo_path, > + commit_id, repo); > if (error) > goto done; > + > + error = open_blame_view(view, link_target ? link_target : in_repo_path, > + commit_id, &refs, repo); > + if (error) > + goto done; > if (worktree) { > /* Release work tree lock. */ > got_worktree_close(worktree); > @@ -4606,6 +4614,7 @@ cmd_blame(int argc, char *argv[]) > done: > free(repo_path); > free(in_repo_path); > + free(link_target); > free(cwd); > free(commit_id); > if (worktree) -- Tracey Emery