summaryrefslogtreecommitdiffstats
path: root/bin/git-qt-merge-mainlines
blob: 251656d6a42a10cff6b007609050bda1af0c0458 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (C) 2015 The Qt Company Ltd.
# Contact: http://www.qt.io/licensing/
#
# 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

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'


# 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

# 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 len(confirm) == 0 or confirm[0].lower() == '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:
        if not do_merge(module, config, from_ref):
            return False
        push_required = True

    if len(config.version) > 0:
        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

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 len(cherry_output) > 0:
                    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 len(config.version):
                    reset_module(module, config)

                if change_count > 0:
                    if not merge(module, config, from_string):
                        manual_merges.append(module)

            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 len(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[0] == '-':
            modules_not_checked_out += [module]
            print('WARNING:', module, 'is not checked out')
        else:
            if module == 'qtqa' or module == 'qtrepotools':
                print('skipping', module)
            else:
                modules += [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).
""")
    parser.add_argument('-s', '--status', action="store_true", help='show the status (which patches will be merged)')
    parser.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")')
    parser.add_argument('-l', '--list-modules', action="store_true", help='list the modules to be merged and exit')
    parser.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')
    parser.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')
    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 not config.status and not config.merge and not config.reset and not config.list_modules and not len(config.version):
        parser.print_help()
    else:
        process_modules(config)
    print("Submodules that are not checked out and are ignored:\n", ' '.join(modules_not_checked_out))