diff --git a/Makefile-hacktree.am b/Makefile-hacktree.am index 6b25cdc5..b62081b5 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 aeff7f77..dd4bc1dc 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 49b3fa63..9bb57928 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 00000000..1273f65b --- /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 + * + * 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 + */ + +#include "config.h" + +#include "ht-builtins.h" +#include "hacktree.h" + +#include + +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 1dc52570..b47d9322 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 f9d8798f..e9c8c6e8 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 ddda1b5a..165dfe75 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 +#include + +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 * @@ -55,6 +61,52 @@ canonicalize_xattrs (char *xattr_string, size_t len) return g_string_free (result, FALSE); } +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, @@ -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 124a90a5..b5ed9016 100644 --- a/src/libhacktree/hacktree-core.h +++ b/src/libhacktree/hacktree-core.h @@ -18,7 +18,6 @@ * * Author: Colin Walters */ -/* 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 7f6fce09..908ed57c 100644 --- a/src/libhacktree/hacktree-repo.c +++ b/src/libhacktree/hacktree-repo.c @@ -24,6 +24,17 @@ #include "hacktree.h" #include "htutil.h" +#include + +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); - return link_one_file (self, path, ignore_exists, force, error); + 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 -iter_object_dir (HacktreeRepo *repo, +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); + + 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 *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 633771cf..55da5eee 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 29f193c0..615699a4 100644 --- a/src/libhtutil/ht-gio-utils.c +++ b/src/libhtutil/ht-gio-utils.c @@ -23,10 +23,11 @@ #include #include +#include #include -#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 5b0c65cb..388a6544 100644 --- a/src/libhtutil/ht-gio-utils.h +++ b/src/libhtutil/ht-gio-utils.h @@ -19,8 +19,8 @@ * Author: Colin Walters */ -#ifndef __HACKTREE_UNIX_UTILS_H__ -#define __HACKTREE_UNIX_UTILS_H__ +#ifndef __HACKTREE_GIO_UTILS_H__ +#define __HACKTREE_GIO_UTILS_H__ #include @@ -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 4637c7df..15ce3748 100644 --- a/src/libhtutil/ht-unix-utils.c +++ b/src/libhtutil/ht-unix-utils.c @@ -30,6 +30,125 @@ #include #include +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 ae3731b4..bd95b552 100644 --- a/src/libhtutil/ht-unix-utils.h +++ b/src/libhtutil/ht-unix-utils.h @@ -31,9 +31,21 @@ #include #include #include +#include +#include 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 d39ef880..6428a1b0 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 00000000..a8f8d2e9 --- /dev/null +++ b/tests/t0002-commit-one.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# +# Copyright (C) 2011 Colin Walters +# +# 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 + +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'