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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
Re: install symbolic links in the work tree
To:
Sebastien Marie <semarie@online.fr>, gameoftrees@openbsd.org
Date:
Sun, 19 Jul 2020 18:54:41 +0200

Download raw body.

Thread
The symlink branch is now ready for review and testing.

To test the new code, compile and install got from the 'symlink' branch
in the repository and let the list know if you see any regressions.
Test coverage is quite decent so I am not expecting many regressions.

I have pushed out a rebased version of this branch, which could be
integrated if we want to. But we may want to cut another release from the
current state of the master branch first. It might be good to have these
changes tested for a while by people who run the latest state of the repo.

More details below, with context from a discussion we had earlier:

On Mon, Jun 01, 2020 at 10:40:15AM +0200, Stefan Sperling wrote:
> On Sun, May 31, 2020 at 03:30:39PM +0200, Sebastien Marie wrote:
> > I agree that such harmfull symlink shouldn't be created. I am just uncertain if
> > the behaviour should be:
> > 
> > 1- create a regular file (instead of a symlink)
> 
> I think it should create a regular file.
> 
> Symlinks may have two possible on-disk representations:
> An actual symlink if the target is safe, and a regular file otherwise.
> This just affects the on-disk state. Meta data looks the same, and all
> commands will behave exactly the same way for both on-disk representations.
> 
> Any software that cares about there being an actual symlink on disk is
> software which uses files in the work tree: editors, compilers, or build
> systems. Got itself doesn't need to care.
> 
> By installing a regular file we can allow users to edit such links.
> 
> Users might expect a symlink and find that a regular file has been installed.
> This will be the documented behaviour in case a symlink target path points
> outside of a work tree (and also applies if a symlink points outside of a
> work-tree's path-prefix).
> 
> Uses can edit this file to change the target path. Only now will 'got status'
> and 'got diff' display it as modified. If the change is committed, the file
> will become an actual symlink if the new target path is safe to use.
> 
> Given an actual symlink, changing the link target with ln -s would have the
> same effect. And if the new target points outside of the work tree, the link
> will now be installed on-disk as a regular file.
> 
> Users will need a way to see whether a regular file is in fact a symlink.
> 'got tree' identifies files which are stored as symlinks in the repository,
> so that's one way of finding out. We could also add 'got info' or a similar
> command which displays meta-data for any path in a work tree, including the
> file type (regular or symlink).

Some of the above ideas didn't make it into the final design.

The code on the branch now behaves as follows:

We always commit (or stage) exactly what the user has put on disk.
If there is a symlink on disk we commit a symlink, otherwise we commit
a regular file. This is more intuitive than what I had in mind back in
June; see above.

If a symlink on disk points outside of the work tree, then 'got commit'
and 'got stage' will error out, unless the user specifies a new -S option
which overrides this safety check. This will avoid mistakes such as
committing links that point into /usr/obj.

If symlinks which point outside of the work tree arrive during 'got update'
or other commands that merge changes such as 'got rebase', those symlinks
are installed as regular files. There is no way to override this.
The idea is that got cares about symlinks pointing to other paths which
are also under version control, not about symlinks to arbitrary paths on
the system which is hosting a work tree. In particular, I don't want to
see people complaining after Got created a symlink which was later used
to facilitate a root hole in some other program that used files in the
work tree. Perhaps I am being paranoid, but I don't see a reason to let
users live with such a risk.

The full diff contained on the symlink branch is below. More than half of
this new code is test code. Reading the tests will help with understanding
the new behaviour in detail.

This is a very large change to review, so perhaps nobody will step up to
do that. But I really wanted to have all commands covered to ensure that
we don't end up with a half-baked implementation.

The incremental history is available on the branch and will hopefully help
with review. I intend to keep the incremental history intact if this branch
gets integrated, even though some code changes were reverted in later commits.
And there are some small improvements which are not specific to symlinks.

The diffstat output is:

 got/got.1                     |   27 
 got/got.c                     |  108 +++
 include/got_error.h           |    3 
 include/got_object.h          |   26 
 include/got_worktree.h        |   12 
 lib/diff.c                    |   19 
 lib/fileindex.c               |   72 ++
 lib/got_lib_fileindex.h       |    9 
 lib/object.c                  |  189 ++++++
 lib/object_create.c           |   33 -
 lib/repository.c              |   11 
 lib/worktree.c                | 1183 +++++++++++++++++++++++++++++++++++++-----
 regress/cmdline/add.sh        |   66 ++
 regress/cmdline/blame.sh      |  121 ++++
 regress/cmdline/cat.sh        |   77 ++
 regress/cmdline/checkout.sh   |  265 +++++++++
 regress/cmdline/cherrypick.sh |  389 +++++++++++++
 regress/cmdline/commit.sh     |  330 +++++++++++
 regress/cmdline/common.sh     |    3 
 regress/cmdline/diff.sh       |  223 +++++++
 regress/cmdline/import.sh     |   51 +
 regress/cmdline/revert.sh     |  432 +++++++++++++++
 regress/cmdline/rm.sh         |   35 +
 regress/cmdline/stage.sh      |  607 +++++++++++++++++++++
 regress/cmdline/status.sh     |   38 +
 regress/cmdline/unstage.sh    |  466 ++++++++++++++++
 regress/cmdline/update.sh     |  431 +++++++++++++++
 tog/tog.c                     |   11 
 28 files changed, 5032 insertions(+), 205 deletions(-)

Full diff follows.

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 .
+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.
 .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.
 .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;
-	
-	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;
+	
+	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)