[hacktree: 4/4] Add probably working commits



commit 98a043c67113fdda0190f5170e881dd4255f9c05
Author: Colin Walters <walters verbum org>
Date:   Thu Oct 13 17:11:01 2011 -0400

    Add probably working commits

 Makefile-hacktree.am            |    1 +
 TODO                            |    4 +-
 configure.ac                    |   13 +
 src/ht-builtin-commit.c         |  115 +++++
 src/ht-builtin-fsck.c           |    1 -
 src/ht-builtins.h               |    1 +
 src/libhacktree/hacktree-core.c |   86 +++-
 src/libhacktree/hacktree-core.h |   50 ++-
 src/libhacktree/hacktree-repo.c |  926 ++++++++++++++++++++++++++++++++++++++-
 src/libhacktree/hacktree-repo.h |   10 +
 src/libhtutil/ht-gio-utils.c    |   51 +++-
 src/libhtutil/ht-gio-utils.h    |    8 +-
 src/libhtutil/ht-unix-utils.c   |  119 +++++
 src/libhtutil/ht-unix-utils.h   |   12 +
 src/main.c                      |    1 +
 tests/t0002-commit-one.sh       |   38 ++
 16 files changed, 1394 insertions(+), 42 deletions(-)
---
diff --git a/Makefile-hacktree.am b/Makefile-hacktree.am
index 6b25cdc..b62081b 100644
--- a/Makefile-hacktree.am
+++ b/Makefile-hacktree.am
@@ -46,6 +46,7 @@ bin_PROGRAMS += hacktree
 
 hacktree_SOURCES = src/main.c \
 	src/ht-builtins.h \
+	src/ht-builtin-commit.c \
 	src/ht-builtin-init.c \
 	src/ht-builtin-link-file.c \
 	src/ht-builtin-fsck.c \
diff --git a/TODO b/TODO
index aeff7f7..dd4bc1d 100644
--- a/TODO
+++ b/TODO
@@ -1,5 +1,5 @@
-* hacktree import-tar
-  - Accepts tarball on stdin, stores the objects
 * tree and commit objects
 * hacktree import
+* hacktree import-tar
+  - Accepts tarball on stdin, stores the objects
 * branches
diff --git a/configure.ac b/configure.ac
index 49b3fa6..9bb5792 100644
--- a/configure.ac
+++ b/configure.ac
@@ -11,6 +11,19 @@ AM_SILENT_RULES([yes])
 AC_PROG_CC
 AM_PROG_CC_C_O
 
+changequote(,)dnl
+if test "x$GCC" = "xyes"; then
+  case " $CFLAGS " in
+  *[\ \	]-Wall[\ \	]*) ;;
+  *) CFLAGS="$CFLAGS -Wall" ;;
+  esac
+  case " $CFLAGS " in
+  *[\ \	]-Wmissing-prototypes[\ \	]*) ;;
+  *) CFLAGS="$CFLAGS -Wmissing-prototypes" ;;
+  esac
+fi
+changequote([,])dnl
+
 # Initialize libtool
 LT_PREREQ([2.2])
 LT_INIT
diff --git a/src/ht-builtin-commit.c b/src/ht-builtin-commit.c
new file mode 100644
index 0000000..1273f65
--- /dev/null
+++ b/src/ht-builtin-commit.c
@@ -0,0 +1,115 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2011 Colin Walters <walters verbum org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Author: Colin Walters <walters verbum org>
+ */
+
+#include "config.h"
+
+#include "ht-builtins.h"
+#include "hacktree.h"
+
+#include <glib/gi18n.h>
+
+static char *repo_path;
+static char *subject;
+static char *body;
+static char **additions;
+static char **removals;
+
+static GOptionEntry options[] = {
+  { "repo", 0, 0, G_OPTION_ARG_FILENAME, &repo_path, "Repository path", "repo" },
+  { "subject", 's', 0, G_OPTION_ARG_STRING, &subject, "One line subject", "subject" },
+  { "body", 'b', 0, G_OPTION_ARG_STRING, &body, "Full description", "body" },
+  { "add", 'a', 0, G_OPTION_ARG_FILENAME_ARRAY, &additions, "Relative file path to add", "filename" },
+  { "remove", 'r', 0, G_OPTION_ARG_FILENAME_ARRAY, &removals, "Relative file path to remove", "filename" },
+  { NULL }
+};
+
+gboolean
+hacktree_builtin_commit (int argc, char **argv, const char *prefix, GError **error)
+{
+  GOptionContext *context;
+  gboolean ret = FALSE;
+  HacktreeRepo *repo = NULL;
+  GPtrArray *additions_array = NULL;
+  GPtrArray *removals_array = NULL;
+  GChecksum *commit_checksum = NULL;
+  char **iter;
+
+  context = g_option_context_new ("- Commit a new revision");
+  g_option_context_add_main_entries (context, options, NULL);
+
+  if (!g_option_context_parse (context, &argc, &argv, error))
+    goto out;
+
+  if (repo_path == NULL)
+    repo_path = ".";
+  if (prefix == NULL)
+    prefix = ".";
+
+  repo = hacktree_repo_new (repo_path);
+  if (!hacktree_repo_check (repo, error))
+    goto out;
+
+  if (!(removals || additions))
+    {
+      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                           "No additions or removals specified");
+      goto out;
+    }
+
+  if (!subject)
+    {
+      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                           "A subject must be specified with --subject");
+      goto out;
+    }
+
+  additions_array = g_ptr_array_new ();
+  removals_array = g_ptr_array_new ();
+
+  if (additions)
+    for (iter = additions; *iter; iter++)
+      g_ptr_array_add (additions_array, *iter);
+  if (removals)
+    for (iter = removals; *iter; iter++)
+      g_ptr_array_add (removals_array, *iter);
+
+  if (!hacktree_repo_commit (repo, subject, body, NULL,
+                             prefix, additions_array,
+                             removals_array,
+                             &commit_checksum,
+                             error))
+    goto out;
+     
+ 
+  ret = TRUE;
+  g_print ("%s\n", g_checksum_get_string (commit_checksum));
+ out:
+  if (context)
+    g_option_context_free (context);
+  g_clear_object (&repo);
+  if (removals_array)
+    g_ptr_array_free (removals_array, TRUE);
+  if (additions_array)
+    g_ptr_array_free (additions_array, TRUE);
+  if (commit_checksum)
+    g_checksum_free (commit_checksum);
+  return ret;
+}
diff --git a/src/ht-builtin-fsck.c b/src/ht-builtin-fsck.c
index 1dc5257..b47d932 100644
--- a/src/ht-builtin-fsck.c
+++ b/src/ht-builtin-fsck.c
@@ -94,7 +94,6 @@ hacktree_builtin_fsck (int argc, char **argv, const char *prefix, GError **error
   HtFsckData data;
   gboolean ret = FALSE;
   HacktreeRepo *repo = NULL;
-  int i;
 
   context = g_option_context_new ("- Check the repository for consistency");
   g_option_context_add_main_entries (context, options, NULL);
diff --git a/src/ht-builtins.h b/src/ht-builtins.h
index f9d8798..e9c8c6e 100644
--- a/src/ht-builtins.h
+++ b/src/ht-builtins.h
@@ -36,6 +36,7 @@ typedef struct {
   int flags; /* HacktreeBuiltinFlags */
 } HacktreeBuiltin;
 
+gboolean hacktree_builtin_commit (int argc, char **argv, const char *prefix, GError **error);
 gboolean hacktree_builtin_init (int argc, char **argv, const char *prefix, GError **error);
 gboolean hacktree_builtin_link_file (int argc, char **argv, const char *prefix, GError **error);
 gboolean hacktree_builtin_fsck (int argc, char **argv, const char *prefix, GError **error);
diff --git a/src/libhacktree/hacktree-core.c b/src/libhacktree/hacktree-core.c
index ddda1b5..165dfe7 100644
--- a/src/libhacktree/hacktree-core.c
+++ b/src/libhacktree/hacktree-core.c
@@ -24,10 +24,16 @@
 #include "hacktree.h"
 #include "htutil.h"
 
-char *
+#include <sys/types.h>
+#include <attr/xattr.h>
+
+static char *
 stat_to_string (struct stat *stbuf)
 {
-  return g_strdup_printf ("%d:%d:%d", stbuf->st_mode, stbuf->st_uid, stbuf->st_gid);
+  return g_strdup_printf ("%u:%u:%u",
+                          (guint32)(stbuf->st_mode & ~S_IFMT),
+                          (guint32)stbuf->st_uid, 
+                          (guint32)stbuf->st_gid);
 }
 
 static char *
@@ -56,6 +62,52 @@ canonicalize_xattrs (char *xattr_string, size_t len)
 }
 
 gboolean
