Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functionality to detect build 'conflicts' while merging #64

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 142 additions & 21 deletions git-imerge
Original file line number Diff line number Diff line change
Expand Up @@ -545,15 +545,84 @@ def reparent(commit, parent_sha1s, msg=None):
return out.strip()


class AutomaticMergeFailed(Exception):
class LogicalFailure(Exception):
def __init__(self, commit1, commit2):
Exception.__init__(
self, 'Automatic merge of %s and %s failed' % (commit1, commit2,)
)
self.commit1, self.commit2 = commit1, commit2


def automerge(commit1, commit2, msg=None):
class AutomaticMergeFailed(LogicalFailure):
pass


class AutomaticTestFailed(LogicalFailure):
pass


class GitConfigError(Exception):
def __init__(self, returncode, output):
Exception.__init__(
self, 'Git config failed with exit code %s: %s' % (returncode, output,)
)


def memo(obj):
cache = {}
@functools.wraps(obj)
def wrap(*args, **kwds):
if args not in cache:
cache[args] = obj(*args, **kwds)
return cache[args]
return wrap


@memo
class GitConfigStore(object):
def __init__(self, name, config_prefix='imerge'):
self.config_prefix = config_prefix
self.config = self._get_all_keys()

def _get_all_keys(self):
d = {}
try:
items_with_prefix = check_output(
['git', 'config', '--get-regex', self.config_prefix]
).rstrip().split('\n')
for row in items_with_prefix:
k, v = row.split()
d[k[len(self.config_prefix + '.'):]] = v
return d
except CalledProcessError:
return {}

def get(self, key):
return self.config.get(key)

def set(self, key, value):
self.config[key] = value
config_key = '.'.join([self.config_prefix, key])
try:
check_call(['git', 'config', config_key, value])
except CalledProcessError as e:
raise GitConfigError(e.returncode, e.output)

def unset(self, key):
if key in self.config:
del self.config[key]
config_key = '.'.join([self.config_prefix, key])
try:
check_call(['git', 'config', '--unset', config_key])
except CalledProcessError as e:
if e.returncode == 5:
# Value was not set
pass
else:
raise GitConfigError(e.returncode, e.output)


def automerge(commit1, commit2, msg=None, test_command=None):
"""Attempt an automatic merge of commit1 and commit2.

Return the SHA1 of the resulting commit, or raise
Expand All @@ -572,8 +641,14 @@ def automerge(commit1, commit2, msg=None):
# added in git version 1.7.4.
call_silently(['git', 'reset', '--merge'])
raise AutomaticMergeFailed(commit1, commit2)
else:
return get_commit_sha1('HEAD')

if test_command is not None:
try:
check_call(['/bin/sh', '-c', test_command])
except CalledProcessError as e:
raise AutomaticTestFailed(commit1, commit2)

return get_commit_sha1('HEAD')


class MergeRecord(object):
Expand Down Expand Up @@ -1374,6 +1449,7 @@ class Block(object):
self.name = name
self.len1 = len1
self.len2 = len2
self.gcs = GitConfigStore(name)

def get_merge_state(self):
"""Return the MergeState instance containing this Block."""
Expand Down Expand Up @@ -1474,11 +1550,17 @@ class Block(object):
'Attempting automerge of %d-%d...' % self.get_original_indexes(i1, i2)
)
try:
automerge(self[i1, 0].sha1, self[0, i2].sha1)
print("Automerging from is_mergeable")
automerge(self[i1, 0].sha1, self[0, i2].sha1,
test_command=self.gcs.get(self.name + '.testcommand'),
)
sys.stderr.write('success.\n')
return True
except AutomaticMergeFailed:
sys.stderr.write('failure.\n')
sys.stderr.write('merge failure.\n')
return False
except AutomaticTestFailed:
sys.stderr.write('test failure.\n')
return False

def auto_outline(self):
Expand All @@ -1497,7 +1579,10 @@ class Block(object):
sys.stderr.write(msg % (i1orig, i2orig))
logmsg = 'imerge \'%s\': automatic merge %d-%d' % (self.name, i1orig, i2orig)
try:
merge = automerge(commit1, commit2, msg=logmsg)
print("Automerging from auto_outline")
merge = automerge(commit1, commit2, msg=logmsg,
test_command=self.gcs.get(self.name + '.testcommand'),
)
sys.stderr.write('success.\n')
except AutomaticMergeFailed as e:
sys.stderr.write('unexpected conflict. Backtracking...\n')
Expand Down Expand Up @@ -1571,10 +1656,12 @@ class Block(object):
sys.stderr.write('Attempting to merge %d-%d...' % (i1orig, i2orig))
logmsg = 'imerge \'%s\': automatic merge %d-%d' % (self.name, i1orig, i2orig)
try:
print("Automerging from auto_fill_micromerge")
merge = automerge(
self[i1, i2 - 1].sha1,
self[i1 - 1, i2].sha1,
msg=logmsg,
test_command=self.gcs.get(self.name + '.testcommand'),
)
sys.stderr.write('success.\n')
except AutomaticMergeFailed:
Expand Down Expand Up @@ -1778,6 +1865,8 @@ class MergeState(Block):
re.VERBOSE,
)

DEFAULT_TEST_COMMAND = None

@staticmethod
def iter_existing_names():
"""Iterate over the names of existing MergeStates in this repo."""
Expand Down Expand Up @@ -1838,26 +1927,19 @@ class MergeState(Block):
"""Set the default merge to the specified one.

