Greg Nisbet
Greg Nisbet

Reputation: 6994

git "trivial" merge strategy that directly commits merge conflicts

Suppose I have a branch b tracking a local branch master.

I'm trying to write a script to cherry-pick all the commits of b as a single unit on top of whatever tree master is currently pointing to.

Because this is part of a non-interactive script, it's important that the cherry-pick always succeeds and never falls back to interactive input.

Is there a merge strategy or some combination of flags that can be used to direct git to commit a merge conflict directly?

I'm okay with amending the commit after the fact to remove the merge conflict.


The point of this is mostly to learn how to script git, and only partially to automate part of my current workflow. I'm aware that constantly cherry-picking is not The Git Way and that I'm throwing away local development history. Using tons of local branches that all track each other is also not The Git Way in all likelihood.

For the purposes of this question, please consider a history in the local repository that is neat and tidy when viewed from the outside world as more important than an accurate local history.


So, here's an example of the situation I'm trying to resolve.

create sandbox directory

$ mkdir -p /tmp/gitdir

navigate to sandbox directory

$ cd /tmp/gitdir

create git repo and master branch

$ git init

write file, add to git, commit.

$ echo master > foo.txt`
$ git add foo.txt`
$ git commit -m 'user commit 1'`
[master (root-commit) e9bcb91] user commit 1
1 file changed, 1 insertion(+)
create mode 100644 foo.txt

create new branch b

$ git checkout -b b
Switched to a new branch 'b'

change contents of foo.txt and commit

$ echo b1 > foo.txt
$ git add -u
$ git commit -m 'user commit 2'

set b to track master

$ git branch -u master

create branch c

$ git checkout -b c

track b from c

$ git branch -u b

add 2 commits to branch c

$ echo c1 > foo.txt
$ git add -u
$ git commit -m 'user commit 3'
[c 04da4ab] user commit 3
1 file changed, 1 insertion(+), 1 deletion(-)
$ echo c2 > foo.txt
$ git add -u > foo.txt
$ git commit -m 'user commit 4'
[c 17df476] user commit 4
1 file changed, 1 insertion(+), 1 deletion(-)

go back to b, and add a commit.

$ git checkout b
Switched to branch 'b'
Your branch is ahead of 'master' by 1 commit.
  (use "git push" to publish your local commits)

$ echo b2 > foo.txt
$ git add -u
$ git commit -m 'user commit 5'
[b 30f68fa] user commit 5
 1 file changed, 1 insertion(+), 1 deletion(-)

go back to branch c.

$ git checkout c
Switched to branch 'c'
Your branch and 'b' have diverged,
and have 2 and 1 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

So, we have a couple of choices about how to repair this situation.

Most of the time what I want to do in a situation like this is move the changes in one branch directly after all the changes in another.

In a situation like this, rebase is correct most of the time, but sometimes pulls in obsolete commits. What I'm really trying to do is move the contents of the branch, considered as a patch or delta forward in the graph.


Appendix I

Here's my attempt to write a script to automate cherry-picking the contents of a branch on top of the branch it is tracking.

The current problem with it is that the git cherry-pick subprocess sometimes gives up because of a merge conflict, I want it to just commit the conflicted files.

Please consider this script a proof of work token of sorts. Feedback on the script itself, while appreciated, is not the thrust of the question. The script is mostly here to be concrete "evidence" of what I'm trying to do and why.

#!/usr/bin/env perl

use strict;
use warnings;
use Carp;
use Data::Dumper;

use vars qw[*CHERRY_PICK_SINK];

BEGIN {
    $Carp::Verbose = 1;
}

# accepts: command string default command interpreter
# returns: lines of output with $/ stripped, error status
sub capture_lines {
    local ${^CHILD_ERROR_NATIVE};
    my ($cmd) = @_;
    croak if ref $cmd;
    my @o = `$cmd`;
    chomp foreach @o;
    return [@o], ${^CHILD_ERROR_NATIVE};
}

# accepts: ()
# returns: UUID, error
sub get_uuid {
    my $err;
    my $cmd = q[python -c 'import uuid; print(str(uuid.uuid4()))'];
    my $lines;
    ($lines, $err) = capture_lines($cmd);
    return undef, $err if $err;
    if (@$lines <= 0) {
        return [undef, 'empty output'];
    }
    my $line = $lines->[0];
    return $line, undef;
}

# accepts: ()
# returns: aref of hashes for current branch, error status
sub current_branch_hashes {
    my $cmd = q[git log --format="%H" '@{upstream}..HEAD'];
    my ($name, $err) = capture_lines($cmd);
    return $name, $err;
}

# accepts: ()
# returns: name of current branch
sub current_branch_name {
    my $cmd = q[git rev-parse --abbrev-ref --symbolic-full-name HEAD];
    my ($lines, $err) = capture_lines($cmd);
    my $name = $lines->[0];
    return $name, $err;
}

# accepts: ()
# returns: name of upstream, error status
sub current_branch_upstream_name {
    my $cmd = q[git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'];
    my ($lines, $err) = capture_lines($cmd);
    my $name = $lines->[0];
    return $name, $err;
}

# accepts: committish (be careful)
# returns: hash, error code
sub rev_parse {
    my ($name) = @_;
    croak if ref $name;
    my $name_quoted = quotemeta($name);
    my $cmd = "git rev-parse ${name_quoted}";
    my ($lines, $err) = capture_lines($cmd);
    return $lines->[0], $err;
}

# accepts: branch_name, committish
# returns: error code
sub assign_branch {
    my ($key, $value) = @_;
    croak if ref $key;
    croak if ref $value;
    my $key_quoted = quotemeta($key);
    my $value_quoted = quotemeta($value);
    my $cmd = "git branch -f $key_quoted $value_quoted";
    my (undef, $err) = capture_lines($cmd);
    return $err;
}

# accepts: branch_name
# returns: error code
sub delete_branch {
    my ($key) = @_;
    croak if ref $key;
    my $key_quoted = quotemeta($key);
    my $cmd = "git branch -D ${key_quoted}";
    my $err;
    (undef, $err) = capture_lines($cmd);
    return $err;
}

# accepts: label1, label2
# returns: error status
# note: swaps the where the branch labels point to
sub swap_branch_labels {
    my ($label1, $label2) = @_;
    croak if ref $label1;
    croak if ref $label2;
    my ($hash1, $hash2, $err);
    ($hash1, $err) = rev_parse($label1);
    return $err if $err;
    ($hash2, $err) = rev_parse($label2);
    return $err if $err;
    $err = assign_branch($label1, $hash2);
    return $err if $err;
    $err = assign_branch($label2, $hash1);
    return $err if $err;
}

# accepts: committish
# returns: error status
sub checkout_old {
    my ($name) = @_;
    my $name_quoted = quotemeta($name);
    my $cmd = "git checkout ${name_quoted}";
    (undef, my $err) = capture_lines($cmd);
    return $err;
}

# accepts: name
# returns: error status
sub checkout_new {
    my ($name) = @_;
    my $name_quoted = quotemeta($name);
    my $cmd = "git checkout -b ${name_quoted}";
    (undef, my $err) = capture_lines($cmd);
    return $err;
}

# accepts: aref of commit hashes
# returns: exit status
sub cherrypick_aref {
    local *CHERRY_PICK_SINK;
    local ${^CHILD_ERROR_NATIVE};
    my ($hashes) = @_;
    my $cmd = 'git cherry-pick --stdin';
    open CHERRY_PICK_SINK, '|-', $cmd;
    for my $item (@$hashes) {
        chomp($item);
        print CHERRY_PICK_SINK "$item\n";
    }
    close CHERRY_PICK_SINK;
    return ${^CHILD_ERROR_NATIVE};
}

# accepts: ()
# returns: error
sub cherrypick_self {
    my ($hashes, $err) = current_branch_hashes();
    return "current_branch_hashes: $err" if $err;
    return "cherrypick_self: empty hashes" unless @$hashes >= 1;
    my $current_branch;
    ($current_branch, $err) = current_branch_name();
    return "current_branch_name: $err" if $err;
    my $temp_branch;
    ($temp_branch, $err) = get_uuid();
    return "get_uuid: $err" if $err;
    my $upstream;
    ($upstream, $err) = current_branch_upstream_name();
    return "current_branch_upstream_name: $err" if $err;
    $err = checkout_old($upstream);
    return "checkout_old: $err" if $err;
    $err = checkout_new($temp_branch);
    return "checkout_new: $err" if $err;
    $err = cherrypick_aref($hashes);
    return "cherry-pick: $err" if $err;
    $err = swap_branch_labels($temp_branch, $current_branch);
    return "swap branch labels: $err" if $err;
    $err = delete_branch($temp_branch);
    return "delete branch: $err" if $err;
}

cherrypick_self();

Upvotes: 1

Views: 293

Answers (1)

torek
torek

Reputation: 489233

Is there a merge strategy or some combination of flags that can be used to direct git to commit a merge conflict directly?

No. You can write one, but this is taking on a very big chunk of responsibility. You can then delegate almost all of that responsibility, but it's going to be somewhat tricky.

A rebase's -s option uses git cherry-pick to invoke the merge machinery, including with the option of providing -s strategy. In other words, you can use git rebase -s resolve instead of git rebase -s recursive.1 This in turn means you can write your own strategy, and place it in your $PATH as an executable named, e.g., git-merge-gregory. Running git rebase -s gregory would then invoke your git-merge-gregory program on each commit to be cherry-picked.

Unfortunately, there is zero documentation on how git-merge-strategy is actually invoked. In the old git stash script, it was possible to see how to invoke it directly. So we should look at an old Git, such as 2.6.0, to find these lines in its git-stash.sh. I won't quote most of them but there are some magic exports to set environment variables to label the commits, followed by this bit:

if git merge-recursive $b_tree -- $c_tree $w_tree
then
    # No conflict

So your git-merge-gregory should be an executable program that takes at least the hash ID of the merge base commit-or-tree $b_base, a double-dash, the hash ID of the "theirs" commit-or-tree $c_tree, and the hash ID of the current commit-or-tree $w_tree. (You might also be passed additional -X arguments that the user passed to the git rebase command, of course.)

Your program must now complete the entire merge and exit with a zero status to indicate that the merge was successful, or leave a merge-mess behind in the index and work-tree and exit nonzero to indicate that the user should clean up after you.

Fortunately, what you can do at this point is cheat: invoke git-merge-recursive with all these arguments and inspect its exit status. If it exited zero, you're done. If it exited nonzero, you can have your program attempt to clean up the mess git-merge-recursive left behind, using whatever code you like. That's probably the way to go for your experiment.


1This particular example is pointless, because rebase invokes git-merge-strategy directly, providing it exactly one merge base commit hash ID. The difference between -s resolve and -s recursive emerges only when git merge invokes the strategy with more than one merge base. So these two behave exactly the same in all rebase cherry-pick cases.


The point of this is mostly to learn how to script git ...

This is probably the wrong task for that. Most Git scripts involve running git rev-parse with various options and/or running git rev-list with various options, getting hash IDs from them, then running other Git plumbing commands on those hash IDs. Usually this all deals with the index in simple ways. Merge is big and hard, with a lot of corner cases and special handling of Git's index, where the index gets expanded to hold up to three copies of each file, instead of just one copy of each file.

Upvotes: 3

Related Questions