+hacktree_get_xattrs_for_directory (const char *path,
+                                   char      **out_xattrs, /* out */
+                                   gsize      *out_len, /* out */
+                                   GError **error)
+{
+  gboolean ret = FALSE;
+  char *xattrs = NULL;
+  ssize_t bytes_read;
+
+  bytes_read = llistxattr (path, NULL, 0);
+
+  if (bytes_read < 0)
+    {
+      if (errno != ENOTSUP)
+        {
+          ht_util_set_error_from_errno (error, errno);
+          goto out;
+        }
+    }
+  else if (bytes_read > 0)
+    {
+      xattrs = g_malloc (bytes_read);
+      if (!llistxattr (path, xattrs, bytes_read))
+        {
+          ht_util_set_error_from_errno (error, errno);
+          g_free (xattrs);
+          xattrs = NULL;
+          goto out;
+        }
+
+      *out_xattrs = canonicalize_xattrs (xattrs, bytes_read);
+      *out_len = (gsize)bytes_read;
+    }
+  else
+    {
+      *out_xattrs = NULL;
+      *out_len = 0;
+    }
+
+  ret = TRUE;
+ out:
+  g_free (xattrs);
+  return ret;
+}
+
+gboolean
 hacktree_stat_and_checksum_file (int dir_fd, const char *path,
                                  GChecksum **out_checksum,
                                  struct stat *out_stbuf,
@@ -73,6 +125,7 @@ hacktree_stat_and_checksum_file (int dir_fd, const char *path,
   gboolean ret = FALSE;
   char *symlink_target = NULL;
   char *device_id = NULL;
+  struct stat stbuf;
 
   basename = g_path_get_basename (path);
 
@@ -89,13 +142,13 @@ hacktree_stat_and_checksum_file (int dir_fd, const char *path,
       dir_fd = dirfd (temp_dir);
     }
 
-  if (fstatat (dir_fd, basename, out_stbuf, AT_SYMLINK_NOFOLLOW) < 0)
+  if (fstatat (dir_fd, basename, &stbuf, AT_SYMLINK_NOFOLLOW) < 0)
     {
       ht_util_set_error_from_errno (error, errno);
       goto out;
     }
 
-  if (!S_ISLNK(out_stbuf->st_mode))
+  if (!S_ISLNK(stbuf.st_mode))
     {
       fd = ht_util_open_file_read_at (dir_fd, basename, error);
       if (fd < 0)
@@ -105,10 +158,10 @@ hacktree_stat_and_checksum_file (int dir_fd, const char *path,
         }
     }
 
-  stat_string = stat_to_string (out_stbuf);
+  stat_string = stat_to_string (&stbuf);
 
   /* FIXME - Add llistxattrat */
-  if (!S_ISLNK(out_stbuf->st_mode))
+  if (!S_ISLNK(stbuf.st_mode))
     bytes_read = flistxattr (fd, NULL, 0);
   else
     bytes_read = llistxattr (path, NULL, 0);
@@ -121,12 +174,12 @@ hacktree_stat_and_checksum_file (int dir_fd, const char *path,
           goto out;
         }
     }
-  if (errno != ENOTSUP)
+  else if (bytes_read > 0)
     {
       gboolean tmp;
       xattrs = g_malloc (bytes_read);
       /* FIXME - Add llistxattrat */
-      if (!S_ISLNK(out_stbuf->st_mode))
+      if (!S_ISLNK(stbuf.st_mode))
         tmp = flistxattr (fd, xattrs, bytes_read);
       else
         tmp = llistxattr (path, xattrs, bytes_read);
@@ -142,7 +195,7 @@ hacktree_stat_and_checksum_file (int dir_fd, const char *path,
 
   content_sha256 = g_checksum_new (G_CHECKSUM_SHA256);
  
-  if (S_ISREG(out_stbuf->st_mode))
+  if (S_ISREG(stbuf.st_mode))
     {
       guint8 buf[8192];
 
@@ -154,7 +207,7 @@ hacktree_stat_and_checksum_file (int dir_fd, const char *path,
           goto out;
         }
     }
-  else if (S_ISLNK(out_stbuf->st_mode))
+  else if (S_ISLNK(stbuf.st_mode))
     {
       symlink_target = g_malloc (PATH_MAX);
 
@@ -163,12 +216,12 @@ hacktree_stat_and_checksum_file (int dir_fd, const char *path,
           ht_util_set_error_from_errno (error, errno);
           goto out;
         }
-      g_checksum_update (content_sha256, symlink_target, strlen (symlink_target));
+      g_checksum_update (content_sha256, (guint8*)symlink_target, strlen (symlink_target));
     }
-  else if (S_ISCHR(out_stbuf->st_mode) || S_ISBLK(out_stbuf->st_mode))
+  else if (S_ISCHR(stbuf.st_mode) || S_ISBLK(stbuf.st_mode))
     {
-      device_id = g_strdup_printf ("%d", out_stbuf->st_rdev);
-      g_checksum_update (content_sha256, device_id, strlen (device_id));
+      device_id = g_strdup_printf ("%u", (guint)stbuf.st_rdev);
+      g_checksum_update (content_sha256, (guint8*)device_id, strlen (device_id));
     }
   else
     {
@@ -181,9 +234,10 @@ hacktree_stat_and_checksum_file (int dir_fd, const char *path,
 
   content_and_meta_sha256 = g_checksum_copy (content_sha256);
 
-  g_checksum_update (content_and_meta_sha256, stat_string, strlen (stat_string));
-  g_checksum_update (content_and_meta_sha256, xattrs_canonicalized, strlen (stat_string));
+  g_checksum_update (content_and_meta_sha256, (guint8*)stat_string, strlen (stat_string));
+  g_checksum_update (content_and_meta_sha256, (guint8*)xattrs_canonicalized, strlen (stat_string));
 
+  *out_stbuf = stbuf;
   *out_checksum = content_and_meta_sha256;
   ret = TRUE;
  out:
diff --git a/src/libhacktree/hacktree-core.h b/src/libhacktree/hacktree-core.h
index 124a90a..b5ed901 100644
--- a/src/libhacktree/hacktree-core.h
+++ b/src/libhacktree/hacktree-core.h
@@ -18,7 +18,6 @@
  *
  * Author: Colin Walters <walters verbum org>
  */
-/* hacktree-repo.h */
 
 #ifndef _HACKTREE_CORE
 #define _HACKTREE_CORE
@@ -27,6 +26,55 @@
 
 G_BEGIN_DECLS
 
+#define HACKTREE_EMPTY_STRING_SHA256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
+
+typedef enum {
+  HACKTREE_SERIALIZED_TREE_VARIANT = 1,
+  HACKTREE_SERIALIZED_COMMIT_VARIANT = 2,
+  HACKTREE_SERIALIZED_DIRMETA_VARIANT = 3
+} HacktreeSerializedVariantType;
+
+#define HACKTREE_SERIALIZED_VARIANT_FORMAT "(uv)"
+
+#define HACKTREE_DIR_META_VERSION 0
+/*
+ * dirmeta objects:
+ * u - Version
+ * u - uid
+ * u - gid
+ * u - mode
+ * ay - xattrs
+ */
+#define HACKTREE_DIRMETA_GVARIANT_FORMAT "(uuuuay)"
+
+#define HACKTREE_TREE_VERSION 0
+/*
+ * Tree objects:
+ * u - Version
+ * a{sv} - Metadata
+ * a(ss) - array of (filename, checksum) for files
+ * a(sss) - array of (dirname, tree_checksum, meta_checksum) for directories
+ */
+#define HACKTREE_TREE_GVARIANT_FORMAT "(ua{sv}a(ss)a(sss)"
+
+#define HACKTREE_COMMIT_VERSION 0
+/*
+ * Commit objects:
+ * u - Version
+ * a{sv} - Metadata
+ * s - parent checksum
+ * s - subject 
+ * s - body
+ * t - Timestamp in seconds since the epoch (UTC)
+ * s - Tree SHA256
+ */
+#define HACKTREE_COMMIT_GVARIANT_FORMAT "(ua{sv}sssts)"
+
+gboolean   hacktree_get_xattrs_for_directory (const char *path,
+                                              char      **out_xattrs,
+                                              gsize      *out_len,
+                                              GError    **error);
+
 gboolean hacktree_stat_and_checksum_file (int dirfd, const char *path,
                                           GChecksum **out_checksum,
                                           struct stat *out_stbuf,
diff --git a/src/libhacktree/hacktree-repo.c b/src/libhacktree/hacktree-repo.c
index 7f6fce0..908ed57 100644
--- a/src/libhacktree/hacktree-repo.c
+++ b/src/libhacktree/hacktree-repo.c
@@ -24,6 +24,17 @@
 #include "hacktree.h"
 #include "htutil.h"
 
+#include <gio/gunixoutputstream.h>
+
+static gboolean
+link_one_file (HacktreeRepo *self, const char *path,
+               gboolean ignore_exists, gboolean force,
+               GChecksum **out_checksum,
+               GError **error);
+static char *
+get_object_path_for_checksum (HacktreeRepo  *self,
+                              const char    *checksum);
+
 enum {
   PROP_0,
 
@@ -39,9 +50,13 @@ typedef struct _HacktreeRepoPrivate HacktreeRepoPrivate;
 
 struct _HacktreeRepoPrivate {
   char *path;
+  GFile *repo_file;
+  char *head_ref_path;
+  char *index_path;
   char *objects_path;
 
   gboolean inited;
+  char *current_head;
 };
 
 static void
@@ -51,7 +66,11 @@ hacktree_repo_finalize (GObject *object)
   HacktreeRepoPrivate *priv = GET_PRIVATE (self);
 
   g_free (priv->path);
+  g_clear_object (&priv->repo_file);
+  g_free (priv->head_ref_path);
+  g_free (priv->index_path);
   g_free (priv->objects_path);
+  g_free (priv->current_head);
 
   G_OBJECT_CLASS (hacktree_repo_parent_class)->finalize (object);
 }
@@ -111,8 +130,11 @@ hacktree_repo_constructor (GType                  gtype,
   priv = GET_PRIVATE (object);
 
   g_assert (priv->path != NULL);
-
+  
+  priv->repo_file = g_file_new_for_path (priv->path);
+  priv->head_ref_path = g_build_filename (priv->path, HACKTREE_REPO_DIR, "HEAD", NULL);
   priv->objects_path = g_build_filename (priv->path, HACKTREE_REPO_DIR, "objects", NULL);
+  priv->index_path = g_build_filename (priv->path, HACKTREE_REPO_DIR, "index", NULL);
 
   return object;
 }
@@ -141,8 +163,6 @@ hacktree_repo_class_init (HacktreeRepoClass *klass)
 static void
 hacktree_repo_init (HacktreeRepo *self)
 {
-  HacktreeRepoPrivate *priv = GET_PRIVATE (self);
-  
 }
 
 HacktreeRepo*
@@ -151,11 +171,44 @@ hacktree_repo_new (const char *path)
   return g_object_new (HACKTREE_TYPE_REPO, "path", path, NULL);
 }
 
+static gboolean
+parse_checksum_file (HacktreeRepo   *self,
+                     const char     *path,
+                     char          **sha256,
+                     GError        **error)
+{
+  GError *temp_error = NULL;
+  gboolean ret = FALSE;
+  char *ret_sha256 = NULL;
+
+  ret_sha256 = ht_util_get_file_contents_utf8 (path, &temp_error);
+  if (ret_sha256 == NULL)
+    {
+      if (g_error_matches (temp_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+        {
+          g_clear_error (&temp_error);
+        }
+      else
+        {
+          g_propagate_error (error, temp_error);
+          goto out;
+        }
+    }
+
+  *sha256 = ret_sha256;
+  ret = TRUE;
+ out:
+  return ret;
+}
+
 gboolean
 hacktree_repo_check (HacktreeRepo *self, GError **error)
 {
   HacktreeRepoPrivate *priv = GET_PRIVATE (self);
 
+  if (priv->inited)
+    return TRUE;
+
   if (!g_file_test (priv->objects_path, G_FILE_TEST_IS_DIR))
     {
       g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
@@ -164,40 +217,228 @@ hacktree_repo_check (HacktreeRepo *self, GError **error)
     }
   
   priv->inited = TRUE;
-  return TRUE;
+
+  return parse_checksum_file (self, priv->head_ref_path, &priv->current_head, error);
 }
 
+static gboolean
+import_gvariant_object (HacktreeRepo  *self,
+                        HacktreeSerializedVariantType type,
+                        GVariant       *variant,
+                        GChecksum    **out_checksum,
+                        GError       **error)
+{
+  HacktreeRepoPrivate *priv = GET_PRIVATE (self);
+  GVariant *serialized = NULL;
+  gboolean ret = FALSE;
+  gsize bytes_written;
+  char *tmp_name = NULL;
+  int fd = -1;
+  GUnixOutputStream *stream = NULL;
+
+  serialized = g_variant_new ("(uv)", (guint32)type, variant);
+
+  tmp_name = g_build_filename (priv->objects_path, "variant-tmp-XXXXXX", NULL);
+  fd = mkstemp (tmp_name);
+  if (fd < 0)
+    {
+      ht_util_set_error_from_errno (error, errno);
+      goto out;
+    }
+
+  stream = (GUnixOutputStream*)g_unix_output_stream_new (fd, FALSE);
+  if (!g_output_stream_write_all ((GOutputStream*)stream,
+                                  g_variant_get_data (serialized),
+                                  g_variant_get_size (serialized),
+                                  &bytes_written,
+                                  NULL,
+                                  error))
+    goto out;
+  if (!g_output_stream_close ((GOutputStream*)stream,
+                              NULL, error))
+    goto out;
+
+  if (!link_one_file (self, tmp_name, FALSE, FALSE, out_checksum, error))
+    goto out;
+  
+  ret = TRUE;
+ out:
+  /* Unconditionally unlink; if we suceeded, there's a new link, if not, clean up. */
+  (void) unlink (tmp_name);
+  if (fd != -1)
+    close (fd);
+  if (serialized != NULL)
+    g_variant_unref (serialized);
+  g_free (tmp_name);
+  g_clear_object (&stream);
+  return ret;
+}
+
+static gboolean
+load_gvariant_object (HacktreeRepo  *self,
+                      HacktreeSerializedVariantType expected_type,
+                      const char    *sha256, 
+                      GVariant     **out_variant,
+                      GError       **error)
+{
+  HacktreeRepoPrivate *priv = GET_PRIVATE (self);
+  GMappedFile *mfile = NULL;
+  gboolean ret = FALSE;
+  GVariant *ret_variant = NULL;
+  GVariant *container = NULL;
+  char *path = NULL;
+
+  path = get_object_path_for_checksum (self, sha256);
+  
+  mfile = g_mapped_file_new (priv->index_path, FALSE, error);
+  if (mfile == NULL)
+    goto out;
+  else
+    {
+      guint32 type;
+      container = g_variant_new_from_data (G_VARIANT_TYPE (HACKTREE_SERIALIZED_VARIANT_FORMAT),
+                                           g_mapped_file_get_contents (mfile),
+                                           g_mapped_file_get_length (mfile),
+                                           FALSE,
+                                           (GDestroyNotify) g_mapped_file_unref,
+                                           mfile);
+      if (!g_variant_is_of_type (container, G_VARIANT_TYPE (HACKTREE_SERIALIZED_VARIANT_FORMAT)))
+        {
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "Corrupted metadata object '%s'", sha256);
+          goto out;
+        }
+      g_variant_get (container, "(uv)",
+                     &type, &ret_variant);
+      if (type != expected_type)
+        {
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "Corrupted metadata object '%s'; found type %u, expected %u", sha256,
+                       type, (guint32)expected_type);
+          goto out;
+
+        }
+      mfile = NULL;
+    }
+
+  ret = TRUE;
+ out:
+  if (!ret)
+    {
+      if (!ret_variant)
+        g_variant_unref (ret_variant);
+    }
+  else
+    *out_variant = ret_variant;
+  if (container != NULL)
+    g_variant_unref (container);
+  g_free (path);
+  if (mfile != NULL)
+    g_mapped_file_unref (mfile);
+  return ret;
+}
+
+static gboolean
+import_directory_meta (HacktreeRepo  *self,
+                       const char *path,
+                       GVariant  **out_variant,
+                       GChecksum **out_checksum,
+                       GError    **error)
+{
+  gboolean ret = FALSE;
+  struct stat stbuf;
+  GChecksum *ret_checksum = NULL;
+  GVariant *dirmeta = NULL;
+  char *xattrs = NULL;
+  gsize xattr_len;
+
+  if (lstat (path, &stbuf) < 0)
+    {
+      ht_util_set_error_from_errno (error, errno);
+      goto out;
+    }
+  
+  if (!S_ISDIR(stbuf.st_mode))
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Not a directory: '%s'", path);
+      goto out;
+    }
+
+  if (!hacktree_get_xattrs_for_directory (path, &xattrs, &xattr_len, error))
+    goto out;
+
+  dirmeta = g_variant_new ("(uuuu ay)",
+                           HACKTREE_DIR_META_VERSION,
+                           (guint32)stbuf.st_uid,
+                           (guint32)stbuf.st_gid,
+                           (guint32)(stbuf.st_mode & ~S_IFMT),
+                           g_variant_new_fixed_array (G_VARIANT_TYPE ("y"),
+                                                      xattrs, xattr_len, 1));
+  g_variant_ref_sink (dirmeta);
+
+  if (!import_gvariant_object (self, HACKTREE_SERIALIZED_DIRMETA_VARIANT, 
+                               dirmeta, &ret_checksum, error))
+        goto out;
+
+  ret = TRUE;
+ out:
+  if (!ret)
+    {
+      if (ret_checksum)
+        g_checksum_free (ret_checksum);
+      if (dirmeta != NULL)
+        g_variant_unref (dirmeta);
+    }
+  else
+    {
+      *out_checksum = ret_checksum;
+      *out_variant = dirmeta;
+    }
+  g_free (xattrs);
+  return ret;
+}
+
+static char *
+get_object_path_for_checksum (HacktreeRepo  *self,
+                              const char    *checksum)
+{
+  HacktreeRepoPrivate *priv = GET_PRIVATE (self);
+  char *checksum_prefix;
+  char *ret;
+
+  checksum_prefix = g_strndup (checksum, 2);
+  ret = g_build_filename (priv->objects_path, checksum_prefix, checksum + 2, NULL);
+  g_free (checksum_prefix);
+ 
+  return ret;
+}
 
 static char *
 prepare_dir_for_checksum_get_object_path (HacktreeRepo *self,
                                           GChecksum    *checksum,
                                           GError      **error)
 {
-  HacktreeRepoPrivate *priv = GET_PRIVATE (self);
-  char *checksum_prefix = NULL;
   char *checksum_dir = NULL;
   char *object_path = NULL;
-  GError *temp_error = NULL;
 
-  checksum_prefix = g_strndup (g_checksum_get_string (checksum), 2);
-  g_assert_cmpuint (strlen (checksum_prefix), ==, 2);
-  checksum_dir = g_build_filename (priv->objects_path, checksum_prefix, NULL);
+  object_path = get_object_path_for_checksum (self, g_checksum_get_string (checksum));
+  checksum_dir = g_path_get_dirname (object_path);
 
   if (!ht_util_ensure_directory (checksum_dir, FALSE, error))
     goto out;
   
-  object_path = g_build_filename (checksum_dir, g_checksum_get_string (checksum) + 2, NULL);
  out:
-  g_free (checksum_prefix);
   g_free (checksum_dir);
   return object_path;
 }
 
 static gboolean
 link_one_file (HacktreeRepo *self, const char *path,
-               gboolean ignore_exists, gboolean force, GError **error)
+               gboolean ignore_exists, gboolean force,
+               GChecksum **out_checksum,
+               GError **error)
 {
-  HacktreeRepoPrivate *priv = GET_PRIVATE (self);
   char *src_basename = NULL;
   char *src_dirname = NULL;
   char *dest_basename = NULL;
@@ -207,10 +448,8 @@ link_one_file (HacktreeRepo *self, const char *path,
   DIR *src_dir = NULL;
   DIR *dest_dir = NULL;
   gboolean ret = FALSE;
-  int fd;
   struct stat stbuf;
   char *dest_path = NULL;
-  char *checksum_prefix;
 
   src_basename = g_path_get_basename (path);
   src_dirname = g_path_get_dirname (path);
@@ -265,6 +504,8 @@ link_one_file (HacktreeRepo *self, const char *path,
       (void) unlinkat (dirfd (dest_dir), tmp_dest_basename, 0);
     }
 
+  *out_checksum = id;
+  id = NULL;
   ret = TRUE;
  out:
   if (id != NULL)
@@ -289,14 +530,661 @@ hacktree_repo_link_file (HacktreeRepo *self,
                          GError      **error)
 {
   HacktreeRepoPrivate *priv = GET_PRIVATE (self);
+  GChecksum *checksum = NULL;
+
+  g_return_val_if_fail (priv->inited, FALSE);
+
+  if (!link_one_file (self, path, ignore_exists, force, &checksum, error))
+    return FALSE;
+  g_checksum_free (checksum);
+  return TRUE;
+}
+
+typedef struct _ParsedTreeData ParsedTreeData;
+typedef struct _ParsedDirectoryData ParsedDirectoryData;
+
+static void parsed_tree_data_free (ParsedTreeData *pdata);
+
+struct _ParsedDirectoryData {
+  ParsedTreeData *tree_data;
+  char *metadata_sha256;
+  GVariant *meta_data;
+};
+
+static void
+parsed_directory_data_free (ParsedDirectoryData *pdata)
+{
+  if (pdata == NULL)
+    return;
+  parsed_tree_data_free (pdata->tree_data);
+  g_free (pdata->metadata_sha256);
+  g_variant_unref (pdata->meta_data);
+  g_free (pdata);
+}
+
+struct _ParsedTreeData {
+  GHashTable *files;  /* char* filename -> char* checksum */
+  GHashTable *directories;  /* char* dirname -> ParsedDirectoryData* */
+};
+
+static ParsedTreeData *
+parsed_tree_data_new (void)
+{
+  ParsedTreeData *ret = g_new0 (ParsedTreeData, 1);
+  ret->files = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                      (GDestroyNotify)g_free, 
+                                      (GDestroyNotify)g_free);
+  ret->directories = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                            (GDestroyNotify)g_free, 
+                                            (GDestroyNotify)parsed_directory_data_free);
+  return ret;
+}
+
+static void
+parsed_tree_data_free (ParsedTreeData *pdata)
+{
+  if (pdata == NULL)
+    return;
+  g_hash_table_destroy (pdata->files);
+  g_hash_table_destroy (pdata->directories);
+  g_free (pdata);
+}
+
+static gboolean
+parse_tree (HacktreeRepo    *self,
+            const char      *sha256,
+            ParsedTreeData **out_pdata,
+            GError         **error)
+{
+  gboolean ret = FALSE;
+  ParsedTreeData *ret_pdata = NULL;
+  int i, n;
+  guint32 version;
+  GVariant *tree_variant = NULL;
+  GVariant *meta_variant = NULL;
+  GVariant *files_variant = NULL;
+  GVariant *dirs_variant = NULL;
+
+  if (!load_gvariant_object (self, HACKTREE_SERIALIZED_TREE_VARIANT,
+                             sha256, &tree_variant, error))
+    goto out;
+
+  g_variant_get (tree_variant, "(u a{sv}@a(ss)@a(sss))",
+                 &version, &meta_variant, &files_variant, &dirs_variant);
+
+  ret_pdata = parsed_tree_data_new ();
+  n = g_variant_n_children (files_variant);
+  for (i = 0; i < n; i++)
+    {
+      const char *filename;
+      const char *checksum;
+
+      g_variant_get_child (files_variant, i, "(ss)", &filename, &checksum);
+
+      g_hash_table_insert (ret_pdata->files, g_strdup (filename), g_strdup (checksum));
+    }
+
+  n = g_variant_n_children (dirs_variant);
+  for (i = 0; i < n; i++)
+    {
+      const char *dirname;
+      const char *tree_checksum;
+      const char *meta_checksum;
+      ParsedTreeData *child_tree = NULL;
+      GVariant *metadata = NULL;
+      ParsedDirectoryData *child_dir = NULL;
+
+      g_variant_get_child (files_variant, i, "(sss)",
+                           &dirname, &tree_checksum, &meta_checksum);
+      
+      if (!parse_tree (self, tree_checksum, &child_tree, error))
+        goto out;
+
+      if (!load_gvariant_object (self, HACKTREE_SERIALIZED_DIRMETA_VARIANT,
+                                 meta_checksum, &metadata, error))
+        {
+          parsed_tree_data_free (child_tree);
+          goto out;
+        }
+
+      child_dir = g_new0 (ParsedDirectoryData, 1);
+      child_dir->tree_data = child_tree;
+      child_dir->metadata_sha256 = g_strdup (meta_checksum);
+      child_dir->meta_data = g_variant_ref_sink (metadata);
+
+      g_hash_table_insert (ret_pdata->directories, g_strdup (dirname), child_dir);
+    }
+
+  ret = TRUE;
+ out:
+  if (!ret)
+    parsed_tree_data_free (ret_pdata);
+  else
+    *out_pdata = ret_pdata;
+  if (tree_variant)
+    g_variant_unref (tree_variant);
+  if (meta_variant)
+    g_variant_unref (meta_variant);
+  if (files_variant)
+    g_variant_unref (files_variant);
+  if (dirs_variant)
+    g_variant_unref (dirs_variant);
+  return ret;
+}
+
+static gboolean
+load_commit_and_trees (HacktreeRepo   *self,
+                       const char     *commit_sha256,
+                       GVariant      **out_commit,
+                       ParsedTreeData **out_tree_data, 
+                       GError        **error)
+{
+  HacktreeRepoPrivate *priv = GET_PRIVATE (self);
+  GVariant *ret_commit = NULL;
+  ParsedTreeData *ret_tree_data = NULL;
+  gboolean ret = FALSE;
+  const char *tree_checksum;
+
+  if (!priv->current_head)
+    {
+      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                           "Can't load current commit; no HEAD reference");
+      goto out;
+    }
+
+  if (!load_gvariant_object (self, HACKTREE_SERIALIZED_COMMIT_VARIANT,
+                             commit_sha256, &ret_commit, error))
+    goto out;
+
+  g_variant_get_child (ret_commit, 5, "&s", &tree_checksum);
+
+  if (!parse_tree (self, tree_checksum, &ret_tree_data, error))
+    goto out;
+
+  ret = TRUE;
+ out:
+  if (!ret)
+    {
+      if (ret_commit)
+        g_variant_unref (ret_commit);
+      parsed_tree_data_free (ret_tree_data);
+    }
+  else
+    {
+      *out_commit = ret_commit;
+      *out_tree_data = ret_tree_data;
+    }
+  return FALSE;
+}
+
+static GVariant *
+create_empty_gvariant_dict (void)
+{
+  GVariantBuilder builder;
+  g_variant_builder_init (&builder, G_VARIANT_TYPE("a{sv}"));
+  return g_variant_builder_end (&builder);
+}
+
+static gboolean
+import_parsed_tree (HacktreeRepo    *self,
+                    ParsedTreeData  *tree,
+                    GChecksum      **out_checksum,
+                    GError         **error)
+{
+  gboolean ret = FALSE;
+  GVariant *serialized_tree = NULL;
+  gboolean builders_initialized = FALSE;
+  GVariantBuilder files_builder;
+  GVariantBuilder dirs_builder;
+  GHashTableIter hash_iter;
+  gpointer key, value;
+
+  g_variant_builder_init (&files_builder, G_VARIANT_TYPE ("a(ss)"));
+  g_variant_builder_init (&dirs_builder, G_VARIANT_TYPE ("a(sss)"));
+  builders_initialized = TRUE;
+
+  g_hash_table_iter_init (&hash_iter, tree->files);
+  while (g_hash_table_iter_next (&hash_iter, &key, &value))
+    {
+      const char *name = key;
+      const char *checksum = value;
+
+      g_variant_builder_add (&files_builder, "(ss)", name, checksum);
+    }
+
+  g_hash_table_iter_init (&hash_iter, tree->directories);
+  while (g_hash_table_iter_next (&hash_iter, &key, &value))
+    {
+      const char *name = key;
+      GChecksum *dir_checksum = NULL;
+      ParsedDirectoryData *dir = value;
+
+      if (!import_parsed_tree (self, dir->tree_data, &dir_checksum, error))
+        goto out;
+
+      g_variant_builder_add (&dirs_builder, "(sss)",
+                             name, g_checksum_get_string (dir_checksum), dir->metadata_sha256);
+    }
+
+  serialized_tree = g_variant_new ("(u a{sv}@a(ss)@a(sss))",
+                                   0,
+                                   create_empty_gvariant_dict (),
+                                   g_variant_builder_end (&files_builder),
+                                   g_variant_builder_end (&dirs_builder));
+  builders_initialized = FALSE;
+  g_variant_ref_sink (serialized_tree);
+  if (!import_gvariant_object (self, HACKTREE_SERIALIZED_TREE_VARIANT, serialized_tree, out_checksum, error))
+    goto out;
+  
+  ret = TRUE;
+ out:
+  if (builders_initialized)
+    {
+      g_variant_builder_clear (&files_builder);
+      g_variant_builder_clear (&dirs_builder);
+    }
+  if (serialized_tree)
+    g_variant_unref (serialized_tree);
+  return ret;
+}
+
+static gboolean
+check_path (const char *filename,
+            GError    **error)
+{
+  gboolean ret = FALSE;
+
+  if (!*filename)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Invalid empty filename");
+      goto out;
+    }
+
+  if (ht_util_filename_has_dotdot (filename))
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Path uplink '..' in filename '%s' not allowed (yet)", filename);
+      goto out;
+    }
+  
+  if (g_path_is_absolute (filename))
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Absolute filename '%s' not allowed (yet)", filename);
+      goto out;
+    }
+
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+static gboolean
+walk_parsed_tree (HacktreeRepo  *self,
+                  const char    *filename,
+                  ParsedTreeData *tree,
+                  int            *out_filename_index, /* out*/
+                  char          **out_component, /* out, but do not free */
+                  ParsedTreeData **out_tree, /* out, but do not free */
+                  GError        **error)
+{
+  gboolean ret = FALSE;
+  GPtrArray *components = NULL;
+  ParsedTreeData *current_tree = tree;
+  const char *component = NULL;
+  const char *file_sha1;
+  ParsedDirectoryData *dir;
+  int i;
+  int ret_filename_index = 0;
+
+  components = ht_util_path_split (filename);
+  g_assert (components != NULL);
+
+  current_tree = tree;
+  for (i = 0; i < components->len - 1; i++)
+    {
+      component = components->pdata[i];
+      file_sha1 = g_hash_table_lookup (current_tree->files, component);
+      dir = g_hash_table_lookup (current_tree->directories, component);
+          
+      if (!(file_sha1 || dir))
+        {
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "No such file or directory: %s",
+                       filename);
+          goto out;
+        }
+      else if (file_sha1)
+        {
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "Encountered non-directory '%s' in '%s'",
+                       (char*)component,
+                       filename);
+          goto out;
+        }
+      else if (!dir)
+        g_assert_not_reached ();
+      current_tree = dir->tree_data;
+      ret_filename_index++;
+    }
+
+  ret = TRUE;
+  g_assert (!(file_sha1 && dir));
+  *out_filename_index = i;
+  *out_component = components->pdata[i-1];
+  *out_tree = current_tree;
+ out:
+  g_ptr_array_free (components, TRUE);
+  return ret;
+}
+
+static gboolean
+remove_files_from_tree (HacktreeRepo   *self,
+                        const char     *base,
+                        GPtrArray      *removed_files,
+                        ParsedTreeData *tree,
+                        GError        **error)
+{
+  gboolean ret = FALSE;
+  int i;
+
+  for (i = 0; i < removed_files->len; i++)
+    {
+      const char *filename = removed_files->pdata[i];
+      int filename_index;
+      const char *component;
+      ParsedTreeData *parent;
+      const char *file_sha1;
+      ParsedTreeData *dir;
+
+      if (!check_path (filename, error))
+        goto out;
+       
+      if (!walk_parsed_tree (self, filename, tree,
+                             &filename_index, (char**)&component, &parent,
+                             error))
+        goto out;
+
+      file_sha1 = g_hash_table_lookup (parent->files, component);
+      dir = g_hash_table_lookup (parent->directories, component);
+
+      if (file_sha1)
+        g_hash_table_remove (parent->files, component);
+      else if (dir)
+        g_hash_table_remove (parent->directories, component);
+      else
+        g_assert_not_reached ();
+    }
+  
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+static gboolean
+add_one_directory_to_tree_and_import (HacktreeRepo   *self,
+                                      const char     *basename,
+                                      const char     *abspath,
+                                      ParsedTreeData *tree,
+                                      ParsedDirectoryData *dir,
+                                      GError        **error)
+{
+  gboolean ret = FALSE;
+  GVariant *dirmeta = NULL;
+  GChecksum *dir_meta_checksum = NULL;
+
+  g_assert (tree != NULL);
+
+  if (!import_directory_meta (self, abspath, &dirmeta, &dir_meta_checksum, error))
+    goto out;
+
+  if (dir)
+    {
+      g_variant_unref (dir->meta_data);
+      dir->meta_data = dirmeta;
+    }
+  else
+    {
+      dir = g_new0 (ParsedDirectoryData, 1);
+      dir->tree_data = parsed_tree_data_new ();
+      dir->metadata_sha256 = g_strdup (g_checksum_get_string (dir_meta_checksum));
+      dir->meta_data = dirmeta;
+      g_hash_table_insert (tree->directories, g_strdup (basename), dir);
+    }
+
+  ret = TRUE;
+ out:
+  if (dir_meta_checksum)
+    g_checksum_free (dir_meta_checksum);
+  return ret;
+}
+
+static gboolean
+add_one_file_to_tree_and_import (HacktreeRepo   *self,
+                                 const char     *basename,
+                                 const char     *abspath,
+                                 ParsedTreeData *tree,
+                                 GError        **error)
+{
+  gboolean ret = FALSE;
+  GChecksum *checksum = NULL;
+  
+  g_assert (tree != NULL);
+
+  if (!link_one_file (self, abspath, TRUE, FALSE, &checksum, error))
+    goto out;
+
+  g_hash_table_replace (tree->files, g_strdup (basename),
+                        g_strdup (g_checksum_get_string (checksum)));
+
+  ret = TRUE;
+ out:
+  if (checksum)
+    g_checksum_free (checksum);
+  return ret;
+}
+
+static gboolean
+add_one_path_to_tree_and_import (HacktreeRepo   *self,
+                                 const char     *base,
+                                 const char     *filename,
+                                 ParsedTreeData *tree,
+                                 GError        **error)
+{
+  gboolean ret = FALSE;
+  GPtrArray *components = NULL;
+  struct stat stbuf;
+  char *component_abspath = NULL;
+  ParsedTreeData *current_tree = tree;
+  const char *component = NULL;
+  const char *file_sha1;
+  char *abspath = NULL;
+  ParsedDirectoryData *dir;
+  int i;
+  gboolean is_directory;
+      
+  if (!check_path (filename, error))
+    goto out;
+
+  abspath = g_build_filename (base, filename, NULL);
+
+  if (lstat (abspath, &stbuf) < 0)
+    {
+      ht_util_set_error_from_errno (error, errno);
+      goto out;
+    }
+  is_directory = S_ISDIR(stbuf.st_mode);
+       
+  if (components)
+    g_ptr_array_free (components, TRUE);
+  components = ht_util_path_split (filename);
+  g_assert (components->len > 0);
+
+  current_tree = tree;
+  for (i = 0; i < components->len; i++)
+    {
+      component = components->pdata[i];
+      g_free (component_abspath);
+      component_abspath = ht_util_path_join_n (base, components, i);
+      file_sha1 = g_hash_table_lookup (current_tree->files, component);
+      dir = g_hash_table_lookup (current_tree->directories, component);
+          
+      if (i < components->len - 1)
+        {
+          if (file_sha1 != NULL)
+            {
+              g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                           "Encountered non-directory '%s' in '%s'",
+                           component,
+                           filename);
+              goto out;
+            }
+          /* Implicitly add intermediate directories */
+          if (!add_one_directory_to_tree_and_import (self, component,
+                                                     abspath, current_tree, dir,
+                                                     error))
+            goto out;
+        }
+      else if (is_directory)
+        {
+          if (file_sha1 != NULL)
+            {
+              g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                           "File '%s' can't be overwritten by directory",
+                           filename);
+              goto out;
+            }
+          if (!add_one_directory_to_tree_and_import (self, component,
+                                                     abspath, current_tree, dir,
+                                                     error))
+            goto out;
+        }
+      else 
+        {
+          g_assert (!is_directory);
+          if (dir != NULL)
+            {
+              g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                           "File '%s' can't be overwritten by directory",
+                           filename);
+              goto out;
+            }
+          if (!add_one_file_to_tree_and_import (self, component, abspath,
+                                                current_tree, error))
+            goto out;
+        }
+    }
+
+  ret = TRUE;
+ out:
+  g_free (component_abspath);
+  g_free (abspath);
+  return ret;
+}
+
+static gboolean
+add_files_to_tree_and_import (HacktreeRepo   *self,
+                              const char     *base,
+                              GPtrArray      *added_files,
+                              ParsedTreeData *tree,
+                              GError        **error)
+{
+  gboolean ret = FALSE;
+  int i;
+
+  for (i = 0; i < added_files->len; i++)
+    {
+      const char *path = added_files->pdata[i];
+
+      if (!add_one_path_to_tree_and_import (self, base, path, tree, error))
+        goto out;
+    }
+  
+  ret = TRUE;
+ out:
+  return ret;
+}
+
+gboolean
+hacktree_repo_commit (HacktreeRepo *self,
+                      const char   *subject,
+                      const char   *body,
+                      GVariant     *metadata,
+                      const char   *base,
+                      GPtrArray    *modified_files,
+                      GPtrArray    *removed_files,
+                      GChecksum   **out_commit,
+                      GError      **error)
+{
+  HacktreeRepoPrivate *priv = GET_PRIVATE (self);
+  gboolean ret = FALSE;
+  ParsedTreeData *tree = NULL;
+  GVariant *previous_commit = NULL;
+  GVariant *commit = NULL;
+  GChecksum *root_checksum = NULL;
+  GChecksum *ret_commit_checksum = NULL;
+  GDateTime *now = NULL;
 
   g_return_val_if_fail (priv->inited, FALSE);
 
-  return link_one_file (self, path, ignore_exists, force, error);
+  if (priv->current_head)
+    {
+      if (!load_commit_and_trees (self, priv->current_head, &previous_commit, &tree, error))
+        goto out;
+    }
+  else
+    {
+      /* Initial commit */
+      tree = parsed_tree_data_new ();
+    }
+
+  if (!remove_files_from_tree (self, base, removed_files, tree, error))
+    goto out;
+
+  if (!add_files_to_tree_and_import (self, base, modified_files, tree, error))
+    goto out;
+  
+  if (!import_parsed_tree (self, tree, &root_checksum, error))
+    goto out;
+
+  now = g_date_time_new_now_utc ();
+  commit = g_variant_new ("(u a{sv}sssts)",
+                          HACKTREE_COMMIT_VERSION,
+                          create_empty_gvariant_dict (),
+                          priv->current_head ? priv->current_head : "",
+                          subject, body,
+                          g_date_time_to_unix (now) / G_TIME_SPAN_SECOND,
+                          g_checksum_get_string (root_checksum));
+  if (!import_gvariant_object (self, HACKTREE_SERIALIZED_COMMIT_VARIANT,
+                               commit, &ret_commit_checksum, error))
+    goto out;
+
+  ret = TRUE;
+ out:
+  if (!ret)
+    {
+      if (ret_commit_checksum)
+        g_checksum_free (ret_commit_checksum);
+    }
+  else
+    {
+      *out_commit = ret_commit_checksum;
+    }
+  if (root_checksum)
+    g_checksum_free (root_checksum);
+  if (previous_commit)
+    g_variant_unref (previous_commit);
+  parsed_tree_data_free (tree);
+  if (commit)
+    g_variant_unref (commit);
+  if (now)
+    g_date_time_unref (now);
+  return ret;
 }
 
 static gboolean
