Skip to content

Commit

Permalink
git notes merge: Manual conflict resolution, part 1/2
Browse files Browse the repository at this point in the history
Conflicts (that are to be resolved manually) are written into a special-
purpose working tree, located at .git/NOTES_MERGE_WORKTREE. Within this
directory, conflicting notes entries are stored (with conflict markers
produced by ll_merge()) using the SHA1 of the annotated object. The
.git/NOTES_MERGE_WORKTREE directory will only contain the _conflicting_
note entries. The non-conflicting note entries (aka. the partial merge
result) are stored in 'local_tree', and the SHA1 of the resulting commit
is written to 'result_sha1'. The return value from notes_merge() is -1.

The user is told to edit the files within the .git/NOTES_MERGE_WORKTREE
directory in order to resolve the conflicts.

The patch also contains documentation and testcases for the correct setup
of .git/NOTES_MERGE_WORKTREE.

The next part will recombine the partial notes merge result with the
resolved conflicts in .git/NOTES_MERGE_WORKTREE to produce the complete
merge result.

This patch has been improved by the following contributions:
- Jonathan Nieder: Use trace_printf(...) instead of OUTPUT(o, 5, ...)

Thanks-to: Jonathan Nieder <jrnieder@gmail.com>
Signed-off-by: Johan Herland <johan@herland.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
  • Loading branch information
Johan Herland authored and Junio C Hamano committed Nov 17, 2010
1 parent 00f0306 commit 809f38c
Show file tree
Hide file tree
Showing 5 changed files with 474 additions and 13 deletions.
10 changes: 7 additions & 3 deletions Documentation/git-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ merge::
+
If conflicts arise and a strategy for automatically resolving
conflicting notes (see the -s/--strategy option) is not given,
the merge fails (TODO).
the "manual" resolver is used. This resolver checks out the
conflicting notes in a special worktree (`.git/NOTES_MERGE_WORKTREE`),
and instructs the user to manually resolve the conflicts there.

remove::
Remove the notes for a given object (defaults to HEAD).
Expand Down Expand Up @@ -191,8 +193,10 @@ object, in which case the history of the notes can be read with
NOTES MERGE STRATEGIES
----------------------

The default notes merge strategy is "manual", which is not yet
implemented (TODO).
The default notes merge strategy is "manual", which checks out
conflicting notes in a special work tree for resolving notes conflicts
(`.git/NOTES_MERGE_WORKTREE`), and instructs the user to resolve the
conflicts in that work tree.

"ours" automatically resolves conflicting notes in favor of the local
version (i.e. the current notes ref).
Expand Down
8 changes: 4 additions & 4 deletions builtin/notes.c
Original file line number Diff line number Diff line change
Expand Up @@ -820,14 +820,14 @@ static int merge(int argc, const char **argv, const char *prefix)
/* Update default notes ref with new commit */
update_ref(msg.buf, default_notes_ref(), result_sha1, NULL,
0, DIE_ON_ERR);
else
/* TODO: */
die("'git notes merge' cannot yet handle conflicts!");
else /* Merge has unresolved conflicts */
printf("Automatic notes merge failed. Fix conflicts in %s.\n",
git_path(NOTES_MERGE_WORKTREE));

free_notes(t);
strbuf_release(&remote_ref);
strbuf_release(&msg);
return 0;
return result < 0; /* return non-zero on conflicts */
}

static int remove_cmd(int argc, const char **argv, const char *prefix)
Expand Down
166 changes: 161 additions & 5 deletions notes-merge.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
#include "refs.h"
#include "diff.h"
#include "diffcore.h"
#include "xdiff-interface.h"
#include "ll-merge.h"
#include "dir.h"
#include "notes.h"
#include "notes-merge.h"

Expand Down Expand Up @@ -263,16 +266,169 @@ static void diff_tree_local(struct notes_merge_options *o,
diff_tree_release_paths(&opt);
}

