From dc7622e8afddcdd32d9bc56a4d962ff6cb1a99e6 Mon Sep 17 00:00:00 2001 From: David Leen Date: Fri, 2 May 2014 10:05:00 -0700 Subject: [PATCH 1/4] Add functionality to detect build 'conflicts' while merging This commit adds functionality to test for build failures during the merge process. The build script is specified via the command line flag: --build-command=make_script. The make_script should return 0 if the source is good, 1-127 if the source is bad, except for code 125 if the source at this commit cannot be built or cannot be tested. Please see the man page for git-bisect for examples of such a build script. --- git-imerge | 125 +++++++++++++++++++++++++++++++++++++++++++-- t/create-test-repo | 51 ++++++++++++++++++ t/reset-test-repo | 5 ++ t/test-build | 60 ++++++++++++++++++++++ 4 files changed, 237 insertions(+), 4 deletions(-) create mode 100755 t/test-build diff --git a/git-imerge b/git-imerge index 457fdab..30f0d92 100755 --- a/git-imerge +++ b/git-imerge @@ -553,6 +553,14 @@ class AutomaticMergeFailed(Exception): self.commit1, self.commit2 = commit1, commit2 +class AutomaticBuildFailed(Exception): + def __init__(self, commit1, commit2): + Exception.__init__( + self, 'Automatic build of %s and %s failed' % (commit1, commit2,) + ) + self.commit1, self.commit2 = commit1, commit2 + + def automerge(commit1, commit2, msg=None): """Attempt an automatic merge of commit1 and commit2. @@ -572,8 +580,18 @@ 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') + + build_script = MergeState.get_default_test_command() + if build_script: + try: + check_call(['/bin/sh', build_script]) + except CalledProcessError as e: + if e.returncode == 125: + return get_commit_sha1('HEAD') + else: + raise AutomaticBuildFailed(commit1, commit2) + + return get_commit_sha1('HEAD') class MergeRecord(object): @@ -1474,11 +1492,15 @@ class Block(object): 'Attempting automerge of %d-%d...' % self.get_original_indexes(i1, i2) ) try: + print("Automerging from is_mergeable") automerge(self[i1, 0].sha1, self[0, i2].sha1) sys.stderr.write('success.\n') return True except AutomaticMergeFailed: - sys.stderr.write('failure.\n') + sys.stderr.write('Merge failure.\n') + return False + except AutomaticBuildFailed: + sys.stderr.write('Build failure.\n') return False def auto_outline(self): @@ -1497,6 +1519,7 @@ class Block(object): sys.stderr.write(msg % (i1orig, i2orig)) logmsg = 'imerge \'%s\': automatic merge %d-%d' % (self.name, i1orig, i2orig) try: + print("Automerging from auto_outline") merge = automerge(commit1, commit2, msg=logmsg) sys.stderr.write('success.\n') except AutomaticMergeFailed as e: @@ -1571,6 +1594,7 @@ 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, @@ -1778,6 +1802,8 @@ class MergeState(Block): re.VERBOSE, ) + DEFAULT_BUILD_COMMAND = None + @staticmethod def iter_existing_names(): """Iterate over the names of existing MergeStates in this repo.""" @@ -1860,6 +1886,38 @@ class MergeState(Block): except CalledProcessError: return None + @staticmethod + def set_default_test_command(name): + """Set the default test command to the specified one. + + name can be None to cause the default to be cleared.""" + + if name is None: + try: + check_call(['git', 'config', '--unset', 'imerge.command']) + except CalledProcessError as e: + if e.returncode == 5: + # Value was not set + pass + else: + raise + else: + check_call(['git', 'config', 'imerge.command', name]) + + @staticmethod + def get_default_test_command(): + """Get the name of the test command, or None if none is currently set.""" + + if MergeState.DEFAULT_BUILD_COMMAND: + return MergeState.DEFAULT_BUILD_COMMAND + else: + try: + MergeState.DEFAULT_BUILD_COMMAND = \ + check_output(['git', 'config', 'imerge.command']).rstrip() + return MergeState.DEFAULT_BUILD_COMMAND + except CalledProcessError: + return None + @staticmethod def _check_no_merges(commits): multiparent_commits = [ @@ -1891,7 +1949,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, build_command=None, ): """Create and return a new MergeState object.""" @@ -1915,6 +1973,7 @@ class MergeState(Block): goal=goal, manual=manual, branch=branch, + build_command=build_command, ) @staticmethod @@ -2109,6 +2168,7 @@ class MergeState(Block): goal=DEFAULT_GOAL, manual=False, branch=None, + build_command=None, ): Block.__init__(self, name, len(commits1) + 1, len(commits2) + 1) self.tip1 = tip1 @@ -2116,6 +2176,7 @@ class MergeState(Block): self.goal = goal self.manual = bool(manual) self.branch = branch or name + self.build_command = build_command # A simulated 2D array. Values are None or MergeRecord instances. self._data = [[None] * self.len2 for i1 in range(self.len1)] @@ -2675,6 +2736,18 @@ def main(args): action='store', default=None, help='the name of the branch to which the result will be stored', ) + subparser.add_argument( + '--build-command', + action='store', default=None, + help=( + 'in addition to identifying for textual conflicts, run the build ' + '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.' + ), + ) subparser.add_argument( '--manual', action='store_true', default=False, @@ -2716,6 +2789,18 @@ def main(args): action='store', default=None, help='the name of the branch to which the result will be stored', ) + subparser.add_argument( + '--build-command', + action='store', default=None, + help=( + 'in addition to identifying for textual conflicts, run the build ' + '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.' + ), + ) subparser.add_argument( '--manual', action='store_true', default=False, @@ -2754,6 +2839,18 @@ def main(args): action='store', default=None, help='the name of the branch to which the result will be stored', ) + subparser.add_argument( + '--build-command', + action='store', default=None, + help=( + 'in addition to identifying for textual conflicts, run the build ' + '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.' + ), + ) subparser.add_argument( '--manual', action='store_true', default=False, @@ -2894,6 +2991,18 @@ def main(args): action='store', default=None, help='the name of the branch to which the result will be stored', ) + subparser.add_argument( + '--build-command', + action='store', default=None, + help=( + 'in addition to identifying for textual conflicts, run the build ' + '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.' + ), + ) subparser.add_argument( '--manual', action='store_true', default=False, @@ -3040,9 +3149,11 @@ def main(args): tip2, commits2, goal=options.goal, manual=options.manual, branch=(options.branch or options.name), + build_command=options.build_command, ) merge_state.save() MergeState.set_default_name(options.name) + MergeState.set_default_test_command(options.build_command) elif options.subcommand == 'start': require_clean_work_tree('proceed') @@ -3068,9 +3179,11 @@ def main(args): tip2, commits2, goal=options.goal, manual=options.manual, branch=(options.branch or options.name), + build_command=options.build_command, ) merge_state.save() MergeState.set_default_name(options.name) + MergeState.set_default_test_command(options.build_command) try: merge_state.auto_complete_frontier() @@ -3126,9 +3239,11 @@ def main(args): tip2, commits2, goal=options.goal, manual=options.manual, branch=options.branch, + build_command=options.build_command, ) merge_state.save() MergeState.set_default_name(name) + MergeState.set_default_test_command(options.build_command) try: merge_state.auto_complete_frontier() @@ -3188,9 +3303,11 @@ def main(args): tip2, commits2, goal=options.goal, manual=options.manual, branch=options.branch, + build_command=options.build_command, ) merge_state.save() MergeState.set_default_name(options.name) + MergeState.set_default_test_command(options.build_command) try: merge_state.auto_complete_frontier() diff --git a/t/create-test-repo b/t/create-test-repo index d9e10a4..98230e7 100755 --- a/t/create-test-repo +++ b/t/create-test-repo @@ -1,6 +1,7 @@ #! /bin/sh set -e +set -x DESCRIPTION="git-imerge test repository" @@ -11,6 +12,20 @@ modify() { git add "$filename" } +modify_buildable() { + filename="$1" + text="$2" + cat > $filename << EOF +#include + +int main() +{ + $text +} +EOF + git add "$filename" +} + BASE="$(dirname "$(cd $(dirname "$0") && pwd)")" TMP="$BASE/t/tmp" @@ -79,6 +94,42 @@ do git commit -m "d$i" done +############### +# Build setup # +############### +git checkout -b build master -- +modify_buildable hello.c printf\(\"0\\n\"\)\; +cat > make_script << EOF +#/bin/bash -ex +gcc -o hello hello.c +EOF +chmod +x make_script +git add make_script +git commit -m "Hello World" + +git checkout -b build-left build -- +for i in $(seq 10) +do + modify_buildable hello.c 'printf("'$i' top\n"); + printf("'$i' bot\n");' + git commit -m "build $i" +done + +git checkout -b build-right build -- +modify_buildable hello.c printf\(\"2\\n\"\)\; +git commit -m "Conflicts" +modify_buildable hello.c printf\(\"2\\n\"\; +git commit -m "Conflicts, breaks build" +modify_buildable hello.c 'printf("'2' top\n"); + printf("'2' bot\n"); + typo' +git commit -m "No conflict, breaks build" +for i in $(seq 4 6) +do + modify_buildable hello.c printf\(\"$i\\n\"\)\; + git commit -m "build $i" +done + git checkout master -- ln -s ../../imerge.css diff --git a/t/reset-test-repo b/t/reset-test-repo index 03645b9..7acf006 100755 --- a/t/reset-test-repo +++ b/t/reset-test-repo @@ -20,3 +20,8 @@ do git update-ref -d refs/heads/$b done +"$GIT_IMERGE" remove --name=build-left-right +for b in build-left-right +do + git update-ref -d refs/heads/$b +done diff --git a/t/test-build b/t/test-build new file mode 100755 index 0000000..a269b09 --- /dev/null +++ b/t/test-build @@ -0,0 +1,60 @@ +#! /bin/sh + +# This should be executed in a clean working copy of the test repo. + +set -e +set -x + +modify_buildable() { + filename="$1" + text="$2" + cat > $filename << EOF +#include + +int main() +{ + $text +} +EOF + git add "$filename" +} + +BASE="$(dirname "$(cd $(dirname "$0") && pwd)")" +TMP="$BASE/t/tmp" +GIT_IMERGE="$BASE/git-imerge" + +cd "$TMP" + +# Clean up detritus from possible previous runs of this test: +git checkout master +"$GIT_IMERGE" remove --name=build-left-right || true +for b in build-left-right-full +do + git branch -D $b || true +done + +git checkout build-left +"$GIT_IMERGE" start --goal=rebase-with-history --first-parent \ + --build-command="make_script" \ + --name=build-left-right --branch=build-left-right-full build-right + +# Resolve conflict +modify_buildable hello.c 'printf("'1' top\n"); + printf("'1' bot\n");' +GIT_EDITOR=cat git commit +"$GIT_IMERGE" continue --no-edit +# Resolve conflict which breaks build +modify_buildable hello.c printf\(\"2\\n\"\; +GIT_EDITOR=cat git commit +"$GIT_IMERGE" continue --no-edit +# Resolve the build breakage, no conflict +modify_buildable hello.c 'printf("'2' top\n"); + printf("'2' bot\n");' +GIT_EDITOR=cat git commit +"$GIT_IMERGE" continue --no-edit +# modify_buildable hello.c 'printf("'1' top\n"); +# printf("'1' bot\n");' +# GIT_EDITOR=cat git commit +# "$GIT_IMERGE" continue --no-edit + +# "$GIT_IMERGE" finish From 4cd15a7608e78c68250f9b06f00fa746f92c2a77 Mon Sep 17 00:00:00 2001 From: David Leen Date: Sat, 24 May 2014 00:03:12 -0700 Subject: [PATCH 2/4] Responded to code review comments https://github.com/mhagger/git-imerge/pull/64#issuecomment-42532424 --- git-imerge | 140 ++++++++++++++++++++--------------------------------- 1 file changed, 53 insertions(+), 87 deletions(-) diff --git a/git-imerge b/git-imerge index 30f0d92..8cded95 100755 --- a/git-imerge +++ b/git-imerge @@ -545,7 +545,7 @@ 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,) @@ -553,12 +553,12 @@ class AutomaticMergeFailed(Exception): self.commit1, self.commit2 = commit1, commit2 -class AutomaticBuildFailed(Exception): - def __init__(self, commit1, commit2): - Exception.__init__( - self, 'Automatic build of %s and %s failed' % (commit1, commit2,) - ) - self.commit1, self.commit2 = commit1, commit2 +class AutomaticMergeFailed(LogicalFailure): + pass + + +class AutomaticTestFailed(LogicalFailure): + pass def automerge(commit1, commit2, msg=None): @@ -581,15 +581,12 @@ def automerge(commit1, commit2, msg=None): call_silently(['git', 'reset', '--merge']) raise AutomaticMergeFailed(commit1, commit2) - build_script = MergeState.get_default_test_command() - if build_script: + test_script = MergeState.get_default_test_command() + if test_script is not None: try: - check_call(['/bin/sh', build_script]) + check_call(['/bin/sh', '-c', test_script]) except CalledProcessError as e: - if e.returncode == 125: - return get_commit_sha1('HEAD') - else: - raise AutomaticBuildFailed(commit1, commit2) + raise AutomaticTestFailed(commit1, commit2) return get_commit_sha1('HEAD') @@ -1497,10 +1494,10 @@ class Block(object): sys.stderr.write('success.\n') return True except AutomaticMergeFailed: - sys.stderr.write('Merge failure.\n') + sys.stderr.write('merge failure.\n') return False - except AutomaticBuildFailed: - sys.stderr.write('Build failure.\n') + except AutomaticTestFailed: + sys.stderr.write('test failure.\n') return False def auto_outline(self): @@ -1802,7 +1799,7 @@ class MergeState(Block): re.VERBOSE, ) - DEFAULT_BUILD_COMMAND = None + DEFAULT_TEST_COMMAND = None @staticmethod def iter_existing_names(): @@ -1867,7 +1864,7 @@ class MergeState(Block): if name is None: try: - check_call(['git', 'config', '--unset', 'imerge.default']) + check_call(['git', 'config', '--unset-all', 'imerge.default']) except CalledProcessError as e: if e.returncode == 5: # Value was not set @@ -1894,7 +1891,7 @@ class MergeState(Block): if name is None: try: - check_call(['git', 'config', '--unset', 'imerge.command']) + check_call(['git', 'config', '--unset-all', 'imerge.testcommand']) except CalledProcessError as e: if e.returncode == 5: # Value was not set @@ -1902,22 +1899,22 @@ class MergeState(Block): else: raise else: - check_call(['git', 'config', 'imerge.command', name]) + check_call(['git', 'config', 'imerge.testcommand', name]) @staticmethod def get_default_test_command(): """Get the name of the test command, or None if none is currently set.""" - if MergeState.DEFAULT_BUILD_COMMAND: - return MergeState.DEFAULT_BUILD_COMMAND - else: + if MergeState.DEFAULT_TEST_COMMAND is None: try: - MergeState.DEFAULT_BUILD_COMMAND = \ - check_output(['git', 'config', 'imerge.command']).rstrip() - return MergeState.DEFAULT_BUILD_COMMAND + MergeState.DEFAULT_TEST_COMMAND = \ + check_output(['git', 'config', 'imerge.testcommand']).rstrip() + return MergeState.DEFAULT_TEST_COMMAND except CalledProcessError: return None + return MergeState.DEFAULT_TEST_COMMAND + @staticmethod def _check_no_merges(commits): multiparent_commits = [ @@ -1949,7 +1946,7 @@ class MergeState(Block): name, merge_base, tip1, commits1, tip2, commits2, - goal=DEFAULT_GOAL, manual=False, branch=None, build_command=None, + goal=DEFAULT_GOAL, manual=False, branch=None, test_command=None, ): """Create and return a new MergeState object.""" @@ -1973,7 +1970,7 @@ class MergeState(Block): goal=goal, manual=manual, branch=branch, - build_command=build_command, + test_command=test_command, ) @staticmethod @@ -2168,7 +2165,7 @@ class MergeState(Block): goal=DEFAULT_GOAL, manual=False, branch=None, - build_command=None, + test_command=None, ): Block.__init__(self, name, len(commits1) + 1, len(commits2) + 1) self.tip1 = tip1 @@ -2176,7 +2173,6 @@ class MergeState(Block): self.goal = goal self.manual = bool(manual) self.branch = branch or name - self.build_command = build_command # A simulated 2D array. Values are None or MergeRecord instances. self._data = [[None] * self.len2 for i1 in range(self.len1)] @@ -2708,6 +2704,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, @@ -2736,18 +2746,7 @@ def main(args): action='store', default=None, help='the name of the branch to which the result will be stored', ) - subparser.add_argument( - '--build-command', - action='store', default=None, - help=( - 'in addition to identifying for textual conflicts, run the build ' - '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.' - ), - ) + add_test_command_argument(subparser) subparser.add_argument( '--manual', action='store_true', default=False, @@ -2789,18 +2788,7 @@ def main(args): action='store', default=None, help='the name of the branch to which the result will be stored', ) - subparser.add_argument( - '--build-command', - action='store', default=None, - help=( - 'in addition to identifying for textual conflicts, run the build ' - '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.' - ), - ) + add_test_command_argument(subparser) subparser.add_argument( '--manual', action='store_true', default=False, @@ -2839,18 +2827,7 @@ def main(args): action='store', default=None, help='the name of the branch to which the result will be stored', ) - subparser.add_argument( - '--build-command', - action='store', default=None, - help=( - 'in addition to identifying for textual conflicts, run the build ' - '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.' - ), - ) + add_test_command_argument(subparser) subparser.add_argument( '--manual', action='store_true', default=False, @@ -2991,18 +2968,7 @@ def main(args): action='store', default=None, help='the name of the branch to which the result will be stored', ) - subparser.add_argument( - '--build-command', - action='store', default=None, - help=( - 'in addition to identifying for textual conflicts, run the build ' - '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.' - ), - ) + add_test_command_argument(subparser) subparser.add_argument( '--manual', action='store_true', default=False, @@ -3149,11 +3115,11 @@ def main(args): tip2, commits2, goal=options.goal, manual=options.manual, branch=(options.branch or options.name), - build_command=options.build_command, + test_command=options.test_command, ) merge_state.save() MergeState.set_default_name(options.name) - MergeState.set_default_test_command(options.build_command) + MergeState.set_default_test_command(options.test_command) elif options.subcommand == 'start': require_clean_work_tree('proceed') @@ -3179,11 +3145,11 @@ def main(args): tip2, commits2, goal=options.goal, manual=options.manual, branch=(options.branch or options.name), - build_command=options.build_command, + test_command=options.test_command, ) merge_state.save() MergeState.set_default_name(options.name) - MergeState.set_default_test_command(options.build_command) + MergeState.set_default_test_command(options.test_command) try: merge_state.auto_complete_frontier() @@ -3239,11 +3205,11 @@ def main(args): tip2, commits2, goal=options.goal, manual=options.manual, branch=options.branch, - build_command=options.build_command, + test_command=options.test_command, ) merge_state.save() MergeState.set_default_name(name) - MergeState.set_default_test_command(options.build_command) + MergeState.set_default_test_command(options.test_command) try: merge_state.auto_complete_frontier() @@ -3303,11 +3269,11 @@ def main(args): tip2, commits2, goal=options.goal, manual=options.manual, branch=options.branch, - build_command=options.build_command, + test_command=options.test_command, ) merge_state.save() MergeState.set_default_name(options.name) - MergeState.set_default_test_command(options.build_command) + MergeState.set_default_test_command(options.test_command) try: merge_state.auto_complete_frontier() From 3ef63ae3329e8d238c29fb7394c507e54a008f62 Mon Sep 17 00:00:00 2001 From: David Leen Date: Tue, 17 Jun 2014 21:42:55 -0700 Subject: [PATCH 3/4] Add a class for interacting with git config This commit adds the GitConfigStore class. This allows us to easily get and set keys and values in git config. Tests pass. --- git-imerge | 80 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/git-imerge b/git-imerge index 8cded95..e56790b 100755 --- a/git-imerge +++ b/git-imerge @@ -561,6 +561,67 @@ 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): """Attempt an automatic merge of commit1 and commit2. @@ -1861,26 +1922,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-all', '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 From d278a38779f77b42c90ed11ee2b5df8a451bd3eb Mon Sep 17 00:00:00 2001 From: David Leen Date: Tue, 17 Jun 2014 23:30:16 -0700 Subject: [PATCH 4/4] Store the test command in git config This commit uses GitConfigStore to read and write the test command using git config. The test command is stored in a different place for each imerge. The key is of the form: imerge.name.testcommand. The test command is removed when the imerge is finished. Also fixed the test-build script to use the make_script in the current dir. --- git-imerge | 68 ++++++++++++++++++++-------------------------------- t/test-build | 2 +- 2 files changed, 27 insertions(+), 43 deletions(-) diff --git a/git-imerge b/git-imerge index e56790b..9caa9e9 100755 --- a/git-imerge +++ b/git-imerge @@ -622,7 +622,7 @@ class GitConfigStore(object): raise GitConfigError(e.returncode, e.output) -def automerge(commit1, commit2, msg=None): +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 @@ -642,10 +642,9 @@ def automerge(commit1, commit2, msg=None): call_silently(['git', 'reset', '--merge']) raise AutomaticMergeFailed(commit1, commit2) - test_script = MergeState.get_default_test_command() - if test_script is not None: + if test_command is not None: try: - check_call(['/bin/sh', '-c', test_script]) + check_call(['/bin/sh', '-c', test_command]) except CalledProcessError as e: raise AutomaticTestFailed(commit1, commit2) @@ -1450,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.""" @@ -1551,7 +1551,9 @@ class Block(object): ) try: print("Automerging from is_mergeable") - automerge(self[i1, 0].sha1, self[0, i2].sha1) + 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: @@ -1578,7 +1580,9 @@ class Block(object): logmsg = 'imerge \'%s\': automatic merge %d-%d' % (self.name, i1orig, i2orig) try: print("Automerging from auto_outline") - merge = automerge(commit1, commit2, msg=logmsg) + 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') @@ -1657,6 +1661,7 @@ class Block(object): 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: @@ -1937,38 +1942,6 @@ class MergeState(Block): except GitConfigError: return None - @staticmethod - def set_default_test_command(name): - """Set the default test command to the specified one. - - name can be None to cause the default to be cleared.""" - - if name is None: - try: - check_call(['git', 'config', '--unset-all', 'imerge.testcommand']) - except CalledProcessError as e: - if e.returncode == 5: - # Value was not set - pass - else: - raise - else: - check_call(['git', 'config', 'imerge.testcommand', name]) - - @staticmethod - def get_default_test_command(): - """Get the name of the test command, or None if none is currently set.""" - - if MergeState.DEFAULT_TEST_COMMAND is None: - try: - MergeState.DEFAULT_TEST_COMMAND = \ - check_output(['git', 'config', 'imerge.testcommand']).rstrip() - return MergeState.DEFAULT_TEST_COMMAND - except CalledProcessError: - return None - - return MergeState.DEFAULT_TEST_COMMAND - @staticmethod def _check_no_merges(commits): multiparent_commits = [ @@ -3173,7 +3146,10 @@ def main(args): ) merge_state.save() MergeState.set_default_name(options.name) - MergeState.set_default_test_command(options.test_command) + 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') @@ -3203,7 +3179,9 @@ def main(args): ) merge_state.save() MergeState.set_default_name(options.name) - MergeState.set_default_test_command(options.test_command) + 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() @@ -3263,7 +3241,9 @@ def main(args): ) merge_state.save() MergeState.set_default_name(name) - MergeState.set_default_test_command(options.test_command) + 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() @@ -3327,7 +3307,9 @@ def main(args): ) merge_state.save() MergeState.set_default_name(options.name) - MergeState.set_default_test_command(options.test_command) + 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() @@ -3402,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 diff --git a/t/test-build b/t/test-build index a269b09..365acf5 100755 --- a/t/test-build +++ b/t/test-build @@ -35,7 +35,7 @@ done git checkout build-left "$GIT_IMERGE" start --goal=rebase-with-history --first-parent \ - --build-command="make_script" \ + --test-command="./make_script" \ --name=build-left-right --branch=build-left-right-full build-right # Resolve conflict