Commit b2be62fd23a9e39ce32725139bdeecf7e10aa2ac

Vicent Martí 2013-08-16T15:33:13

Merge pull request #1790 from libgit2/examples-init Add "git init"-like example

diff --git a/examples/Makefile b/examples/Makefile
index 95e46f0..d53ed82 100644
--- a/examples/Makefile
+++ b/examples/Makefile
@@ -3,7 +3,7 @@
 CC = gcc
 CFLAGS = -g -I../include -I../src -Wall -Wextra -Wmissing-prototypes -Wno-missing-field-initializers
 LFLAGS = -L../build -lgit2 -lz
-APPS = general showindex diff rev-list cat-file status log rev-parse
+APPS = general showindex diff rev-list cat-file status log rev-parse init
 
 all: $(APPS)
 
diff --git a/examples/init.c b/examples/init.c
new file mode 100644
index 0000000..4a379c6
--- /dev/null
+++ b/examples/init.c
@@ -0,0 +1,245 @@
+/*
+ * This is a sample program that is similar to "git init".  See the
+ * documentation for that (try "git help init") to understand what this
+ * program is emulating.
+ *
+ * This demonstrates using the libgit2 APIs to initialize a new repository.
+ *
+ * This also contains a special additional option that regular "git init"
+ * does not support which is "--initial-commit" to make a first empty commit.
+ * That is demonstrated in the "create_initial_commit" helper function.
+ *
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#include <stdio.h>
+#include <git2.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+/* not actually good error handling */
+static void fail(const char *msg, const char *arg)
+{
+	if (arg)
+		fprintf(stderr, "%s %s\n", msg, arg);
+	else
+		fprintf(stderr, "%s\n", msg);
+	exit(1);
+}
+
+static void usage(const char *error, const char *arg)
+{
+	fprintf(stderr, "error: %s '%s'\n", error, arg);
+	fprintf(stderr, "usage: init [-q | --quiet] [--bare] "
+			"[--template=<dir>] [--shared[=perms]] <directory>\n");
+	exit(1);
+}
+
+/* simple string prefix test used in argument parsing */
+static size_t is_prefixed(const char *arg, const char *pfx)
+{
+	size_t len = strlen(pfx);
+	return !strncmp(arg, pfx, len) ? len : 0;
+}
+
+/* parse the tail of the --shared= argument */
+static uint32_t parse_shared(const char *shared)
+{
+	if (!strcmp(shared, "false") || !strcmp(shared, "umask"))
+		return GIT_REPOSITORY_INIT_SHARED_UMASK;
+
+	else if (!strcmp(shared, "true") || !strcmp(shared, "group"))
+		return GIT_REPOSITORY_INIT_SHARED_GROUP;
+
+	else if (!strcmp(shared, "all") || !strcmp(shared, "world") ||
+			 !strcmp(shared, "everybody"))
+		return GIT_REPOSITORY_INIT_SHARED_ALL;
+
+	else if (shared[0] == '0') {
+		long val;
+		char *end = NULL;
+		val = strtol(shared + 1, &end, 8);
+		if (end == shared + 1 || *end != 0)
+			usage("invalid octal value for --shared", shared);
+		return (uint32_t)val;
+	}
+
+	else
+		usage("unknown value for --shared", shared);
+
+	return 0;
+}
+
+/* forward declaration of helper to make an empty parent-less commit */
+static void create_initial_commit(git_repository *repo);
+
+
+int main(int argc, char *argv[])
+{
+	git_repository *repo = NULL;
+	int no_options = 1, quiet = 0, bare = 0, initial_commit = 0, i;
+	uint32_t shared = GIT_REPOSITORY_INIT_SHARED_UMASK;
+	const char *template = NULL, *gitdir = NULL, *dir = NULL;
+	size_t pfxlen;
+
+	git_threads_init();
+
+	/* Process arguments */
+
+	for (i = 1; i < argc; ++i) {
+		char *a = argv[i];
+
+		if (a[0] == '-')
+			no_options = 0;
+
+		if (a[0] != '-') {
+			if (dir != NULL)
+				usage("extra argument", a);
+			dir = a;
+		}
+		else if (!strcmp(a, "-q") || !strcmp(a, "--quiet"))
+			quiet = 1;
+		else if (!strcmp(a, "--bare"))
+			bare = 1;
+		else if ((pfxlen = is_prefixed(a, "--template=")) > 0)
+			template = a + pfxlen;
+		else if (!strcmp(a, "--separate-git-dir"))
+			gitdir = argv[++i];
+		else if ((pfxlen = is_prefixed(a, "--separate-git-dir=")) > 0)
+			gitdir = a + pfxlen;
+		else if (!strcmp(a, "--shared"))
+			shared = GIT_REPOSITORY_INIT_SHARED_GROUP;
+		else if ((pfxlen = is_prefixed(a, "--shared=")) > 0)
+			shared = parse_shared(a + pfxlen);
+		else if (!strcmp(a, "--initial-commit"))
+			initial_commit = 1;
+		else
+			usage("unknown option", a);
+	}
+
+	if (!dir)
+		usage("must specify directory to init", NULL);
+
+	/* Initialize repository */
+
+	if (no_options) {
+		/* No options were specified, so let's demonstrate the default
+		 * simple case of git_repository_init() API usage...
+		 */
+
+		if (git_repository_init(&repo, dir, 0) < 0)
+			fail("Could not initialize repository", dir);
+	}
+	else {
+		/* Some command line options were specified, so we'll use the
+		 * extended init API to handle them
+		 */
+		git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;
+
+		if (bare)
+			opts.flags |= GIT_REPOSITORY_INIT_BARE;
+
+		if (template) {
+			opts.flags |= GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE;
+			opts.template_path = template;
+		}
+
+		if (gitdir) {
+			/* if you specified a separate git directory, then initialize
+			 * the repository at that path and use the second path as the
+			 * working directory of the repository (with a git-link file)
+			 */
+			opts.workdir_path = dir;
+			dir = gitdir;
+		}
+
+		if (shared != 0)
+			opts.mode = shared;
+
+		if (git_repository_init_ext(&repo, dir, &opts) < 0)
+			fail("Could not initialize repository", dir);
+	}
+
+	/* Print a message to stdout like "git init" does */
+
+	if (!quiet) {
+		if (bare || gitdir)
+			dir = git_repository_path(repo);
+		else
+			dir = git_repository_workdir(repo);
+
+		printf("Initialized empty Git repository in %s\n", dir);
+	}
+
+	/* As an extension to the basic "git init" command, this example
+	 * gives the option to create an empty initial commit.  This is
+	 * mostly to demonstrate what it takes to do that, but also some
+	 * people like to have that empty base commit in their repo.
+	 */
+	if (initial_commit) {
+		create_initial_commit(repo);
+		printf("Created empty initial commit\n");
+	}
+
+	git_repository_free(repo);
+	git_threads_shutdown();
+
+	return 0;
+}
+
+/* Unlike regular "git init", this example shows how to create an initial
+ * empty commit in the repository.  This is the helper function that does
+ * that.
+ */
+static void create_initial_commit(git_repository *repo)
+{
+	git_signature *sig;
+	git_index *index;
+	git_oid tree_id, commit_id;
+	git_tree *tree;
+
+	/* First use the config to initialize a commit signature for the user */
+
+	if (git_signature_default(&sig, repo) < 0)
+		fail("Unable to create a commit signature.",
+			 "Perhaps 'user.name' and 'user.email' are not set");
+
+	/* Now let's create an empty tree for this commit */
+
+	if (git_repository_index(&index, repo) < 0)
+		fail("Could not open repository index", NULL);
+
+	/* Outside of this example, you could call git_index_add_bypath()
+	 * here to put actual files into the index.  For our purposes, we'll
+	 * leave it empty for now.
+	 */
+
+	if (git_index_write_tree(&tree_id, index) < 0)
+		fail("Unable to write initial tree from index", NULL);
+
+	git_index_free(index);
+
+	if (git_tree_lookup(&tree, repo, &tree_id) < 0)
+		fail("Could not look up initial tree", NULL);
+
+	/* Ready to create the initial commit
+	 *
+	 * Normally creating a commit would involve looking up the current
+	 * HEAD commit and making that be the parent of the initial commit,
+	 * but here this is the first commit so there will be no parent.
+	 */
+
+	if (git_commit_create_v(
+			&commit_id, repo, "HEAD", sig, sig,
+			NULL, "Initial commit", tree, 0) < 0)
+		fail("Could not create the initial commit", NULL);
+
+	/* Clean up so we don't leak memory */
+
+	git_tree_free(tree);
+	git_signature_free(sig);
+}
diff --git a/include/git2/signature.h b/include/git2/signature.h
index 00d19de..2fa46d0 100644
--- a/include/git2/signature.h
+++ b/include/git2/signature.h
@@ -48,6 +48,19 @@ GIT_EXTERN(int) git_signature_new(git_signature **out, const char *name, const c
  */
 GIT_EXTERN(int) git_signature_now(git_signature **out, const char *name, const char *email);
 
+/**
+ * Create a new action signature with default user and now timestamp.
+ *
+ * This looks up the user.name and user.email from the configuration and
+ * uses the current time as the timestamp, and creates a new signature
+ * based on that information.  It will return GIT_ENOTFOUND if either the
+ * user.name or user.email are not set.
+ *
+ * @param out new signature
+ * @param repo repository pointer
+ * @return 0 on success, GIT_ENOTFOUND if config is missing, or error code
+ */
+GIT_EXTERN(int) git_signature_default(git_signature **out, git_repository *repo);
 
 /**
  * Create a copy of an existing signature.  All internal strings are also
diff --git a/src/signature.c b/src/signature.c
index 0a34ccf..52ca2b3 100644
--- a/src/signature.c
+++ b/src/signature.c
@@ -74,7 +74,7 @@ int git_signature_new(git_signature **sig_out, const char *name, const char *ema
 		git_signature_free(p);
 		return signature_error("Signature cannot have an empty name");
 	}
-		
+
 	p->when.time = time;
 	p->when.offset = offset;
 
@@ -129,6 +129,23 @@ int git_signature_now(git_signature **sig_out, const char *name, const char *ema
 	return 0;
 }
 
+int git_signature_default(git_signature **out, git_repository *repo)
+{
+	int error;
+	git_config *cfg;
+	const char *user_name, *user_email;
+
+	if ((error = git_repository_config(&cfg, repo)) < 0)
+		return error;
+
+	if (!(error = git_config_get_string(&user_name, cfg, "user.name")) &&
+		!(error = git_config_get_string(&user_email, cfg, "user.email")))
+		error = git_signature_now(out, user_name, user_email);
+
+	git_config_free(cfg);
+	return error;
+}
+
 int git_signature__parse(git_signature *sig, const char **buffer_out,
 		const char *buffer_end, const char *header, char ender)
 {
diff --git a/tests-clar/repo/init.c b/tests-clar/repo/init.c
index 8cf7379..5076184 100644
--- a/tests-clar/repo/init.c
+++ b/tests-clar/repo/init.c
@@ -530,3 +530,63 @@ void test_repo_init__can_reinit_an_initialized_repository(void)
 
 	git_repository_free(reinit);
 }
+
+void test_repo_init__init_with_initial_commit(void)
+{
+	git_index *index;
+
+	cl_set_cleanup(&cleanup_repository, "committed");
+
+	/* Initialize the repository */
+	cl_git_pass(git_repository_init(&_repo, "committed", 0));
+
+	/* Init will be automatically created when requested for a new repo */
+	cl_git_pass(git_repository_index(&index, _repo));
+
+	/* Create a file so we can commit it
+	 *
+	 * If you are writing code outside the test suite, you can create this
+	 * file any way that you like, such as:
+	 *      FILE *fp = fopen("committed/file.txt", "w");
+	 *      fputs("some stuff\n", fp);
+	 *      fclose(fp);
+	 * We like to use the help functions because they do error detection
+	 * in a way that's easily compatible with our test suite.
+	 */
+	cl_git_mkfile("committed/file.txt", "some stuff\n");
+
+	/* Add file to the index */
+	cl_git_pass(git_index_add_bypath(index, "file.txt"));
+	cl_git_pass(git_index_write(index));
+
+	/* Make sure we're ready to use git_signature_default :-) */
+	{
+		git_config *cfg, *local;
+		cl_git_pass(git_repository_config(&cfg, _repo));
+		cl_git_pass(git_config_open_level(&local, cfg, GIT_CONFIG_LEVEL_LOCAL));
+		cl_git_pass(git_config_set_string(local, "user.name", "Test User"));
+		cl_git_pass(git_config_set_string(local, "user.email", "t@example.com"));
+		git_config_free(local);
+		git_config_free(cfg);
+	}
+
+	/* Create a commit with the new contents of the index */
+	{
+		git_signature *sig;
+		git_oid tree_id, commit_id;
+		git_tree *tree;
+
+		cl_git_pass(git_signature_default(&sig, _repo));
+		cl_git_pass(git_index_write_tree(&tree_id, index));
+		cl_git_pass(git_tree_lookup(&tree, _repo, &tree_id));
+
+		cl_git_pass(git_commit_create_v(
+			&commit_id, _repo, "HEAD", sig, sig,
+			NULL, "First", tree, 0));
+
+		git_tree_free(tree);
+		git_signature_free(sig);
+	}
+
+	git_index_free(index);
+}