static void check_notes_merge_worktree(struct notes_merge_options *o)
{
if (!o->has_worktree) {
/*
* Must establish NOTES_MERGE_WORKTREE.
* Abort if NOTES_MERGE_WORKTREE already exists
*/
if (file_exists(git_path(NOTES_MERGE_WORKTREE))) {
if (advice_resolve_conflict)
die("You have not concluded your previous "
"notes merge (%s exists).\nPlease, use "
"'git notes merge --commit' or 'git notes "
"merge --reset' to commit/abort the "
"previous merge before you start a new "
"notes merge.", git_path("NOTES_MERGE_*"));
else
die("You have not concluded your notes merge "
"(%s exists).", git_path("NOTES_MERGE_*"));
}

if (safe_create_leading_directories(git_path(
NOTES_MERGE_WORKTREE "/.test")))
die_errno("unable to create directory %s",
git_path(NOTES_MERGE_WORKTREE));
o->has_worktree = 1;
} else if (!file_exists(git_path(NOTES_MERGE_WORKTREE)))
/* NOTES_MERGE_WORKTREE should already be established */
die("missing '%s'. This should not happen",
git_path(NOTES_MERGE_WORKTREE));
}

static void write_buf_to_worktree(const unsigned char *obj,
const char *buf, unsigned long size)
{
int fd;
char *path = git_path(NOTES_MERGE_WORKTREE "/%s", sha1_to_hex(obj));
if (safe_create_leading_directories(path))
die_errno("unable to create directory for '%s'", path);
if (file_exists(path))
die("found existing file at '%s'", path);

fd = open(path, O_WRONLY | O_TRUNC | O_CREAT, 0666);
if (fd < 0)
die_errno("failed to open '%s'", path);

while (size > 0) {
long ret = write_in_full(fd, buf, size);
if (ret < 0) {
/* Ignore epipe */
if (errno == EPIPE)
break;
die_errno("notes-merge");
} else if (!ret) {
die("notes-merge: disk full?");
}
size -= ret;
buf += ret;
}

close(fd);
}

static void write_note_to_worktree(const unsigned char *obj,
const unsigned char *note)
{
enum object_type type;
unsigned long size;
void *buf = read_sha1_file(note, &type, &size);

if (!buf)
die("cannot read note %s for object %s",
sha1_to_hex(note), sha1_to_hex(obj));
if (type != OBJ_BLOB)
die("blob expected in note %s for object %s",
sha1_to_hex(note), sha1_to_hex(obj));
write_buf_to_worktree(obj, buf, size);
free(buf);
}

static int ll_merge_in_worktree(struct notes_merge_options *o,
struct notes_merge_pair *p)
{
mmbuffer_t result_buf;
mmfile_t base, local, remote;
int status;

read_mmblob(&base, p->base);
read_mmblob(&local, p->local);
read_mmblob(&remote, p->remote);

status = ll_merge(&result_buf, sha1_to_hex(p->obj), &base, NULL,
&local, o->local_ref, &remote, o->remote_ref, 0);

free(base.ptr);
free(local.ptr);
free(remote.ptr);

if ((status < 0) || !result_buf.ptr)
die("Failed to execute internal merge");

write_buf_to_worktree(p->obj, result_buf.ptr, result_buf.size);
free(result_buf.ptr);

return status;
}

