Commit Diff


commit - 43012d5817071b74fc38aef617a3eb8f2da4a945
commit + 3ce1b84566a5dc6cbbfcfc87507aa84de4f0c9b9
blob - ac8037722555366872b56f211bcccace894db4d8
blob + 1103aa277a4635a8fe84c1c7112afb0056d03ded
--- got/got.1
+++ got/got.1
@@ -63,6 +63,58 @@ are as follows:
 .It Cm init Ar path
 Create a new empty repository at the specified
 .Ar path .
+.Pp
+After
+.Cm got init ,
+the
+.Cm got import
+command must be used to populate the empty repository before
+.Cm got checkout
+can be used.
+.Pp
+.It Cm import [ Fl b Ar branch ] [ Fl m Ar message ] [ Fl r Ar repository-path ] [ Fl I Ar pattern ] directory
+Create an initial commit in a repository from the file hierarchy
+within the specified
+.Ar directory .
+The created commit will not have any parent commits, i.e. it will be a
+root commit.
+Also create a new reference which provides a branch name for the newly
+created commit.
+.Pp
+Show the path of each added file to indicate progress.
+.Pp
+The options for
+.Cm got import
+are as follows:
+.Bl -tag -width Ds
+.It Fl b Ar branch
+Create the specified
+.Ar branch
+instead of creating the default branch
+.Dq master .
+Use of this option is required if the
+.Dq master
+branch already exists.
+.It Fl m Ar message
+Use the specified log message when creating the new commit.
+Without the
+.Fl m
+option,
+.Cm got import
+opens a temporary file in an editor where a log message can be written.
+.It Fl r Ar repository-path
+Use the repository at the specified path.
+If not specified, assume the repository is located at or above the current
+working directory.
+.It Fl I Ar pattern
+Ignore files or directories with a name which matches the specified
+.Ar pattern .
+This option may be specified multiple times to build a list of ignore patterns.
+The
+.Ar pattern
+follows the globbing rules documented in
+.Xr glob 7 .
+.El
 .It Cm checkout [ Fl b Ar branch ] [ Fl c Ar commit ] [ Fl p Ar path-prefix ] repository-path [ work-tree-path ]
 Copy files from a repository into a new work tree.
 If the
@@ -639,7 +691,6 @@ The editor spawned by
 .Sh EXIT STATUS
 .Ex -std got
 .Sh EXAMPLES
-.Pp
 Clone an existing Git repository for use with
 .Nm .
 This step currently requires
@@ -648,8 +699,17 @@ This step currently requires
 .Dl $ cd /var/git/
 .Dl $ git clone --bare https://github.com/openbsd/src.git
 .Pp
-Check out a work tree from this Git repository to /usr/src:
+Alternatively, for quick and dirty local testing of
+.Nm
+a new Git repository could be created and populated with files,
+e.g. from a temporary CVS checkout located at
+.Pa /tmp/src :
 .Pp
+.Dl $ got init /var/git/src.git
+.Dl $ got import -r /var/src.git -I CVS -I obj /tmp/src
+.Pp
+Check out a work tree from the Git repository to /usr/src:
+.Pp
 .Dl $ got checkout /var/git/src.git /usr/src
 .Pp
 View local changes in a work tree directory:
