Skip to content

Commit

Permalink
Add git-unresolve <paths>...
Browse files Browse the repository at this point in the history
This is an attempt to address the issue raised on #git channel
recently by Carl Worth.

After a conflicted automerge, "git diff" shows a combined diff
to give you how the tentative automerge result differs from
what came from each branch.  During a complex merge, it is
tempting to be able to resolve a few paths at a time, mark
them "I've dealt with them" with git-update-index to unclutter
the next "git diff" output, and keep going.  However, when the
final result does not compile or otherwise found to be a
mismerge, the workflow to fix the mismerged paths suddenly
changes to "git diff HEAD -- path" (to get a diff from our
HEAD before merging) and "git diff MERGE_HEAD -- path" (to get
a diff from theirs), and it cannot show the combined anymore.

With git-unresolve <paths>..., the versions from our branch and
their branch for specified blobs are placed in stage #2 and
stage #3, without touching the working tree files.  This gives
you the combined diff back for easier review, along with
"diff --ours" and "diff --theirs".

One thing it does not do is to place the base in stage #1; this
means "diff --base" would behave differently between the run
immediately after a conflicted three-way merge, and the run
after an update-index by mistake followed by a git-unresolve.

We could theoretically run merge-base between HEAD and
MERGE_HEAD to find which tree to place in stage #1, but
reviewing "diff --base" is not that useful so....

Signed-off-by: Junio C Hamano <junkio@cox.net>
  • Loading branch information
Junio C Hamano committed Apr 19, 2006
1 parent a4d0cce commit ec16779
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ git-tag
git-tar-tree
git-unpack-file
git-unpack-objects
git-unresolve
git-update-index
git-update-ref
git-update-server-info
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ PROGRAMS = \
git-upload-pack$X git-verify-pack$X git-write-tree$X \
git-update-ref$X git-symbolic-ref$X git-check-ref-format$X \
git-name-rev$X git-pack-redundant$X git-repo-config$X git-var$X \
git-describe$X git-merge-tree$X git-blame$X git-imap-send$X
git-describe$X git-merge-tree$X git-blame$X git-imap-send$X \
git-unresolve$X

BUILT_INS = git-log$X

Expand Down
146 changes: 146 additions & 0 deletions unresolve.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#include "cache.h"
#include "tree-walk.h"

static const char unresolve_usage[] =
"git-unresolve <paths>...";

static struct cache_file cache_file;
static unsigned char head_sha1[20];
static unsigned char merge_head_sha1[20];

static struct cache_entry *read_one_ent(const char *which,
unsigned char *ent, const char *path,
int namelen, int stage)
{
unsigned mode;
unsigned char sha1[20];
int size;
struct cache_entry *ce;

if (get_tree_entry(ent, path, sha1, &mode)) {
error("%s: not in %s branch.", path, which);
return NULL;
}
if (mode == S_IFDIR) {
error("%s: not a blob in %s branch.", path, which);
return NULL;
}
size = cache_entry_size(namelen);
ce = xcalloc(1, size);

memcpy(ce->sha1, sha1, 20);
memcpy(ce->name, path, namelen);
ce->ce_flags = create_ce_flags(namelen, stage);
ce->ce_mode = create_ce_mode(mode);
return ce;
}

static int unresolve_one(const char *path)
{
int namelen = strlen(path);
int pos;
int ret = 0;
struct cache_entry *ce_2 = NULL, *ce_3 = NULL;

/* See if there is such entry in the index. */
pos = cache_name_pos(path, namelen);
if (pos < 0) {
/* If there isn't, either it is unmerged, or
* resolved as "removed" by mistake. We do not
* want to do anything in the former case.
*/
pos = -pos-1;
if (pos < active_nr) {
struct cache_entry *ce = active_cache[pos];
if (ce_namelen(ce) == namelen &&
!memcmp(ce->name, path, namelen)) {
fprintf(stderr,
"%s: skipping still unmerged path.\n",
path);
goto free_return;
}
}
}

/* Grab blobs from given path from HEAD and MERGE_HEAD,
* stuff HEAD version in stage #2,
* stuff MERGE_HEAD version in stage #3.
*/
ce_2 = read_one_ent("our", head_sha1, path, namelen, 2);
ce_3 = read_one_ent("their", merge_head_sha1, path, namelen, 3);

if (!ce_2 || !ce_3) {
ret = -1;
goto free_return;
}
if (!memcmp(ce_2->sha1, ce_3->sha1, 20) &&
ce_2->ce_mode == ce_3->ce_mode) {
fprintf(stderr, "%s: identical in both, skipping.\n",
path);
goto free_return;
}

remove_file_from_cache(path);
if (add_cache_entry(ce_2, ADD_CACHE_OK_TO_ADD)) {
error("%s: cannot add our version to the index.", path);
ret = -1;
goto free_return;
}
if (!add_cache_entry(ce_3, ADD_CACHE_OK_TO_ADD))
return 0;
error("%s: cannot add their version to the index.", path);
ret = -1;
free_return:
free(ce_2);
free(ce_3);
return ret;
}

static void read_head_pointers(void)
{
if (read_ref(git_path("HEAD"), head_sha1))
die("Cannot read HEAD -- no initial commit yet?");
if (read_ref(git_path("MERGE_HEAD"), merge_head_sha1)) {
fprintf(stderr, "Not in the middle of a merge.\n");
exit(0);
}
}

int main(int ac, char **av)
{
int i;
int err = 0;
int newfd;

if (ac < 2)
usage(unresolve_usage);

git_config(git_default_config);

/* Read HEAD and MERGE_HEAD; if MERGE_HEAD does not exist, we
* are not doing a merge, so exit with success status.
*/
read_head_pointers();

/* Otherwise we would need to update the cache. */
newfd= hold_index_file_for_update(&cache_file, get_index_file());
if (newfd < 0)
die("unable to create new cachefile");

if (read_cache() < 0)
die("cache corrupted");

for (i = 1; i < ac; i++) {
char *arg = av[i];
err |= unresolve_one(arg);
}
if (err)
die("Error encountered; index not updated.");

if (active_cache_changed) {
if (write_cache(newfd, active_cache, active_nr) ||
commit_index_file(&cache_file))
die("Unable to write new cachefile");
}
return 0;
}

0 comments on commit ec16779

Please sign in to comment.