"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Tracey Emery <tracey@traceyemery.net>
Subject:
Re: install symbolic links in the work tree
To:
gameoftrees@openbsd.org
Date:
Tue, 21 Jul 2020 09:41:42 -0600

Download raw body.

Thread
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