blob - 0bfd597accf4b09dfa3552fc715ec1c20c3108b7
blob + 2f7d8b5ef12121c16e9340c8363bb580459f61da
--- got/got.c
+++ got/got.c
@@ -76,6 +76,7 @@ struct got_cmd {
 
 __dead static void	usage(int);
 __dead static void	usage_init(void);
+__dead static void	usage_import(void);
 __dead static void	usage_checkout(void);
 __dead static void	usage_update(void);
 __dead static void	usage_log(void);
@@ -94,6 +95,7 @@ __dead static void	usage_backout(void);
 __dead static void	usage_rebase(void);
 
 static const struct got_error*		cmd_init(int, char *[]);
+static const struct got_error*		cmd_import(int, char *[]);
 static const struct got_error*		cmd_checkout(int, char *[]);
 static const struct got_error*		cmd_update(int, char *[]);
 static const struct got_error*		cmd_log(int, char *[]);
@@ -113,6 +115,7 @@ static const struct got_error*		cmd_rebase(int, char *
 
 static struct got_cmd got_commands[] = {
 	{ "init",	cmd_init,	usage_init,	"" },
+	{ "import",	cmd_import,	usage_import,	"" },
 	{ "checkout",	cmd_checkout,	usage_checkout,	"co" },
 	{ "update",	cmd_update,	usage_update,	"up" },
 	{ "log",	cmd_log,	usage_log,	"" },
@@ -327,10 +330,317 @@ cmd_init(int argc, char *argv[])
 
 	error = got_repo_init(repo_path);
 	if (error != NULL)
+		goto done;
+
+done:
+	free(repo_path);
+	return error;
+}
+
+__dead static void
+usage_import(void)
+{
+	fprintf(stderr, "usage: %s import [-b branch] [-m message] "
+	    "[-r repository-path] [-I pattern] path\n", getprogname());
+	exit(1);
+}
+
+int
+spawn_editor(const char *editor, const char *file)
+{
+	pid_t pid;
+	sig_t sighup, sigint, sigquit;
+	int st = -1;
+
+	sighup = signal(SIGHUP, SIG_IGN);
+	sigint = signal(SIGINT, SIG_IGN);
+	sigquit = signal(SIGQUIT, SIG_IGN);
+
+	switch (pid = fork()) {
+	case -1:
+		goto doneediting;
+	case 0:
+		execl(editor, editor, file, (char *)NULL);
+		_exit(127);
+	}
+
+	while (waitpid(pid, &st, 0) == -1)
+		if (errno != EINTR)
+			break;
+
+doneediting:
+	(void)signal(SIGHUP, sighup);
+	(void)signal(SIGINT, sigint);
+	(void)signal(SIGQUIT, sigquit);
+
+	if (!WIFEXITED(st)) {
+		errno = EINTR;
+		return -1;
+	}
+
+	return WEXITSTATUS(st);
+}
+
+static const struct got_error *
+edit_logmsg(char **logmsg, const char *editor, const char *logmsg_path,
+    const char *initial_content)
+{
+	const struct got_error *err = NULL;
+	char buf[1024];
+	struct stat st, st2;
+	FILE *fp;
+	int content_changed = 0;
+	size_t len;
+
+	*logmsg = NULL;
+
+	if (stat(logmsg_path, &st) == -1)
+		return got_error_from_errno2("stat", logmsg_path);
+
+	if (spawn_editor(editor, logmsg_path) == -1)
+		return got_error_from_errno("failed spawning editor");
+
+	if (stat(logmsg_path, &st2) == -1)
+		return got_error_from_errno("stat");
+
+	if (st.st_mtime == st2.st_mtime && st.st_size == st2.st_size)
+		return got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY,
+		    "no changes made to commit message, aborting");
+
+	*logmsg = malloc(st2.st_size + 1);
+	if (*logmsg == NULL)
+		return got_error_from_errno("malloc");
+	(*logmsg)[0] = '\0';
+	len = 0;
+
+	fp = fopen(logmsg_path, "r");
+	if (fp == NULL) {
+		err = got_error_from_errno("fopen");
+		goto done;
+	}
+	while (fgets(buf, sizeof(buf), fp) != NULL) {
+		if (!content_changed && strcmp(buf, initial_content) != 0)
+			content_changed = 1;
+		if (buf[0] == '#' || (len == 0 && buf[0] == '\n'))
+			continue; /* remove comments and leading empty lines */
+		len = strlcat(*logmsg, buf, st2.st_size);
+	}
+	fclose(fp);
+
+	while (len > 0 && (*logmsg)[len - 1] == '\n') {
+		(*logmsg)[len - 1] = '\0';
+		len--;
+	}
+
+	if (len == 0 || !content_changed)
+		err = got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY,
+		    "commit message cannot be empty, aborting");
+done:
+	if (err) {
+		free(*logmsg);
+		*logmsg = NULL;
+	}
+	return err;
+}
+
+static const struct got_error *
+collect_import_msg(char **logmsg, const char *editor, const char *path_dir,
+    const char *branch_name)
+{
+	char *initial_content = NULL, *logmsg_path = NULL;
+	const struct got_error *err = NULL;
+	int fd;
+
+	if (asprintf(&initial_content,
+	    "\n# %s to be imported to branch %s\n", path_dir,
+	    branch_name) == -1)
+		return got_error_from_errno("asprintf");
+
+	err = got_opentemp_named_fd(&logmsg_path, &fd, "/tmp/got-importmsg");
+	if (err)
+		goto done;
+
+	dprintf(fd, initial_content);
+	close(fd);
+
+	err = edit_logmsg(logmsg, editor, logmsg_path, initial_content);
+done:
+	free(initial_content);
+	free(logmsg_path);
+	return err;
+}
+
+static const struct got_error *
+import_progress(void *arg, const char *path)
+{
+	printf("A  %s\n", path);
+	return NULL;
+}
+
+static const struct got_error *
+cmd_import(int argc, char *argv[])
+{
+	const struct got_error *error = NULL;
+	char *path_dir = NULL, *repo_path = NULL, *logmsg = NULL;
+	char *editor = NULL;
+	const char *got_author = getenv("GOT_AUTHOR");
+	const char *branch_name = "master";
+	char *refname = NULL, *id_str = NULL;
+	struct got_repository *repo = NULL;
+	struct got_reference *branch_ref = NULL, *head_ref = NULL;
+	struct got_object_id *new_commit_id = NULL;
+	int ch;
+	struct got_pathlist_head ignores;
+	struct got_pathlist_entry *pe;
+
+	TAILQ_INIT(&ignores);
+
+	while ((ch = getopt(argc, argv, "b:m:r:I:")) != -1) {
+		switch (ch) {
+		case 'b':
+			branch_name = optarg;
+			break;
+		case 'm':
+			logmsg = strdup(optarg);
+			if (logmsg == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+			break;
+		case 'r':
+			repo_path = realpath(optarg, NULL);
+			if (repo_path == NULL) {
+				error = got_error_from_errno("realpath");
+				goto done;
+			}
+			break;
+		case 'I':
+			if (optarg[0] == '\0')
+				break;
+			error = got_pathlist_insert(&pe, &ignores, optarg,
+			    NULL);
+			if (error)
+				goto done;
+			break;
+		default:
+			usage_init();
+			/* NOTREACHED */
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+#ifndef PROFILE
+	if (pledge("stdio rpath wpath cpath fattr flock proc exec unveil",
+	    NULL) == -1)
+		err(1, "pledge");
+#endif
+	if (argc != 1)
+		usage_import();
+
+	if (got_author == NULL) {
+		/* TODO: Look current user up in password database */
+		error = got_error(GOT_ERR_COMMIT_NO_AUTHOR);
 		goto done;
+	}
 
+	if (repo_path == NULL) {
+		repo_path = getcwd(NULL, 0);
+		if (repo_path == NULL)
+			return got_error_from_errno("getcwd");
+	}
+	got_path_strip_trailing_slashes(repo_path);
+	error = got_repo_open(&repo, repo_path);
+	if (error)
+		goto done;
+
+	if (asprintf(&refname, "refs/heads/%s", branch_name) == -1) {
+		error = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	error = got_ref_open(&branch_ref, repo, refname, 0);
+	if (error) {
+		if (error->code != GOT_ERR_NOT_REF)
+			goto done;
+	} else {
+		error = got_error_msg(GOT_ERR_BRANCH_EXISTS,
+		    "import target branch already exists");
+		goto done;
+	}
+
+	path_dir = realpath(argv[0], NULL);
+	if (path_dir == NULL) {
+		error = got_error_from_errno("realpath");
+		goto done;
+	}
+	got_path_strip_trailing_slashes(path_dir);
+
+	/*
+	 * unveil(2) traverses exec(2); if an editor is used we have
+	 * to apply unveil after the log message has been written.
+	 */
+	if (logmsg == NULL || strlen(logmsg) == 0) {
+		error = get_editor(&editor);
+		if (error)
+			goto done;
+		error = collect_import_msg(&logmsg, editor, path_dir, refname);
+		if (error)
+			goto done;
+	}
+
+	if (unveil(path_dir, "r") != 0)
+		return got_error_from_errno2("unveil", path_dir);
+
+	error = apply_unveil(got_repo_get_path(repo), 0, NULL, 0);
+	if (error)
+		goto done;
+
+	error = got_repo_import(&new_commit_id, path_dir, logmsg,
+	    got_author, &ignores, repo, import_progress, NULL);
+	if (error)
+		goto done;
+
+	error = got_ref_alloc(&branch_ref, refname, new_commit_id);
+	if (error)
+		goto done;
+
+	error = got_ref_write(branch_ref, repo);
+	if (error)
+		goto done;
+
+	error = got_object_id_str(&id_str, new_commit_id);
+	if (error)
+		goto done;
+
+	error = got_ref_open(&head_ref, repo, GOT_REF_HEAD, 0);
+	if (error) {
+		if (error->code != GOT_ERR_NOT_REF)
+			goto done;
+
+		error = got_ref_alloc_symref(&head_ref, GOT_REF_HEAD,
+		    branch_ref);
+		if (error)
+			goto done;
+
+		error = got_ref_write(head_ref, repo);
+		if (error)
+			goto done;
+	}
+
+	printf("Created branch %s with commit %s\n",
+	    got_ref_get_name(branch_ref), id_str);
 done:
 	free(repo_path);
+	free(editor);
+	free(refname);
+	free(new_commit_id);
+	free(id_str);
+	if (branch_ref)
+		got_ref_close(branch_ref);
+	if (head_ref)
+		got_ref_close(head_ref);
 	return error;
 }
 
@@ -2759,43 +3069,7 @@ usage_commit(void)
 	fprintf(stderr, "usage: %s commit [-m msg] file-path\n", getprogname());
 	exit(1);
 }
-
-int
-spawn_editor(const char *editor, const char *file)
-{
-	pid_t pid;
-	sig_t sighup, sigint, sigquit;
-	int st = -1;
 
-	sighup = signal(SIGHUP, SIG_IGN);
-	sigint = signal(SIGINT, SIG_IGN);
-	sigquit = signal(SIGQUIT, SIG_IGN);
-
-	switch (pid = fork()) {
-	case -1:
-		goto doneediting;
-	case 0:
-		execl(editor, editor, file, (char *)NULL);
-		_exit(127);
-	}
-
-	while (waitpid(pid, &st, 0) == -1)
-		if (errno != EINTR)
-			break;
-
-doneediting:
-	(void)signal(SIGHUP, sighup);
-	(void)signal(SIGINT, sigint);
-	(void)signal(SIGQUIT, sigquit);
-
-	if (!WIFEXITED(st)) {
-		errno = EINTR;
-		return -1;
-	}
-
-	return WEXITSTATUS(st);
-}
-
 struct collect_commit_logmsg_arg {
 	const char *cmdline_log;
 	const char *editor;
@@ -2815,11 +3089,8 @@ collect_commit_logmsg(struct got_pathlist_head *commit
 	const struct got_error *err = NULL;
 	char *template = NULL;
 	struct collect_commit_logmsg_arg *a = arg;
-	char buf[1024];
-	struct stat st, st2;
-	FILE *fp;
+	int fd;
 	size_t len;
-	int fd, content_changed = 0;
 
 	/* if a message was specified on the command line, just use it */
 	if (a->cmdline_log != NULL && strlen(a->cmdline_log) != 0) {
@@ -2853,72 +3124,21 @@ collect_commit_logmsg(struct got_pathlist_head *commit
 	}
 	close(fd);
 
-	if (stat(a->logmsg_path, &st) == -1) {
-		err = got_error_from_errno2("stat", a->logmsg_path);
-		goto done;
-	}
-
-	if (spawn_editor(a->editor, a->logmsg_path) == -1) {
-		err = got_error_from_errno("failed spawning editor");
-		goto done;
-	}
-
-	if (stat(a->logmsg_path, &st2) == -1) {
-		err = got_error_from_errno("stat");
-		goto done;
-	}
-
-	if (st.st_mtime == st2.st_mtime && st.st_size == st2.st_size) {
-		unlink(a->logmsg_path);
-		free(a->logmsg_path);
-		a->logmsg_path = NULL;
-		err = got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY,
-		    "no changes made to commit message, aborting");
-		goto done;
-	}
-
-	*logmsg = malloc(st2.st_size + 1);
-	if (*logmsg == NULL) {
-		err = got_error_from_errno("malloc");
-		goto done;
-	}
-	(*logmsg)[0] = '\0';
-	len = 0;
-
-	fp = fopen(a->logmsg_path, "r");
-	if (fp == NULL) {
-		err = got_error_from_errno("fopen");
-		goto done;
-	}
-	while (fgets(buf, sizeof(buf), fp) != NULL) {
-		if (!content_changed && strcmp(buf, initial_content) != 0)
-			content_changed = 1;
-		if (buf[0] == '#' || (len == 0 && buf[0] == '\n'))
-			continue; /* remove comments and leading empty lines */
-		len = strlcat(*logmsg, buf, st2.st_size);
-	}
-	fclose(fp);
-
-	while (len > 0 && (*logmsg)[len - 1] == '\n') {
-		(*logmsg)[len - 1] = '\0';
-		len--;
-	}
-
-	if (len == 0 || !content_changed) {
-		unlink(a->logmsg_path);
-		free(a->logmsg_path);
-		a->logmsg_path = NULL;
-		err = got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY,
-		    "commit message cannot be empty, aborting");
-		goto done;
-	}
+	err = edit_logmsg(logmsg, a->editor, a->logmsg_path, initial_content);
 done:
+	unlink(a->logmsg_path);
+	free(a->logmsg_path);
 	free(initial_content);
 	free(template);
 
 	/* Editor is done; we can now apply unveil(2) */
-	if (err == NULL)
+	if (err == NULL) {
 		err = apply_unveil(a->repo_path, 0, a->worktree_path, 0);
+		if (err) {
+			free(*logmsg);
+			*logmsg = NULL;
+		}
+	}
 	return err;
 }
 
blob - d4ac915a022f60064bf883eaa2bc5b4bb7355611
blob + 6b8435a8f69b24786fa17e77464465e2339529e4
--- include/got_repository.h
+++ include/got_repository.h
@@ -15,6 +15,7 @@
  */
 
 struct got_repository;
+struct got_pathlist_head;
 
 /* Open and close repositories. */
 const struct got_error *got_repo_open(struct got_repository**, const char *);
@@ -61,3 +62,14 @@ const struct got_error *got_repo_init(const char *);
 /* Attempt to find a unique object ID for a given ID string prefix. */
 const struct got_error *got_repo_match_object_id_prefix(struct got_object_id **,
     const char *, int, struct got_repository *);
+
+/* A callback function which is invoked when a path is imported. */
+typedef const struct got_error *(*got_repo_import_cb)(void *, const char *);
+
+/*
+ * Import an unversioned directory tree into the repository.
+ * Creates a root commit, i.e. a commit with zero parents.
+ */
+const struct got_error *got_repo_import(struct got_object_id **, const char *,
+    const char *, const char *, struct got_pathlist_head *,
+    struct got_repository *, got_repo_import_cb, void *);
blob - feb2dcacbe2d9ef61576ea6b746e5588d5a1d92a
blob + 867b2d236255af662c58302c5e1d984d299dbc85
--- lib/object_create.c
+++ lib/object_create.c
@@ -392,28 +392,30 @@ got_object_commit_create(struct got_object_id **id,
 		goto done;
 	}
 
-	SIMPLEQ_FOREACH(qid, parent_ids, entry) {
-		char *parent_str = NULL;
+	if (parent_ids) {
+		SIMPLEQ_FOREACH(qid, parent_ids, entry) {
+			char *parent_str = NULL;
 
-		free(id_str);
+			free(id_str);
 
-		err = got_object_id_str(&id_str, qid->id);
-		if (err)
-			goto done;
-		if (asprintf(&parent_str, "%s%s\n", GOT_COMMIT_LABEL_PARENT,
-		    id_str) == -1) {
-			err = got_error_from_errno("asprintf");
-			goto done;
-		}
-		len = strlen(parent_str);
-		SHA1Update(&sha1_ctx, parent_str, len);
-		n = fwrite(parent_str, 1, len, commitfile);
-		if (n != len) {
-			err = got_ferror(commitfile, GOT_ERR_IO);
+			err = got_object_id_str(&id_str, qid->id);
+			if (err)
+				goto done;
+			if (asprintf(&parent_str, "%s%s\n",
+			    GOT_COMMIT_LABEL_PARENT, id_str) == -1) {
+				err = got_error_from_errno("asprintf");
+				goto done;
+			}
+			len = strlen(parent_str);
+			SHA1Update(&sha1_ctx, parent_str, len);
+			n = fwrite(parent_str, 1, len, commitfile);
+			if (n != len) {
+				err = got_ferror(commitfile, GOT_ERR_IO);
+				free(parent_str);
+				goto done;
+			}
 			free(parent_str);
-			goto done;
 		}
-		free(parent_str);
 	}
 
 	len = strlen(author_str);
blob - c8076c6355d6683f3fadaa770be0345c15825f48
blob + 39c436e7f663ea304f8d339f4eebc4f646156e15
--- lib/repository.c
+++ lib/repository.c
@@ -23,6 +23,7 @@
 
 #include <ctype.h>
 #include <fcntl.h>
+#include <fnmatch.h>
 #include <limits.h>
 #include <dirent.h>
 #include <stdlib.h>
@@ -46,6 +47,8 @@
 #include "got_lib_delta.h"
 #include "got_lib_inflate.h"
 #include "got_lib_object.h"
+#include "got_lib_object_parse.h"
+#include "got_lib_object_create.h"
 #include "got_lib_pack.h"
 #include "got_lib_privsep.h"
 #include "got_lib_worktree.h"
@@ -1109,3 +1112,223 @@ done:
 
 	return err;
 }
+
+static const struct got_error *
+alloc_added_blob_tree_entry(struct got_tree_entry **new_te,
+    const char *name, mode_t mode, struct got_object_id *blob_id)
+{
+	const struct got_error *err = NULL;
+
+	 *new_te = NULL;
+
+	*new_te = calloc(1, sizeof(**new_te));
+	if (*new_te == NULL)
+		return got_error_from_errno("calloc");
+
+	(*new_te)->name = strdup(name);
+	if ((*new_te)->name == NULL) {
+		err = got_error_from_errno("strdup");
+		goto done;
+	}
+
+	(*new_te)->mode = S_IFREG | (mode & ((S_IRWXU | S_IRWXG | S_IRWXO)));
+	(*new_te)->id = blob_id;
+done:
+	if (err && *new_te) {
+		got_object_tree_entry_close(*new_te);
+		*new_te = NULL;
+	}
+	return err;
+}
+
+static const struct got_error *
+import_file(struct got_tree_entry **new_te, struct dirent *de,
+    const char *path, struct got_repository *repo)
+{
+	const struct got_error *err;
+	struct got_object_id *blob_id = NULL;
+	char *filepath;
+	struct stat sb;
+
+	if (asprintf(&filepath, "%s%s%s", path,
+	    path[0] == '\0' ? "" : "/", de->d_name) == -1)
+		return got_error_from_errno("asprintf");
+
+	if (lstat(filepath, &sb) != 0) {
+		err = got_error_from_errno2("lstat", path);
+		goto done;
+	}
+
+	err = got_object_blob_create(&blob_id, filepath, repo);
+	if (err)
+		goto done;
+
+	err = alloc_added_blob_tree_entry(new_te, de->d_name, sb.st_mode,
+	    blob_id);
+done:
+	free(filepath);
+	if (err)
+		free(blob_id);
+	return err;
+}
+
+static const struct got_error *
+insert_tree_entry(struct got_tree_entry *new_te,
+    struct got_pathlist_head *paths)
+{
+	const struct got_error *err = NULL;
+	struct got_pathlist_entry *new_pe;
+
+	err = got_pathlist_insert(&new_pe, paths, new_te->name, new_te);
+	if (err)
+		return err;
+	if (new_pe == NULL)
+		return got_error(GOT_ERR_TREE_DUP_ENTRY);
+	return NULL;
+}
+
+static const struct got_error *write_tree(struct got_object_id **,
+    const char *, struct got_pathlist_head *, struct got_repository *,
+    got_repo_import_cb progress_cb, void *progress_arg);
+
+static const struct got_error *
+import_subdir(struct got_tree_entry **new_te, struct dirent *de,
+    const char *path, struct got_pathlist_head *ignores,
+    struct got_repository *repo,
+    got_repo_import_cb progress_cb, void *progress_arg)
+{
+	const struct got_error *err;
+	char *subdirpath;
+
+	if (asprintf(&subdirpath, "%s%s%s", path,
+	    path[0] == '\0' ? "" : "/", de->d_name) == -1)
+		return got_error_from_errno("asprintf");
+
+	(*new_te) = calloc(1, sizeof(**new_te));
+	(*new_te)->mode = S_IFDIR;
+	(*new_te)->name = strdup(de->d_name);
+	if ((*new_te)->name == NULL) {
+		err = got_error_from_errno("strdup");
+		goto done;
+	}
+
+	err = write_tree(&(*new_te)->id, subdirpath, ignores,  repo,
+	    progress_cb, progress_arg);
+done:
+	free(subdirpath);
+	if (err) {
+		got_object_tree_entry_close(*new_te);
+		*new_te = NULL;
+	}
+	return err;
+}
+
+static const struct got_error *
+write_tree(struct got_object_id **new_tree_id, const char *path_dir,
+    struct got_pathlist_head *ignores, struct got_repository *repo,
+    got_repo_import_cb progress_cb, void *progress_arg)
+{
+	const struct got_error *err = NULL;
+	DIR *dir;
+	struct dirent *de;
+	struct got_tree_entries new_tree_entries;
+	struct got_tree_entry *new_te = NULL;
+	struct got_pathlist_head paths;
+	struct got_pathlist_entry *pe;
+	char *name;
+
+	*new_tree_id = NULL;
+
+	TAILQ_INIT(&paths);
+	new_tree_entries.nentries = 0;
+	SIMPLEQ_INIT(&new_tree_entries.head);
+
+	dir = opendir(path_dir);
+	if (dir == NULL) {
+		err = got_error_from_errno2("opendir", path_dir);
+		goto done;
+	}
+
+	while ((de = readdir(dir)) != NULL) {
+		int ignore = 0;
+
+		if (strcmp(de->d_name, ".") == 0 ||
+		    strcmp(de->d_name, "..") == 0)
+			continue;
+
+		TAILQ_FOREACH(pe, ignores, entry) {
+			if (fnmatch(pe->path, de->d_name, 0) == 0) {
+				ignore = 1;
+				break;
+			}
+		}
+		if (ignore)
+			continue;
+		if (de->d_type == DT_DIR) {
+			err = import_subdir(&new_te, de, path_dir,
+			    ignores, repo, progress_cb, progress_arg);
+			if (err)
+				goto done;
+		} else if (de->d_type == DT_REG) {
+			err = import_file(&new_te, de, path_dir, repo);
+			if (err)
+				goto done;
+		} else
+			continue;
+
+		err = insert_tree_entry(new_te, &paths);
+		if (err)
+			goto done;
+	}
+
+	TAILQ_FOREACH(pe, &paths, entry) {
+		struct got_tree_entry *te = pe->data;
+		char *path;
+		new_tree_entries.nentries++;
+		SIMPLEQ_INSERT_TAIL(&new_tree_entries.head, te, entry);
+		if (!S_ISREG(te->mode))
+			continue;
+		if (asprintf(&path, "%s/%s", path_dir, pe->path) == -1) {
+			err = got_error_from_errno("asprintf");
+			goto done;
+		}
+		err = (*progress_cb)(progress_arg, path);
+		free(path);
+		if (err)
+			goto done;
+	}
+
+	name = basename(path_dir);
+	if (name == NULL) {
+		err = got_error_from_errno("basename");
+		goto done;
+	}
+
+	err = got_object_tree_create(new_tree_id, &new_tree_entries, repo);
+done:
+	if (dir)
+		closedir(dir);
+	got_object_tree_entries_close(&new_tree_entries);
+	got_pathlist_free(&paths);
+	return err;
+}
+
+const struct got_error *
+got_repo_import(struct got_object_id **new_commit_id, const char *path_dir,
+    const char *logmsg, const char *author, struct got_pathlist_head *ignores,
+    struct got_repository *repo, got_repo_import_cb progress_cb,
+    void *progress_arg)
+{
+	const struct got_error *err;
+	struct got_object_id *new_tree_id;
+
+	err = write_tree(&new_tree_id, path_dir, ignores, repo,
+	    progress_cb, progress_arg);
+	if (err)
+		return err;
+
+	err = got_object_commit_create(new_commit_id, new_tree_id, NULL, 0,
+	    author, time(NULL), author, time(NULL), logmsg, repo);
+	free(new_tree_id);
+	return err;
+}
blob - cc53c4052b7de555d29b4d516d502908cf15873f
blob + 682c9e1542649798fd47bc71d7532329397d40f9
--- regress/cmdline/Makefile
+++ regress/cmdline/Makefile
@@ -1,5 +1,5 @@
 REGRESS_TARGETS=checkout update status log add rm diff commit \
-	cherrypick backout rebase
+	cherrypick backout rebase import
 NOOBJ=Yes
 
 checkout:
@@ -35,4 +35,7 @@ backout:
 rebase:
 	./rebase.sh
 
+import:
+	./import.sh
+
 .include <bsd.regress.mk>
blob - 793da6f9f59ddd31762b67fadea5b2769de756cf
blob + 4b6b06494055b3a907d857c1b28fb33f12c57ada
--- regress/cmdline/common.sh
+++ regress/cmdline/common.sh
@@ -88,9 +88,17 @@ function make_test_tree
 	echo delta > $repo/gamma/delta
 	mkdir $repo/epsilon
 	echo zeta > $repo/epsilon/zeta
-	(cd $repo && git add .)
 }
 
+function get_blob_id
+{
+	repo="$1"
+	tree_path="$2"
+	filename="$3"
+
+	got tree -r $repo -i $tree_path | grep ${filename}$ | cut -d' ' -f 1
+}
+
 function test_init
 {
 	local testname="$1"
@@ -104,6 +112,7 @@ function test_init
 	git_init $testroot/repo
 	if [ -z "$no_tree" ]; then
 		make_test_tree $testroot/repo
+		(cd $repo && git add .)
 		git_commit $testroot/repo -m "adding the test tree"
 	fi
 	echo "$testroot"
blob - /dev/null
blob + bf8a0681d0998fa6be6c82f3b8fb3dde90f1cbba (mode 755)
--- /dev/null
+++ regress/cmdline/import.sh
@@ -0,0 +1,201 @@
+#!/bin/sh
+#
+# Copyright (c) 2019 Stefan Sperling <stsp@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+. ./common.sh
+
+function test_import_basic {
+	local testroot=`mktemp -p /tmp -d got-test-$testname-XXXXXXXX`
+
+	got init $testroot/repo
+
+	mkdir $testroot/tree
+	make_test_tree $testroot/tree
+
+	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/gamma/delta" > $testroot/stdout.expected
+	echo "A  $testroot/tree/epsilon/zeta" >> $testroot/stdout.expected
+	echo "A  $testroot/tree/alpha" >> $testroot/stdout.expected
+	echo "A  $testroot/tree/beta" >> $testroot/stdout.expected
+	echo "Created branch refs/heads/master 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
+
+	(cd $testroot/repo && got log -p | grep -v ^date: > $testroot/stdout)
+
+	id_alpha=`get_blob_id $testroot/repo "" alpha`
+	id_beta=`get_blob_id $testroot/repo "" beta`
+	id_zeta=`get_blob_id $testroot/repo epsilon zeta`
+	id_delta=`get_blob_id $testroot/repo gamma delta`
+
+	echo "-----------------------------------------------" \
+		> $testroot/stdout.expected
+	echo "commit $head_commit (master)" >> $testroot/stdout.expected
+	echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+	echo " " >> $testroot/stdout.expected
+	echo " init" >> $testroot/stdout.expected
+	echo " " >> $testroot/stdout.expected
+	echo "diff /dev/null $head_commit" >> $testroot/stdout.expected
+	echo "blob - /dev/null" >> $testroot/stdout.expected
+	echo "blob + $id_alpha" >> $testroot/stdout.expected
+	echo "--- /dev/null" >> $testroot/stdout.expected
+	echo "+++ alpha" >> $testroot/stdout.expected
+	echo "@@ -0,0 +1 @@" >> $testroot/stdout.expected
+	echo "+alpha" >> $testroot/stdout.expected
+	echo "blob - /dev/null" >> $testroot/stdout.expected
+	echo "blob + $id_beta" >> $testroot/stdout.expected
+	echo "--- /dev/null" >> $testroot/stdout.expected
+	echo "+++ beta" >> $testroot/stdout.expected
+	echo "@@ -0,0 +1 @@" >> $testroot/stdout.expected
+	echo "+beta" >> $testroot/stdout.expected
+	echo "blob - /dev/null" >> $testroot/stdout.expected
+	echo "blob + $id_zeta" >> $testroot/stdout.expected
+	echo "--- /dev/null" >> $testroot/stdout.expected
+	echo "+++ epsilon/zeta" >> $testroot/stdout.expected
+	echo "@@ -0,0 +1 @@" >> $testroot/stdout.expected
+	echo "+zeta" >> $testroot/stdout.expected
+	echo "blob - /dev/null" >> $testroot/stdout.expected
+	echo "blob + $id_delta" >> $testroot/stdout.expected
+	echo "--- /dev/null" >> $testroot/stdout.expected
+	echo "+++ gamma/delta" >> $testroot/stdout.expected
+	echo "@@ -0,0 +1 @@" >> $testroot/stdout.expected
+	echo "+delta" >> $testroot/stdout.expected
+	echo "" >> $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
+
+	echo "A  $testroot/wt/alpha" > $testroot/stdout.expected
+	echo "A  $testroot/wt/beta" >> $testroot/stdout.expected
+	echo "A  $testroot/wt/epsilon/zeta" >> $testroot/stdout.expected
+	echo "A  $testroot/wt/gamma/delta" >> $testroot/stdout.expected
+	echo "Now shut up and hack" >> $testroot/stdout.expected
+
+	got checkout $testroot/repo $testroot/wt > $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cmp -s $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 "alpha" > $testroot/content.expected
+	echo "beta" >> $testroot/content.expected
+	echo "zeta" >> $testroot/content.expected
+	echo "delta" >> $testroot/content.expected
+	cat $testroot/wt/alpha $testroot/wt/beta $testroot/wt/epsilon/zeta \
+	    $testroot/wt/gamma/delta > $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_import_requires_new_branch {
+	local testroot=`test_init import_requires_new_branch`
+
+	mkdir $testroot/tree
+	make_test_tree $testroot/tree
+
+	got import -m 'init' -r $testroot/repo $testroot/tree \
+		> $testroot/stdout 2> $testroot/stderr
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "import command should have failed but did not"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "got: import target branch already exists" \
+		> $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
+
+	got import -b newbranch -m 'init' -r $testroot/repo $testroot/tree  \
+		> $testroot/stdout
+	ret="$?"
+	test_done "$testroot" "$ret"
+
+}
+
+function test_import_ignores {
+	local testroot=`mktemp -p /tmp -d got-test-$testname-XXXXXXXX`
+
+	got init $testroot/repo
+
+	mkdir $testroot/tree
+	make_test_tree $testroot/tree
+
+	got import -I alpha -I '*lta*' -I '*silon' \
+		-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/beta" >> $testroot/stdout.expected
+	echo "Created branch refs/heads/master 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
+	fi
+	test_done "$testroot" "$ret"
+
+
+}
+
+run_test test_import_basic
+run_test test_import_requires_new_branch
+run_test test_import_ignores