-iter_object_dir (HacktreeRepo   *repo,
+iter_object_dir (HacktreeRepo   *self,
                  GFile          *dir,
                  HacktreeRepoObjectIter  callback,
                  gpointer                user_data,
@@ -328,7 +1216,7 @@ iter_object_dir (HacktreeRepo   *repo,
       if (strlen (name) == 62 && type != G_FILE_TYPE_DIRECTORY)
         {
           char *path = g_build_filename (dirpath, name, NULL);
-          callback (repo, path, file_info, user_data);
+          callback (self, path, file_info, user_data);
           g_free (path);
         }
 
diff --git a/src/libhacktree/hacktree-repo.h b/src/libhacktree/hacktree-repo.h
index 633771c..55da5ee 100644
--- a/src/libhacktree/hacktree-repo.h
+++ b/src/libhacktree/hacktree-repo.h
@@ -59,6 +59,16 @@ gboolean      hacktree_repo_link_file (HacktreeRepo *repo,
                                        gboolean      force,
                                        GError      **error);
 
+gboolean      hacktree_repo_commit (HacktreeRepo *repo,
+                                    const char   *subject,
+                                    const char   *body,
+                                    GVariant     *metadata,
+                                    const char   *base,
+                                    GPtrArray    *modified_files,
+                                    GPtrArray    *removed_files,
+                                    GChecksum   **out_commit,
+                                    GError      **error);
+
 typedef void (*HacktreeRepoObjectIter) (HacktreeRepo *repo, const char *path,
                                         GFileInfo *fileinfo, gpointer user_data);
 
diff --git a/src/libhtutil/ht-gio-utils.c b/src/libhtutil/ht-gio-utils.c
index 29f193c..615699a 100644
--- a/src/libhtutil/ht-gio-utils.c
+++ b/src/libhtutil/ht-gio-utils.c
@@ -23,10 +23,11 @@
 
 #include <glib-unix.h>
 #include <gio/gio.h>
+#include <gio/gunixinputstream.h>
 
 #include <string.h>
 
-#include "ht-gio-utils.h"
+#include "htutil.h"
 
 gboolean
 ht_util_ensure_directory (const char *path, gboolean with_parents, GError **error)
@@ -56,3 +57,51 @@ ht_util_ensure_directory (const char *path, gboolean with_parents, GError **erro
   g_clear_object (&dir);
   return ret;
 }
+
+
+char *
+ht_util_get_file_contents_utf8 (const char *path,
+                                GError    **error)
+{
+  char *contents;
+  gsize len;
+  if (!g_file_get_contents (path, &contents, &len, error))
+    return NULL;
+  if (!g_utf8_validate (contents, len, NULL))
+    {
+      g_free (contents);
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_FAILED,
+                   "File %s contains invalid UTF-8",
+                   path);
+      return NULL;
+    }
+  return contents;
+}
+
+GInputStream *
+ht_util_read_file_noatime (GFile *file, GCancellable *cancellable, GError **error)
+{
+  GInputStream *ret = NULL;
+  int fd;
+  int flags = O_RDONLY;
+  char *path = NULL;
+
+  path = g_file_get_path (file);
+#ifdef O_NOATIME
+  flags |= O_NOATIME;
+#endif
+  fd = open (path, flags);
+  if (fd < 0)
+    {
+      ht_util_set_error_from_errno (error, errno);
+      goto out;
+    }
+
+  ret = (GInputStream*)g_unix_input_stream_new (fd, TRUE);
+  
+ out:
+  g_free (path);
+  return ret;
+}
diff --git a/src/libhtutil/ht-gio-utils.h b/src/libhtutil/ht-gio-utils.h
index 5b0c65c..388a654 100644
--- a/src/libhtutil/ht-gio-utils.h
+++ b/src/libhtutil/ht-gio-utils.h
@@ -19,8 +19,8 @@
  * Author: Colin Walters <walters verbum org>
  */
 
-#ifndef __HACKTREE_UNIX_UTILS_H__
-#define __HACKTREE_UNIX_UTILS_H__
+#ifndef __HACKTREE_GIO_UTILS_H__
+#define __HACKTREE_GIO_UTILS_H__
 
 #include <gio/gio.h>
 
@@ -28,6 +28,10 @@ G_BEGIN_DECLS
 
 gboolean ht_util_ensure_directory (const char *path, gboolean with_parents, GError **error);
 
+char * ht_util_get_file_contents_utf8 (const char *path, GError    **error);
+
+GInputStream *ht_util_read_file_noatime (GFile *file, GCancellable *cancellable, GError **error);
+
 G_END_DECLS
 
 #endif
diff --git a/src/libhtutil/ht-unix-utils.c b/src/libhtutil/ht-unix-utils.c
index 4637c7d..15ce374 100644
--- a/src/libhtutil/ht-unix-utils.c
+++ b/src/libhtutil/ht-unix-utils.c
@@ -30,6 +30,125 @@
 #include <sys/types.h>
 #include <dirent.h>
 
+static int
+compare_filenames_by_component_length (const char *a,
+                                       const char *b)
+{
+  char *a_slash, *b_slash;
+
+  a_slash = strchr (a, '/');
+  b_slash = strchr (b, '/');
+  while (a_slash && b_slash)
+    {
+      a = a_slash + 1;
+      b = b_slash + 1;
+      a_slash = strchr (a, '/');
+      b_slash = strchr (b, '/');
+    }
+  if (a_slash)
+    return -1;
+  else if (b_slash)
+    return 1;
+  else
+    return 0;
+}
+
+GPtrArray *
+ht_util_sort_filenames_by_component_length (GPtrArray *files)
+{
+  GPtrArray *array = g_ptr_array_sized_new (files->len);
+  memcpy (array->pdata, files->pdata, sizeof (gpointer) * files->len);
+  g_ptr_array_sort (array, (GCompareFunc) compare_filenames_by_component_length);
+  return array;
+}
+
+int
+ht_util_count_filename_components (const char *path)
+{
+  int i = 0;
+
+  while (path)
+    {
+      i++;
+      path = strchr (path, '/');
+      if (path)
+        path++;
+    }
+  return i;
+}
+
+gboolean
+ht_util_filename_has_dotdot (const char *path)
+{
+  char *p;
+  char last;
+
+  if (strcmp (path, "..") == 0)
+    return TRUE;
+  if (g_str_has_prefix (path, "../"))
+    return TRUE;
+  p = strstr (path, "/..");
+  if (!p)
+    return FALSE;
+  last = *(p + 1);
+  return last == '\0' || last == '/';
+}
+
+GPtrArray *
+ht_util_path_split (const char *path)
+{
+  GPtrArray *ret = NULL;
+  const char *p;
+  const char *slash;
+
+  g_return_val_if_fail (path[0] != '/', NULL);
+
+  ret = g_ptr_array_new ();
+  g_ptr_array_set_free_func (ret, g_free);
+
+  p = path;
+  do {
+    slash = strchr (p, '/');
+    if (!slash)
+      {
+        g_ptr_array_add (ret, g_strdup (p));
+        p = NULL;
+      }
+    else
+      {
+        g_ptr_array_add (ret, g_strndup (p, slash - p));
+        p += 1;
+      }
+  } while (p);
+
+  return ret;
+}
+
+char *
+ht_util_path_join_n (const char *base, GPtrArray *components, int n)
+{
+  int max = MAX(n+1, components->len);
+  GPtrArray *subcomponents;
+  char *path;
+  int i;
+
+  subcomponents = g_ptr_array_new ();
+
+  if (base != NULL)
+    g_ptr_array_add (subcomponents, (char*)base);
+
+  for (i = 0; i < max; i++)
+    {
+      g_ptr_array_add (subcomponents, components->pdata[i]);
+    }
+  g_ptr_array_add (subcomponents, NULL);
+  
+  path = g_build_filenamev ((char**)subcomponents->pdata);
+  g_ptr_array_free (subcomponents, TRUE);
+  
+  return path;
+}
+
 void
 ht_util_set_error_from_errno (GError **error,
                               gint     saved_errno)
diff --git a/src/libhtutil/ht-unix-utils.h b/src/libhtutil/ht-unix-utils.h
index ae3731b..bd95b55 100644
--- a/src/libhtutil/ht-unix-utils.h
+++ b/src/libhtutil/ht-unix-utils.h
@@ -31,9 +31,21 @@
 #include <unistd.h>
 #include <dirent.h>
 #include <string.h>
+#include <fcntl.h>
+#include <stdio.h>
 
 G_BEGIN_DECLS
 
+gboolean ht_util_filename_has_dotdot (const char *path);
+
+GPtrArray *ht_util_sort_filenames_by_component_length (GPtrArray *files);
+
+GPtrArray* ht_util_path_split (const char *path);
+
+char *ht_util_path_join_n (const char *base, GPtrArray *components, int n);
+
+int ht_util_count_filename_components (const char *path);
+
 int ht_util_open_file_read (const char *path, GError **error);
 
 int ht_util_open_file_read_at (int dirfd, const char *name, GError **error);
diff --git a/src/main.c b/src/main.c
index d39ef88..6428a1b 100644
--- a/src/main.c
+++ b/src/main.c
@@ -30,6 +30,7 @@
 
 static HacktreeBuiltin builtins[] = {
   { "init", hacktree_builtin_init, 0 },
+  { "commit", hacktree_builtin_commit, 0 },
   { "link-file", hacktree_builtin_link_file, 0 },
   { "fsck", hacktree_builtin_fsck, 0 },
   { NULL }
diff --git a/tests/t0002-commit-one.sh b/tests/t0002-commit-one.sh
new file mode 100755
index 0000000..a8f8d2e
--- /dev/null
+++ b/tests/t0002-commit-one.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+#
+# Copyright (C) 2011 Colin Walters <walters verbum org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+# Author: Colin Walters <walters verbum org>
+
+set -e
+
+. libtest.sh
+
+echo '1..3'
+
+mkdir files
+cd files
+echo hello > yy
+
+mkdir ../repo
+repo="--repo=../repo"
+hacktree init $repo
+echo 'ok init'
+hacktree commit $repo -s "Test Commit" -b "Commit body" --add=yy
+echo 'ok commit'
+hacktree fsck -q $repo
+echo 'ok fsck'



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]