static int merge_one_change_manual(struct notes_merge_options *o,
struct notes_merge_pair *p,
struct notes_tree *t)
{
const char *lref = o->local_ref ? o->local_ref : "local version";
const char *rref = o->remote_ref ? o->remote_ref : "remote version";

trace_printf("\t\t\tmerge_one_change_manual(obj = %.7s, base = %.7s, "
"local = %.7s, remote = %.7s)\n",
sha1_to_hex(p->obj), sha1_to_hex(p->base),
sha1_to_hex(p->local), sha1_to_hex(p->remote));

OUTPUT(o, 2, "Auto-merging notes for %s", sha1_to_hex(p->obj));
check_notes_merge_worktree(o);
if (is_null_sha1(p->local)) {
/* D/F conflict, checkout p->remote */
assert(!is_null_sha1(p->remote));
OUTPUT(o, 1, "CONFLICT (delete/modify): Notes for object %s "
"deleted in %s and modified in %s. Version from %s "
"left in tree.", sha1_to_hex(p->obj), lref, rref, rref);
write_note_to_worktree(p->obj, p->remote);
} else if (is_null_sha1(p->remote)) {
/* D/F conflict, checkout p->local */
assert(!is_null_sha1(p->local));
OUTPUT(o, 1, "CONFLICT (delete/modify): Notes for object %s "
"deleted in %s and modified in %s. Version from %s "
"left in tree.", sha1_to_hex(p->obj), rref, lref, lref);
write_note_to_worktree(p->obj, p->local);
} else {
/* "regular" conflict, checkout result of ll_merge() */
const char *reason = "content";
if (is_null_sha1(p->base))
reason = "add/add";
assert(!is_null_sha1(p->local));
assert(!is_null_sha1(p->remote));
OUTPUT(o, 1, "CONFLICT (%s): Merge conflict in notes for "
"object %s", reason, sha1_to_hex(p->obj));
ll_merge_in_worktree(o, p);
}

trace_printf("\t\t\tremoving from partial merge result\n");
remove_note(t, p->obj);

return 1;
}

static int merge_one_change(struct notes_merge_options *o,
struct notes_merge_pair *p, struct notes_tree *t)
{
/*
* Return 0 if change was resolved (and added to notes_tree),
* 1 if conflict
* Return 0 if change is successfully resolved (stored in notes_tree).
* Return 1 is change results in a conflict (NOT stored in notes_tree,
* but instead written to NOTES_MERGE_WORKTREE with conflict markers).
*/
switch (o->strategy) {
case NOTES_MERGE_RESOLVE_MANUAL:
return 1;
return merge_one_change_manual(o, p, t);
case NOTES_MERGE_RESOLVE_OURS:
OUTPUT(o, 2, "Using local notes for %s", sha1_to_hex(p->obj));
/* nothing to do */
Expand Down Expand Up @@ -479,8 +635,8 @@ int notes_merge(struct notes_merge_options *o,
result = merge_from_diffs(o, base_tree_sha1, local->tree->object.sha1,
remote->tree->object.sha1, local_tree);

if (result > 0) { /* successful non-trivial merge */
/* Commit result */
if (result != 0) { /* non-trivial merge (with or without conflicts) */
/* Commit (partial) result */
struct commit_list *parents = NULL;
commit_list_insert(remote, &parents); /* LIFO order */
commit_list_insert(local, &parents);
Expand Down
11 changes: 10 additions & 1 deletion notes-merge.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#ifndef NOTES_MERGE_H
#define NOTES_MERGE_H

#define NOTES_MERGE_WORKTREE "NOTES_MERGE_WORKTREE"

enum notes_merge_verbosity {
NOTES_MERGE_VERBOSITY_DEFAULT = 2,
NOTES_MERGE_VERBOSITY_MAX = 5
Expand All @@ -17,6 +19,7 @@ struct notes_merge_options {
NOTES_MERGE_RESOLVE_THEIRS,
NOTES_MERGE_RESOLVE_UNION
} strategy;
unsigned has_worktree:1;
};

void init_notes_merge_options(struct notes_merge_options *o);
Expand Down Expand Up @@ -51,7 +54,13 @@ void create_notes_commit(struct notes_tree *t, struct commit_list *parents,
* 2. The merge successfully completes, producing a merge commit. local_tree
* contains the updated notes tree, the SHA1 of the resulting commit is
* written into 'result_sha1', and 1 is returned.
* 3. The merge fails. result_sha1 is set to null_sha1, and -1 is returned.
* 3. The merge results in conflicts. This is similar to #2 in that the
* partial merge result (i.e. merge result minus the unmerged entries)
* are stored in 'local_tree', and the SHA1 or the resulting commit
* (to be amended when the conflicts have been resolved) is written into
* 'result_sha1'. The unmerged entries are written into the
* .git/NOTES_MERGE_WORKTREE directory with conflict markers.
* -1 is returned.
*
* Both o->local_ref and o->remote_ref must be given (non-NULL), but either ref
* (although not both) may refer to a non-existing notes ref, in which case
Expand Down
Loading

0 comments on commit 809f38c

Please sign in to comment.