summaryrefslogtreecommitdiffstats
path: root/save-git.c
diff options
context:
space:
mode:
Diffstat (limited to 'save-git.c')
-rw-r--r--save-git.c180
1 files changed, 148 insertions, 32 deletions
diff --git a/save-git.c b/save-git.c
index 4760fe32a..9b0d2dbba 100644
--- a/save-git.c
+++ b/save-git.c
@@ -15,6 +15,18 @@
#include "membuffer.h"
#include "ssrf-version.h"
+/*
+ * handle libgit2 revision 0.20 and earlier
+ */
+#if !LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR <= 20 && !defined(USE_LIBGIT21_API)
+ #define GIT_CHECKOUT_OPTIONS_INIT GIT_CHECKOUT_OPTS_INIT
+ #define git_checkout_options git_checkout_opts
+ #define git_branch_create(out,repo,branch_name,target,force,sig,msg) \
+ git_branch_create(out,repo,branch_name,target,force)
+ #define git_reference_set_target(out,ref,target,signature,log_message) \
+ git_reference_set_target(out,ref,target)
+#endif
+
#define VA_BUF(b, fmt) do { va_list args; va_start(args, fmt); put_vformat(b, fmt, args); va_end(args); } while (0)
static void cond_put_format(int cond, struct membuffer *b, const char *fmt, ...)
@@ -340,16 +352,35 @@ static void create_dive_buffer(struct dive *dive, struct membuffer *b)
save_dive_temperature(b, dive);
}
-int report_error(const char *fmt, ...)
+static struct membuffer error_string_buffer = { 0 };
+
+/*
+ * Note that the act of "getting" the error string
+ * buffer doesn't de-allocate the buffer, but it does
+ * set the buffer length to zero, so that any future
+ * error reports will overwrite the string rather than
+ * append to it.
+ */
+const char *get_error_string(void)
{
- struct membuffer b = { 0 };
- VA_BUF(&b, fmt);
+ const char *str;
+
+ if (!error_string_buffer.len)
+ return "";
+ str = mb_cstring(&error_string_buffer);
+ error_string_buffer.len = 0;
+ return str;
+}
- /* We should do some UI element thing describing the failure */
- put_bytes(&b, "\n", 1);
- flush_buffer(&b, stderr);
- free_buffer(&b);
+int report_error(const char *fmt, ...)
+{
+ struct membuffer *buf = &error_string_buffer;
+ /* Previous unprinted errors? Add a newline in between */
+ if (buf->len)
+ put_bytes(buf, "\n", 1);
+ VA_BUF(buf, fmt);
+ mb_cstring(buf);
return -1;
}
@@ -741,15 +772,53 @@ static int create_git_tree(git_repository *repo, struct dir *root, bool select_o
}
/*
- * libgit2 revision 0.20 and earlier do not have the signature and
- * message log arguments.
+ * See if we can find the parent ID that the git data came from
*/
-#if !LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR <= 20 && !defined(USE_LIBGIT21_API)
- #define git_branch_create(out,repo,branch_name,target,force,sig,msg) \
- git_branch_create(out,repo,branch_name,target,force)
- #define git_reference_set_target(out,ref,target,signature,log_message) \
- git_reference_set_target(out,ref,target)
-#endif
+static git_object *try_to_find_parent(const char *hex_id, git_repository *repo)
+{
+ git_oid object_id;
+ git_commit *commit;
+
+ if (!hex_id)
+ return NULL;
+ if (git_oid_fromstr(&object_id, hex_id))
+ return NULL;
+ if (git_commit_lookup(&commit, repo, &object_id))
+ return NULL;
+ return (git_object *)commit;
+}
+
+static int notify_cb(git_checkout_notify_t why,
+ const char *path,
+ const git_diff_file *baseline,
+ const git_diff_file *target,
+ const git_diff_file *workdir,
+ void *payload)
+{
+ report_error("File '%s' does not match in working tree", path);
+ return 0; /* Continue with checkout */
+}
+
+static git_tree *get_git_tree(git_repository *repo, git_object *parent)
+{
+ git_tree *tree;
+ if (!parent)
+ return NULL;
+ if (git_tree_lookup(&tree, repo, git_commit_tree_id((const git_commit *) parent)))
+ return NULL;
+ return tree;
+}
+
+static int update_git_checkout(git_repository *repo, git_object *parent, git_tree *tree)
+{
+ git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
+
+ opts.checkout_strategy = GIT_CHECKOUT_SAFE;
+ opts.notify_flags = GIT_CHECKOUT_NOTIFY_CONFLICT | GIT_CHECKOUT_NOTIFY_DIRTY;
+ opts.notify_cb = notify_cb;
+ opts.baseline = get_git_tree(repo, parent);
+ return git_checkout_tree(repo, (git_object *) tree, &opts);
+}
static int create_new_commit(git_repository *repo, const char *branch, git_oid *tree_id)
{
@@ -769,15 +838,18 @@ static int create_new_commit(git_repository *repo, const char *branch, git_oid *
case GIT_EINVALIDSPEC:
return report_error("Invalid branch name '%s'", branch);
case GIT_ENOTFOUND: /* We'll happily create it */
- ref = NULL; parent = NULL;
+ ref = NULL;
+ parent = try_to_find_parent(saved_git_id, repo);
break;
case 0:
if (git_reference_peel(&parent, ref, GIT_OBJ_COMMIT))
return report_error("Unable to look up parent in branch '%s'", branch);
- /* If the parent commit has the same tree ID, do nothing */
- if (git_oid_equal(tree_id, git_commit_tree_id((const git_commit *) parent)))
- return 0;
+ if (saved_git_id) {
+ const git_oid *id = git_commit_id((const git_commit *) parent);
+ if (git_oid_strcmp(id, saved_git_id))
+ return report_error("The git branch does not match the git parent of the source");
+ }
/* all good */
break;
@@ -790,19 +862,45 @@ static int create_new_commit(git_repository *repo, const char *branch, git_oid *
if (git_signature_now(&author, "Subsurface", "subsurface@hohndel.org"))
return report_error("No user name configuration in git repo");
- put_format(&commit_msg, "Created by subsurface %s\n", VERSION_STRING);
- if (git_commit_create_v(&commit_id, repo, NULL, author, author, NULL, mb_cstring(&commit_msg), tree, parent != NULL, parent))
- return report_error("Git commit create failed (%s)", strerror(errno));
-
- if (git_commit_lookup(&commit, repo, &commit_id))
- return report_error("Could not look up newly created commit");
+ /* If the parent commit has the same tree ID, do not create a new commit */
+ if (parent && git_oid_equal(tree_id, git_commit_tree_id((const git_commit *) parent))) {
+ /* If the parent already came from the ref, the commit is already there */
+ if (ref)
+ return 0;
+ /* Else we do want to create the new branch, but with the old commit */
+ commit = (git_commit *) parent;
+ } else {
+ put_format(&commit_msg, "Created by subsurface %s\n", VERSION_STRING);
+ if (git_commit_create_v(&commit_id, repo, NULL, author, author, NULL, mb_cstring(&commit_msg), tree, parent != NULL, parent))
+ return report_error("Git commit create failed (%s)", strerror(errno));
+
+ if (git_commit_lookup(&commit, repo, &commit_id))
+ return report_error("Could not look up newly created commit");
+ }
if (!ref) {
if (git_branch_create(&ref, repo, branch, commit, 0, author, "Create branch"))
return report_error("Failed to create branch '%s'", branch);
}
+ /*
+ * If it's a checked-out branch, try to also update the working
+ * tree and index. If that fails (dirty working tree or whatever),
+ * this is not technically a save error (we did save things in
+ * the object database), but it can cause extreme confusion, so
+ * warn about it.
+ */
+ if (git_branch_is_head(ref) && !git_repository_is_bare(repo)) {
+ if (update_git_checkout(repo, parent, tree)) {
+ const git_error *err = giterr_last();
+ const char *errstr = err ? err->message : strerror(errno);
+ report_error("Git branch '%s' is checked out, but worktree is dirty (%s)",
+ branch, errstr);
+ }
+ }
+
if (git_reference_set_target(&ref, ref, &commit_id, author, "Subsurface save event"))
return report_error("Failed to update branch '%s'", branch);
+ set_git_id(&commit_id);
return 0;
}
@@ -850,8 +948,7 @@ static int do_git_save(git_repository *repo, const char *branch, bool select_onl
return report_error("git tree write failed");
/* And save the tree! */
- create_new_commit(repo, branch, &id);
- return 0;
+ return create_new_commit(repo, branch, &id);
}
/*
@@ -876,30 +973,45 @@ struct git_repository *is_git_repository(const char *filename, const char **bran
if (!flen)
return NULL;
+ /*
+ * This is the "point of no return": the name matches
+ * the git repository name rules, and we will no longer
+ * return NULL.
+ *
+ * We will either return "dummy_git_repository" and the
+ * branch pointer will have the _whole_ filename in it,
+ * or we will return a real git repository with the
+ * branch pointer being filled in with just the branch
+ * name.
+ *
+ * The actual git reading/writing routines can use this
+ * to generate proper error messages.
+ */
+ *branchp = filename;
loc = malloc(flen+1);
if (!loc)
- return NULL;
+ return dummy_git_repository;
memcpy(loc, filename, flen);
loc[flen] = 0;
branch = malloc(blen+1);
if (!branch) {
free(loc);
- return NULL;
+ return dummy_git_repository;
}
memcpy(branch, filename+flen+1, blen);
branch[blen] = 0;
if (stat(loc, &st) < 0 || !S_ISDIR(st.st_mode)) {
free(loc);
- return NULL;
+ return dummy_git_repository;
}
ret = git_repository_open(&repo, loc);
free(loc);
if (ret < 0) {
free(branch);
- return NULL;
+ return dummy_git_repository;
}
*branchp = branch;
return repo;
@@ -907,7 +1019,11 @@ struct git_repository *is_git_repository(const char *filename, const char **bran
int git_save_dives(struct git_repository *repo, const char *branch, bool select_only)
{
- int ret = do_git_save(repo, branch, select_only);
+ int ret;
+
+ if (repo == dummy_git_repository)
+ return report_error("Unable to open git repository '%s'", branch);
+ ret = do_git_save(repo, branch, select_only);
git_repository_free(repo);
free((void *)branch);
return ret;