Download raw body.
install symbolic links in the work tree
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 <sha1.h>
> #include <zlib.h>
> #include <ctype.h>
> +#include <libgen.h>
> #include <limits.h>
> #include <imsg.h>
> #include <time.h>
> @@ -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 <<EOF
> +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 <<EOF
> +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 <<EOF
> +-----------------------------------------------
> +@@ -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 <<EOF
> + 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 <<EOF
> +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 <<EOF
> +-----------------------------------------------
> +@@ -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 <<EOF
> +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 <<EOF
> + 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 <<EOF
> +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 <<EOF
> + 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 <<EOF
> +-----------------------------------------------
> +@@ -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
install symbolic links in the work tree