Revision control

++ #!/usr/bin/python
++ # -*- coding: utf-8 -*-
++
++ # Copyright (C) 2015 The Qt Company Ltd.
++ #
++ # You may use this file under the terms of the 3-clause BSD license.
++ # See the file LICENSE from this package for details.
++ #
++
++ from __future__ import print_function
++
++ import os
++ import sys
++ import logging
++ import subprocess
++ import re
++ import warnings
++
++ fnull = open(os.devnull, "w")
++
++ class colors:
++ HEADER = '\033[95m'
++ BLUE = '\033[94m'
++ GREEN = '\033[92m'
++ WARNING = '\033[93m'
++ FAIL = '\033[91m'
++ ENDC = '\033[0m'
++
++ class BlameParser:
++ __begin = ('<' * 7, '=' * 7)
++ __end_map = {'merge': '=', 'diff3': '|'}
++ # One example line to parse (result from git blame -c --date=short):
++ # "ddd8ab31 (Andras Becsi 2014-06-13 18:17:07 +0200 414)bool GLSurface::InitializeOneOffInternal()"
++ __matcher = re.compile(r"([0-9a-f]+)\s+\((.*?)\s+\d+\)(.*)").match
++
++ def __init__(self):
++ self.__end = (self.__from_git_config('merge.conflictstyle', 'merge', self.__end_map) * 7, '>' * 7)
++
++ def __from_git_config(self, parameter, default, mapping):
++ try:
++ value = subprocess.check_output(['git', 'config', parameter]).decode('utf-8').strip()
++ except subprocess.CalledProcessError:
++ # When the value is unset
++ value = ''
++ try:
++ return mapping[value or default]
++ except KeyError:
++ raise ValueError('Unknown git config', parameter, value)
++
++ def __parse_blame_line(self, codeline):
++ # Match a "git blame -c --date=short" output line
++ m = self.__matcher(codeline)
++ if m:
++ return m.group(1, 2, 3)
++ raise ValueError("Failed to parse blame line: %s" % (codeline))
++
++ def __get_blame_results(self, conflict):
++ cmd_git_blame = ["git", "blame", "-c", "--date=short", conflict]
++ blame_output = subprocess.check_output(cmd_git_blame).decode('utf-8')
++ return blame_output.split('\n')
++
++ def parse(self, conflict):
++ results, scan = {}, False
++ for line in self.__get_blame_results(conflict):
++ if not line.strip():
++ continue
++ sha1, info, code = self.__parse_blame_line(line)
++ if any(code.startswith(b) for b in self.__begin):
++ scan = True
++ continue
++ if any(code.startswith(e) for e in self.__end):
++ scan = False
++ if scan:
++ results[sha1] = info
++
++ return results.items()
++
++ # Try to find related commits for the conflicts
++ def check_conflicts(module, config, from_ref, conflicts):
++ results = ''
++ blameparser = BlameParser()
++ for conflict in conflicts:
++ changes = blameparser.parse(conflict)
++ results += conflict + ':\n\t' + '\n\t'.join('%s %s' %c for c in changes) + '\n'
++ return results
++
++ # Replace all occurrences of searchExp in one file
++ def replaceInFile(file, searchExp, replaceExp):
++ import fileinput
++ for line in fileinput.input(file, inplace=1):
++ line = re.sub(searchExp, replaceExp, line)
++ sys.stdout.write(line)
++
++ # Reset module to destination branch
++ def reset_module(module, config):
++ opts = {}
++ opts['to'] = config.branch_to
++ try:
++ git_checkout = "git checkout %(to)s" %opts
++ subprocess.check_call(git_checkout.split(), stdout=fnull)
++ git_reset = "git reset --hard origin/%(to)s" %opts
++ subprocess.check_call(git_reset.split(), stdout=fnull, stderr=fnull)
++ except Exception as e:
++ logging.error("Git reset failed: %s", str(e))
++ return False
++ return True
++
++ # Run git merge for one module
++ def do_merge(module, config, from_ref):
++ try:
++ cmd_git_merge = ["git", "merge", "-m", \
++ "Merge remote-tracking branch \'origin/" + config.branch_from + "\' into " + config.branch_to, \
++ from_ref, "--no-edit", "--no-ff"]
++ ret = subprocess.call(cmd_git_merge, stdout=fnull)
++ if ret != 0 and config.mergetool:
++ print("Starting mergetool")
++ ret = subprocess.call(['git', 'mergetool', '-y'])
++ if ret == 0:
++ ret = subprocess.call(['git', 'commit', '--no-edit'], stdout=fnull)
++ if ret != 0:
++ print("Module %s failed to merge, manual merge needed." % module)
++ return False
++
++ # Make sure we get a change-id as git merge doesn't generate it for us
++ git_amend = "git commit --amend --no-edit"
++ subprocess.check_call(git_amend.split(), stdout=fnull)
++
++ except Exception as e:
++ logging.error("Merge failed: %s", str(e))
++ return False
++
++ return True
++
++ def wip_merge(module, config, from_ref):
++ conflicts = []
++ try:
++ cmd_git_status = ["git", "status", "--porcelain"]
++ status_output = subprocess.check_output(cmd_git_status).decode('utf-8').strip()
++ status_count = 0
++ if status_output:
++ lines = status_output.split('\n')
++ status_count = len(lines)
++
++ for line in lines:
++ try: mode, file = line.split()
++ except (ValueError, TypeError): pass # not exactly two tokens
++ else:
++ if 'U' in mode:
++ print("conflict: ", file)
++ conflicts.append(file)
++
++ if not conflicts:
++ print("Didn't find any conflict. Need to check carefully.")
++ return False
++
++ cmd_git_commit = ["git", "commit", "-a", "-m",
++ "WIP: Merge remote-tracking branch \'origin/" + config.branch_from + "\' into " + config.branch_to + "\n"
++ + '\nConflicts:\n\t' + '\n\t'.join(conflicts) + '\n']
++ ret = subprocess.call(cmd_git_commit, stdout=fnull)
++ if ret != 0:
++ print("Failed to do \"git commit -a\".")
++ return False
++
++ # Make sure we get a change-id as git merge doesn't generate it for us
++ git_amend = "git commit --amend --no-edit"
++ subprocess.check_call(git_amend.split(), stdout=fnull)
++
++ related = check_conflicts(module, config, from_ref, conflicts)
++ print("Related: ", related)
++
++ except Exception as e:
++ logging.error("Create WIP change failed: %s", str(e))
++ return False
++
++ return True
++
++ # Change the version number in the .qmake.conf file to a given string
++ def update_qmake_conf(module, version):
++ fileName = ".qmake.conf"
++ replaceInFile(fileName, "5\..\..", version)
++ cmd_git_status = "git status".split()
++
++ status = subprocess.check_output(cmd_git_status).decode('utf-8')
++ print(status)
++ if status.find(fileName) < 0:
++ return False
++
++ cmd_git_add = "git add .qmake.conf".split()
++ subprocess.check_call(cmd_git_add, stdout=fnull)
++ cmd_git_commit = ['git', 'commit', '-m', 'Update module version to %s' % version]
++ subprocess.check_call(cmd_git_commit, stdout=fnull)
++ return True
++
++ # Run git push to the destination branch for one module
++ def push_gerrit(module, config):
++ subprocess.call(["git", "show"])
++ print('\n"%s" was successfully updated and merged' % module)
++ # fix for python 2.x compatibility
++ try: input = raw_input
++ except NameError: pass
++ confirm = input('Push merge? [Y/n] ')
++ if not confirm or confirm.startswith(('y', 'Y')):
++ output = subprocess.check_output(('git push gerrit HEAD:refs/for/%s' % config.branch_to).split()).decode('utf-8')
++ #url = re.search("https.*", output).group(0).strip()
++ #print(url)
++
++ #GERRIT_CHANGE_ID=$(echo "$GIT_PUSH" | sed 's/.*https:\/\/codereview.qt-project.org\/\(.*\)/\;tx;d;:x')
++ #echo "ChangeId: $GERRIT_CHANGE_ID"
++ # if we had gerrit 2.4, we could do this:
++ #for reviewer in
++ # sh codereview.qt-project.org gerrit set-reviewers --add jedrzej.nowacki@digia.com 49938
++
++ else:
++ print(colors.WARNING + "Merge not pushed." + colors.ENDC)
++
++ # Perform merge and push it for one module
++ def merge(module, config, from_ref):
++ push_required = False
++ if config.merge or config.wip:
++ if not do_merge(module, config, from_ref):
++ return False
++ push_required = True
++
++ if config.version:
++ print('Updating .qmake.conf for', module)
++ if update_qmake_conf(module, config.version):
++ push_required = True
++
++ if push_required:
++ push_gerrit(module, config)
++ return True
++
++ # Create WIP change when having conflicts
++ def create_wip(module, config, from_ref):
++ if config.wip:
++ if not wip_merge(module, config, from_ref):
++ return False
++
++ push_gerrit(module, config)
++
++ return True
++
++ def get_module_sha_from_super(module, branch):
++ result = ""
++ cmd_git_lstree = ["git", "ls-tree", "origin/" + branch, module]
++ lstree_output = subprocess.check_output(cmd_git_lstree).decode('utf-8').strip()
++ lstree_outputs = lstree_output.split()
++ if len(lstree_outputs) == 4 and lstree_outputs[3] == module:
++ result = lstree_outputs[2].strip()
++ return result
++
++ # Iterate over all modules (either default or passed with -m option)
++ def process_modules(config):
++ if config.list_modules:
++ print("Modules: ", config.modules)
++ return
++
++ cmd_git_fetch = ["git", "fetch"]
++ subprocess.check_call(cmd_git_fetch, stdout=fnull, stderr=fnull)
++
++ manual_merges = []
++
++ for module in config.modules.split():
++ print("\nModule: " + colors.GREEN + module + colors.ENDC)
++ if not os.path.isdir(module):
++ print("Directory '%s' does not exist. Skipping..." % module)
++ continue
++ try:
++ os.chdir(module)
++ try:
++ cmd_git_fetch = ["git", "fetch"]
++ subprocess.check_call(cmd_git_fetch, stdout=fnull, stderr=fnull)
++
++ to_string = "origin/" + config.branch_to
++ from_string = "origin/" + config.branch_from
++
++ if config.super:
++ os.chdir("..")
++ from_sha1 = get_module_sha_from_super(module, config.branch_from)
++ if from_sha1:
++ from_string = from_sha1
++ else:
++ print("Unable to obtain sha1 from super module for module ", module, "\n")
++ os.chdir(module)
++
++ print("Merge from", from_string, "to", to_string, ":")
++ cmd_git_cherry = ["git", "cherry", to_string, from_string, "-v"]
++ cherry_output = subprocess.check_output(cmd_git_cherry).decode('utf-8').strip()
++
++ change_count = 0
++ if cherry_output:
++ change_count = len(cherry_output.split('\n'))
++
++ if config.status:
++ print(cherry_output)
++ print(colors.GREEN, change_count, colors.ENDC, "patches to be merged in", module, "\n")
++
++ if config.reset or config.merge or config.version or config.wip:
++ reset_module(module, config)
++
++ if change_count > 0:
++ if not merge(module, config, from_string):
++ manual_merges.append(module)
++ if config.wip:
++ create_wip(module, config, from_string)
++
++ except Exception as e:
++ logging.error("Command execution failed: %s", str(e))
++ import traceback
++ traceback.print_exc(file=sys.stdout)
++ finally:
++ os.chdir("..")
++
++ except Exception as e:
++ logging.error("Changing current dir failed: %s", str(e))
++ import traceback
++ traceback.print_exc(file=sys.stdout)
++
++ if manual_merges:
++ print("Modules failed to merge: ", manual_merges)
++
++ # get a list of submodules and a list of submodules that are not checked out
++ def get_submodules():
++ git_submodule_status = subprocess.check_output('git submodule status'.split(' ')).strip()
++ modules = []
++ modules_not_checked_out = []
++ for line in git_submodule_status.split('\n'):
++ module = line.strip().split(' ')[1]
++ if line.startswith('-'):
++ modules_not_checked_out.append(module)
++ print('WARNING:', module, 'is not checked out')
++ elif module in ('qtqa', 'qtrepotools'):
++ print('skipping', module)
++ else:
++ modules.append(module)
++ return modules, modules_not_checked_out
++
++
++ if __name__== "__main__":
++ import argparse
++ parser = argparse.ArgumentParser(prog="git-qt-merge-branches",
++ description="Merge branches for the Qt Project",
++ epilog="""
++
++ Run from within a work tree of the qt5 super-module, with all relevant
++ sub-modules checked out (e.g. using qt5/init-repository).
++ """)
++ group = parser.add_argument_group("Primary action", "Chose one of:")
++ group.add_argument('-s', '--status', action="store_true",
++ help='show the status (which patches will be merged)')
++ group.add_argument('-d', '--merge', action="store_true",
++ help='do the merge')
++ parser.add_argument('-u', '--super', action="store_true",
++ help='obtain sha1s from supermodule instead of using branch tips')
++ parser.add_argument('-m', '--modules',
++ help='override the list of modules (eg. -m "qtbase qtdeclarative")')
++ group.add_argument('-l', '--list-modules', action="store_true",
++ help='list the modules to be merged and exit')
++ group.add_argument('--reset', action="store_true",
++ help='reset to origin/to_branch. this is implicit in the merge command')
++ parser.add_argument('-f', '--branch-from', default='stable',
++ help='from which branch to merge')
++ parser.add_argument('-t', '--branch-to', default='dev',
++ help='the target branch')
++ group.add_argument('-v', '--version', default='',
++ help='set version in .qmake.conf to given version string')
++ parser.add_argument('--mergetool', action="store_true",
++ help='run mergetool for conflicts')
++ parser.add_argument('-w', '--wip', action="store_true",
++ help='create WIP change when conflicts arise')
++ config = parser.parse_args()
++
++ print("Qt Project merge tool\n")
++ default_modules, modules_not_checked_out = get_submodules()
++ if config.modules is None:
++ config.modules = ' '.join(default_modules)
++ print("Submodules: ", default_modules)
++ print("Ignored submodules: ", modules_not_checked_out)
++
++ logging.basicConfig(format='%(levelname)s: %(message)s')
++
++ if config.status or config.merge or config.reset or config.list_modules or config.version or config.wip:
++ process_modules(config)
++ else:
++ parser.print_help()
++ print("Submodules that are not checked out and are ignored:\n",
++ ' '.join(modules_not_checked_out))