name can be None to cause the default to be cleared."""

gcs = GitConfigStore(name)
if name is None:
try:
check_call(['git', 'config', '--unset', 'imerge.default'])
except CalledProcessError as e:
if e.returncode == 5:
# Value was not set
pass
else:
raise
gcs.unset("default")
else:
check_call(['git', 'config', 'imerge.default', name])
gcs.set("default", name)

@staticmethod
def get_default_name():
"""Get the name of the default merge, or None if none is currently set."""

gcs = GitConfigStore(None)
try:
return check_output(['git', 'config', 'imerge.default']).rstrip()
except CalledProcessError:
return gcs.get("default")
except GitConfigError:
return None

@staticmethod
Expand Down Expand Up @@ -1891,7 +1973,7 @@ class MergeState(Block):
name, merge_base,
tip1, commits1,
tip2, commits2,
goal=DEFAULT_GOAL, manual=False, branch=None,
goal=DEFAULT_GOAL, manual=False, branch=None, test_command=None,
):
"""Create and return a new MergeState object."""

Expand All @@ -1915,6 +1997,7 @@ class MergeState(Block):
goal=goal,
manual=manual,
branch=branch,
test_command=test_command,
)

@staticmethod
Expand Down Expand Up @@ -2109,6 +2192,7 @@ class MergeState(Block):
goal=DEFAULT_GOAL,
manual=False,
branch=None,
test_command=None,
):
Block.__init__(self, name, len(commits1) + 1, len(commits2) + 1)
self.tip1 = tip1
Expand Down Expand Up @@ -2647,6 +2731,20 @@ def read_merge_state(name=None, default_to_unique=True):

@Failure.wrap
def main(args):
def add_test_command_argument(subparser):
subparser.add_argument(
'--test-command',
action='store', default=None,
help=(
'in addition to identifying for textual conflicts, run the test '
'or test script specified by TEST to identify where logical '
'conflicts are introduced. The test script is expected to return 0 '
'if the source is good, exit with code 1-127 if the source is bad, '
'except for exit code 125 which indicates the source code can not '
'be built or tested.'
),
)

parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
Expand Down Expand Up @@ -2675,6 +2773,7 @@ def main(args):
action='store', default=None,
help='the name of the branch to which the result will be stored',
)
add_test_command_argument(subparser)
subparser.add_argument(
'--manual',
action='store_true', default=False,
Expand Down Expand Up @@ -2716,6 +2815,7 @@ def main(args):
action='store', default=None,
help='the name of the branch to which the result will be stored',
)
add_test_command_argument(subparser)
subparser.add_argument(
'--manual',
action='store_true', default=False,
Expand Down Expand Up @@ -2754,6 +2854,7 @@ def main(args):
action='store', default=None,
help='the name of the branch to which the result will be stored',
)
add_test_command_argument(subparser)
subparser.add_argument(
'--manual',
action='store_true', default=False,
Expand Down Expand Up @@ -2894,6 +2995,7 @@ def main(args):
action='store', default=None,
help='the name of the branch to which the result will be stored',
)
add_test_command_argument(subparser)
subparser.add_argument(
'--manual',
action='store_true', default=False,
Expand Down Expand Up @@ -3040,9 +3142,14 @@ def main(args):
tip2, commits2,
goal=options.goal, manual=options.manual,
branch=(options.branch or options.name),
test_command=options.test_command,
)
merge_state.save()
MergeState.set_default_name(options.name)
gcs = GitConfigStore(options.name)
if options.test_command is not None:
gcs.set(options.name + '.testcommand', options.test_command)

elif options.subcommand == 'start':
require_clean_work_tree('proceed')

Expand All @@ -3068,9 +3175,13 @@ def main(args):
tip2, commits2,
goal=options.goal, manual=options.manual,
branch=(options.branch or options.name),
test_command=options.test_command,
)
merge_state.save()
MergeState.set_default_name(options.name)
gcs = GitConfigStore(options.name)
if options.test_command is not None:
gcs.set(options.name + '.testcommand', options.test_command)

try:
merge_state.auto_complete_frontier()
Expand Down Expand Up @@ -3126,9 +3237,13 @@ def main(args):
tip2, commits2,
goal=options.goal, manual=options.manual,
branch=options.branch,
test_command=options.test_command,
)
merge_state.save()
MergeState.set_default_name(name)
gcs = GitConfigStore(options.name)
if options.test_command is not None:
gcs.set(options.name + '.testcommand', options.test_command)

try:
merge_state.auto_complete_frontier()
Expand Down Expand Up @@ -3188,9 +3303,13 @@ def main(args):
tip2, commits2,
goal=options.goal, manual=options.manual,
branch=options.branch,
test_command=options.test_command,
)
merge_state.save()
MergeState.set_default_name(options.name)
gcs = GitConfigStore(options.name)
if options.test_command is not None:
gcs.set(options.name + '.testcommand', options.test_command)

try:
merge_state.auto_complete_frontier()
Expand Down Expand Up @@ -3265,6 +3384,8 @@ def main(args):
merge_state.save()
merge_state.simplify(refname, force=options.force)
MergeState.remove(merge_state.name)
gcs = GitConfigStore(merge_state.name)
gcs.unset(merge_state.name + '.testcommand')
elif options.subcommand == 'diagram':
if not (options.commits or options.frontier):
options.frontier = True
Expand Down
Loading