xref: /aosp_15_r20/development/tools/repo_pull/repo_pull.py (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1*90c8c64dSAndroid Build Coastguard Worker#!/usr/bin/env python3
2*90c8c64dSAndroid Build Coastguard Worker
3*90c8c64dSAndroid Build Coastguard Worker#
4*90c8c64dSAndroid Build Coastguard Worker# Copyright (C) 2018 The Android Open Source Project
5*90c8c64dSAndroid Build Coastguard Worker#
6*90c8c64dSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
7*90c8c64dSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
8*90c8c64dSAndroid Build Coastguard Worker# You may obtain a copy of the License at
9*90c8c64dSAndroid Build Coastguard Worker#
10*90c8c64dSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
11*90c8c64dSAndroid Build Coastguard Worker#
12*90c8c64dSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
13*90c8c64dSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
14*90c8c64dSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15*90c8c64dSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
16*90c8c64dSAndroid Build Coastguard Worker# limitations under the License.
17*90c8c64dSAndroid Build Coastguard Worker#
18*90c8c64dSAndroid Build Coastguard Worker
19*90c8c64dSAndroid Build Coastguard Worker"""A command line utility to pull multiple change lists from Gerrit."""
20*90c8c64dSAndroid Build Coastguard Worker
21*90c8c64dSAndroid Build Coastguard Workerfrom __future__ import print_function
22*90c8c64dSAndroid Build Coastguard Worker
23*90c8c64dSAndroid Build Coastguard Workerimport argparse
24*90c8c64dSAndroid Build Coastguard Workerimport collections
25*90c8c64dSAndroid Build Coastguard Workerimport itertools
26*90c8c64dSAndroid Build Coastguard Workerimport json
27*90c8c64dSAndroid Build Coastguard Workerimport multiprocessing
28*90c8c64dSAndroid Build Coastguard Workerimport os
29*90c8c64dSAndroid Build Coastguard Workerimport os.path
30*90c8c64dSAndroid Build Coastguard Workerimport re
31*90c8c64dSAndroid Build Coastguard Workerimport sys
32*90c8c64dSAndroid Build Coastguard Workerimport xml.dom.minidom
33*90c8c64dSAndroid Build Coastguard Worker
34*90c8c64dSAndroid Build Coastguard Workerfrom gerrit import (
35*90c8c64dSAndroid Build Coastguard Worker    add_common_parse_args, create_url_opener_from_args, find_gerrit_name,
36*90c8c64dSAndroid Build Coastguard Worker    normalize_gerrit_name, query_change_lists, run
37*90c8c64dSAndroid Build Coastguard Worker)
38*90c8c64dSAndroid Build Coastguard Workerfrom subprocess import PIPE
39*90c8c64dSAndroid Build Coastguard Worker
40*90c8c64dSAndroid Build Coastguard Workertry:
41*90c8c64dSAndroid Build Coastguard Worker    # pylint: disable=redefined-builtin
42*90c8c64dSAndroid Build Coastguard Worker    from __builtin__ import raw_input as input  # PY2
43*90c8c64dSAndroid Build Coastguard Workerexcept ImportError:
44*90c8c64dSAndroid Build Coastguard Worker    pass
45*90c8c64dSAndroid Build Coastguard Worker
46*90c8c64dSAndroid Build Coastguard Workertry:
47*90c8c64dSAndroid Build Coastguard Worker    from shlex import quote as _sh_quote  # PY3.3
48*90c8c64dSAndroid Build Coastguard Workerexcept ImportError:
49*90c8c64dSAndroid Build Coastguard Worker    # Shell language simple string pattern.  If a string matches this pattern,
50*90c8c64dSAndroid Build Coastguard Worker    # it doesn't have to be quoted.
51*90c8c64dSAndroid Build Coastguard Worker    _SHELL_SIMPLE_PATTERN = re.compile('^[a-zA-Z90-9_./-]+$')
52*90c8c64dSAndroid Build Coastguard Worker
53*90c8c64dSAndroid Build Coastguard Worker    def _sh_quote(txt):
54*90c8c64dSAndroid Build Coastguard Worker        """Quote a string if it contains special characters."""
55*90c8c64dSAndroid Build Coastguard Worker        return txt if _SHELL_SIMPLE_PATTERN.match(txt) else json.dumps(txt)
56*90c8c64dSAndroid Build Coastguard Worker
57*90c8c64dSAndroid Build Coastguard Worker
58*90c8c64dSAndroid Build Coastguard Workerif bytes is str:
59*90c8c64dSAndroid Build Coastguard Worker    def write_bytes(data, file):  # PY2
60*90c8c64dSAndroid Build Coastguard Worker        """Write bytes to a file."""
61*90c8c64dSAndroid Build Coastguard Worker        # pylint: disable=redefined-builtin
62*90c8c64dSAndroid Build Coastguard Worker        file.write(data)
63*90c8c64dSAndroid Build Coastguard Workerelse:
64*90c8c64dSAndroid Build Coastguard Worker    def write_bytes(data, file):  # PY3
65*90c8c64dSAndroid Build Coastguard Worker        """Write bytes to a file."""
66*90c8c64dSAndroid Build Coastguard Worker        # pylint: disable=redefined-builtin
67*90c8c64dSAndroid Build Coastguard Worker        file.buffer.write(data)
68*90c8c64dSAndroid Build Coastguard Worker
69*90c8c64dSAndroid Build Coastguard Worker
70*90c8c64dSAndroid Build Coastguard Workerdef _confirm(question, default, file=sys.stderr):
71*90c8c64dSAndroid Build Coastguard Worker    """Prompt a yes/no question and convert the answer to a boolean value."""
72*90c8c64dSAndroid Build Coastguard Worker    # pylint: disable=redefined-builtin
73*90c8c64dSAndroid Build Coastguard Worker    answers = {'': default, 'y': True, 'yes': True, 'n': False, 'no': False}
74*90c8c64dSAndroid Build Coastguard Worker    suffix = '[Y/n] ' if default else ' [y/N] '
75*90c8c64dSAndroid Build Coastguard Worker    while True:
76*90c8c64dSAndroid Build Coastguard Worker        file.write(question + suffix)
77*90c8c64dSAndroid Build Coastguard Worker        file.flush()
78*90c8c64dSAndroid Build Coastguard Worker        ans = answers.get(input().lower())
79*90c8c64dSAndroid Build Coastguard Worker        if ans is not None:
80*90c8c64dSAndroid Build Coastguard Worker            return ans
81*90c8c64dSAndroid Build Coastguard Worker
82*90c8c64dSAndroid Build Coastguard Worker
83*90c8c64dSAndroid Build Coastguard Workerclass ChangeList(object):
84*90c8c64dSAndroid Build Coastguard Worker    """A ChangeList to be checked out."""
85*90c8c64dSAndroid Build Coastguard Worker    # pylint: disable=too-few-public-methods,too-many-instance-attributes
86*90c8c64dSAndroid Build Coastguard Worker
87*90c8c64dSAndroid Build Coastguard Worker    def __init__(self, project, fetch, commit_sha1, commit, change_list):
88*90c8c64dSAndroid Build Coastguard Worker        """Initialize a ChangeList instance."""
89*90c8c64dSAndroid Build Coastguard Worker        # pylint: disable=too-many-arguments
90*90c8c64dSAndroid Build Coastguard Worker
91*90c8c64dSAndroid Build Coastguard Worker        self.project = project
92*90c8c64dSAndroid Build Coastguard Worker        self.number = change_list['_number']
93*90c8c64dSAndroid Build Coastguard Worker        self.branch = change_list['branch']
94*90c8c64dSAndroid Build Coastguard Worker
95*90c8c64dSAndroid Build Coastguard Worker        self.fetch = fetch
96*90c8c64dSAndroid Build Coastguard Worker
97*90c8c64dSAndroid Build Coastguard Worker        fetch_git = None
98*90c8c64dSAndroid Build Coastguard Worker        for protocol in ('http', 'sso', 'rpc', 'ssh'):
99*90c8c64dSAndroid Build Coastguard Worker            fetch_git = fetch.get(protocol)
100*90c8c64dSAndroid Build Coastguard Worker            if fetch_git:
101*90c8c64dSAndroid Build Coastguard Worker                break
102*90c8c64dSAndroid Build Coastguard Worker
103*90c8c64dSAndroid Build Coastguard Worker        if not fetch_git:
104*90c8c64dSAndroid Build Coastguard Worker            raise ValueError(
105*90c8c64dSAndroid Build Coastguard Worker                'unknown fetch protocols: ' + str(list(fetch.keys())))
106*90c8c64dSAndroid Build Coastguard Worker
107*90c8c64dSAndroid Build Coastguard Worker        self.fetch_url = fetch_git['url']
108*90c8c64dSAndroid Build Coastguard Worker        self.fetch_ref = fetch_git['ref']
109*90c8c64dSAndroid Build Coastguard Worker
110*90c8c64dSAndroid Build Coastguard Worker        self.commit_sha1 = commit_sha1
111*90c8c64dSAndroid Build Coastguard Worker        self.commit = commit
112*90c8c64dSAndroid Build Coastguard Worker        self.parents = commit['parents']
113*90c8c64dSAndroid Build Coastguard Worker
114*90c8c64dSAndroid Build Coastguard Worker        self.change_list = change_list
115*90c8c64dSAndroid Build Coastguard Worker
116*90c8c64dSAndroid Build Coastguard Worker
117*90c8c64dSAndroid Build Coastguard Worker    def is_merge(self):
118*90c8c64dSAndroid Build Coastguard Worker        """Check whether this change list a merge commit."""
119*90c8c64dSAndroid Build Coastguard Worker        return len(self.parents) > 1
120*90c8c64dSAndroid Build Coastguard Worker
121*90c8c64dSAndroid Build Coastguard Worker
122*90c8c64dSAndroid Build Coastguard Workerdef find_repo_top(curdir):
123*90c8c64dSAndroid Build Coastguard Worker    """Find the top directory for this git-repo source tree."""
124*90c8c64dSAndroid Build Coastguard Worker    olddir = None
125*90c8c64dSAndroid Build Coastguard Worker    while curdir != olddir:
126*90c8c64dSAndroid Build Coastguard Worker        if os.path.exists(os.path.join(curdir, '.repo')):
127*90c8c64dSAndroid Build Coastguard Worker            return curdir
128*90c8c64dSAndroid Build Coastguard Worker        olddir = curdir
129*90c8c64dSAndroid Build Coastguard Worker        curdir = os.path.dirname(curdir)
130*90c8c64dSAndroid Build Coastguard Worker    raise ValueError('.repo dir not found')
131*90c8c64dSAndroid Build Coastguard Worker
132*90c8c64dSAndroid Build Coastguard Worker
133*90c8c64dSAndroid Build Coastguard Workerclass ProjectNameDirDict:
134*90c8c64dSAndroid Build Coastguard Worker    """A dict which maps project name and revision to the source path."""
135*90c8c64dSAndroid Build Coastguard Worker    def __init__(self):
136*90c8c64dSAndroid Build Coastguard Worker        self._dirs = dict()
137*90c8c64dSAndroid Build Coastguard Worker
138*90c8c64dSAndroid Build Coastguard Worker
139*90c8c64dSAndroid Build Coastguard Worker    def add_directory(self, name, revision, path):
140*90c8c64dSAndroid Build Coastguard Worker        """Maps project name and revision to path."""
141*90c8c64dSAndroid Build Coastguard Worker        self._dirs[name] = path or name
142*90c8c64dSAndroid Build Coastguard Worker        if revision:
143*90c8c64dSAndroid Build Coastguard Worker            self._dirs[(name, revision)] = path or name
144*90c8c64dSAndroid Build Coastguard Worker
145*90c8c64dSAndroid Build Coastguard Worker
146*90c8c64dSAndroid Build Coastguard Worker    def find_directory(self, name, revision, default_result=None):
147*90c8c64dSAndroid Build Coastguard Worker        """Finds corresponding path of project name and revision."""
148*90c8c64dSAndroid Build Coastguard Worker        if (name, revision) in self._dirs:
149*90c8c64dSAndroid Build Coastguard Worker            return self._dirs[(name, revision)]
150*90c8c64dSAndroid Build Coastguard Worker        if default_result is None:
151*90c8c64dSAndroid Build Coastguard Worker            return self._dirs[name]
152*90c8c64dSAndroid Build Coastguard Worker        return self._dirs.get(name, default_result)
153*90c8c64dSAndroid Build Coastguard Worker
154*90c8c64dSAndroid Build Coastguard Worker
155*90c8c64dSAndroid Build Coastguard Workerdef build_project_name_dir_dict(manifest_name):
156*90c8c64dSAndroid Build Coastguard Worker    """Build the mapping from Gerrit project name to source tree project
157*90c8c64dSAndroid Build Coastguard Worker    directory path."""
158*90c8c64dSAndroid Build Coastguard Worker    manifest_cmd = ['repo', 'manifest']
159*90c8c64dSAndroid Build Coastguard Worker    if manifest_name:
160*90c8c64dSAndroid Build Coastguard Worker        manifest_cmd.extend(['-m', manifest_name])
161*90c8c64dSAndroid Build Coastguard Worker    raw_manifest_xml = run(manifest_cmd, stdout=PIPE, check=True).stdout
162*90c8c64dSAndroid Build Coastguard Worker
163*90c8c64dSAndroid Build Coastguard Worker    manifest_xml = xml.dom.minidom.parseString(raw_manifest_xml)
164*90c8c64dSAndroid Build Coastguard Worker    project_dirs = ProjectNameDirDict()
165*90c8c64dSAndroid Build Coastguard Worker    for project in manifest_xml.getElementsByTagName('project'):
166*90c8c64dSAndroid Build Coastguard Worker        name = project.getAttribute('name')
167*90c8c64dSAndroid Build Coastguard Worker        path = project.getAttribute('path')
168*90c8c64dSAndroid Build Coastguard Worker        revision = project.getAttribute('revision')
169*90c8c64dSAndroid Build Coastguard Worker        project_dirs.add_directory(name, revision, path)
170*90c8c64dSAndroid Build Coastguard Worker
171*90c8c64dSAndroid Build Coastguard Worker    return project_dirs
172*90c8c64dSAndroid Build Coastguard Worker
173*90c8c64dSAndroid Build Coastguard Worker
174*90c8c64dSAndroid Build Coastguard Workerdef group_and_sort_change_lists(change_lists):
175*90c8c64dSAndroid Build Coastguard Worker    """Build a dict that maps projects to a list of topologically sorted change
176*90c8c64dSAndroid Build Coastguard Worker    lists."""
177*90c8c64dSAndroid Build Coastguard Worker
178*90c8c64dSAndroid Build Coastguard Worker    # Build a dict that map projects to dicts that map commits to changes.
179*90c8c64dSAndroid Build Coastguard Worker    projects = collections.defaultdict(dict)
180*90c8c64dSAndroid Build Coastguard Worker    for change_list in change_lists:
181*90c8c64dSAndroid Build Coastguard Worker        commit_sha1 = None
182*90c8c64dSAndroid Build Coastguard Worker        for commit_sha1, value in change_list['revisions'].items():
183*90c8c64dSAndroid Build Coastguard Worker            fetch = value['fetch']
184*90c8c64dSAndroid Build Coastguard Worker            commit = value['commit']
185*90c8c64dSAndroid Build Coastguard Worker
186*90c8c64dSAndroid Build Coastguard Worker        if not commit_sha1:
187*90c8c64dSAndroid Build Coastguard Worker            raise ValueError('bad revision')
188*90c8c64dSAndroid Build Coastguard Worker
189*90c8c64dSAndroid Build Coastguard Worker        project = change_list['project']
190*90c8c64dSAndroid Build Coastguard Worker
191*90c8c64dSAndroid Build Coastguard Worker        project_changes = projects[project]
192*90c8c64dSAndroid Build Coastguard Worker        if commit_sha1 in project_changes:
193*90c8c64dSAndroid Build Coastguard Worker            raise KeyError('repeated commit sha1 "{}" in project "{}"'.format(
194*90c8c64dSAndroid Build Coastguard Worker                commit_sha1, project))
195*90c8c64dSAndroid Build Coastguard Worker
196*90c8c64dSAndroid Build Coastguard Worker        project_changes[commit_sha1] = ChangeList(
197*90c8c64dSAndroid Build Coastguard Worker            project, fetch, commit_sha1, commit, change_list)
198*90c8c64dSAndroid Build Coastguard Worker
199*90c8c64dSAndroid Build Coastguard Worker    # Sort all change lists in a project in post ordering.
200*90c8c64dSAndroid Build Coastguard Worker    def _sort_project_change_lists(changes):
201*90c8c64dSAndroid Build Coastguard Worker        visited_changes = set()
202*90c8c64dSAndroid Build Coastguard Worker        sorted_changes = []
203*90c8c64dSAndroid Build Coastguard Worker
204*90c8c64dSAndroid Build Coastguard Worker        def _post_order_traverse(change):
205*90c8c64dSAndroid Build Coastguard Worker            visited_changes.add(change)
206*90c8c64dSAndroid Build Coastguard Worker            for parent in change.parents:
207*90c8c64dSAndroid Build Coastguard Worker                parent_change = changes.get(parent['commit'])
208*90c8c64dSAndroid Build Coastguard Worker                if parent_change and parent_change not in visited_changes:
209*90c8c64dSAndroid Build Coastguard Worker                    _post_order_traverse(parent_change)
210*90c8c64dSAndroid Build Coastguard Worker            sorted_changes.append(change)
211*90c8c64dSAndroid Build Coastguard Worker
212*90c8c64dSAndroid Build Coastguard Worker        for change in sorted(changes.values(), key=lambda x: x.number):
213*90c8c64dSAndroid Build Coastguard Worker            if change not in visited_changes:
214*90c8c64dSAndroid Build Coastguard Worker                _post_order_traverse(change)
215*90c8c64dSAndroid Build Coastguard Worker
216*90c8c64dSAndroid Build Coastguard Worker        return sorted_changes
217*90c8c64dSAndroid Build Coastguard Worker
218*90c8c64dSAndroid Build Coastguard Worker    # Sort changes in each projects
219*90c8c64dSAndroid Build Coastguard Worker    sorted_changes = []
220*90c8c64dSAndroid Build Coastguard Worker    for project in sorted(projects.keys()):
221*90c8c64dSAndroid Build Coastguard Worker        sorted_changes.append(_sort_project_change_lists(projects[project]))
222*90c8c64dSAndroid Build Coastguard Worker
223*90c8c64dSAndroid Build Coastguard Worker    return sorted_changes
224*90c8c64dSAndroid Build Coastguard Worker
225*90c8c64dSAndroid Build Coastguard Worker
226*90c8c64dSAndroid Build Coastguard Workerdef _main_json(args):
227*90c8c64dSAndroid Build Coastguard Worker    """Print the change lists in JSON format."""
228*90c8c64dSAndroid Build Coastguard Worker    change_lists = _get_change_lists_from_args(args)
229*90c8c64dSAndroid Build Coastguard Worker    json.dump(change_lists, sys.stdout, indent=4, separators=(', ', ': '))
230*90c8c64dSAndroid Build Coastguard Worker    print()  # Print the end-of-line
231*90c8c64dSAndroid Build Coastguard Worker
232*90c8c64dSAndroid Build Coastguard Worker
233*90c8c64dSAndroid Build Coastguard Worker# Git commands for merge commits
234*90c8c64dSAndroid Build Coastguard Worker_MERGE_COMMANDS = {
235*90c8c64dSAndroid Build Coastguard Worker    'merge': ['git', 'merge', '--no-edit'],
236*90c8c64dSAndroid Build Coastguard Worker    'merge-ff-only': ['git', 'merge', '--no-edit', '--ff-only'],
237*90c8c64dSAndroid Build Coastguard Worker    'merge-no-ff': ['git', 'merge', '--no-edit', '--no-ff'],
238*90c8c64dSAndroid Build Coastguard Worker    'reset': ['git', 'reset', '--hard'],
239*90c8c64dSAndroid Build Coastguard Worker    'checkout': ['git', 'checkout'],
240*90c8c64dSAndroid Build Coastguard Worker}
241*90c8c64dSAndroid Build Coastguard Worker
242*90c8c64dSAndroid Build Coastguard Worker
243*90c8c64dSAndroid Build Coastguard Worker# Git commands for non-merge commits
244*90c8c64dSAndroid Build Coastguard Worker_PICK_COMMANDS = {
245*90c8c64dSAndroid Build Coastguard Worker    'pick': ['git', 'cherry-pick', '--allow-empty'],
246*90c8c64dSAndroid Build Coastguard Worker    'merge': ['git', 'merge', '--no-edit'],
247*90c8c64dSAndroid Build Coastguard Worker    'merge-ff-only': ['git', 'merge', '--no-edit', '--ff-only'],
248*90c8c64dSAndroid Build Coastguard Worker    'merge-no-ff': ['git', 'merge', '--no-edit', '--no-ff'],
249*90c8c64dSAndroid Build Coastguard Worker    'reset': ['git', 'reset', '--hard'],
250*90c8c64dSAndroid Build Coastguard Worker    'checkout': ['git', 'checkout'],
251*90c8c64dSAndroid Build Coastguard Worker}
252*90c8c64dSAndroid Build Coastguard Worker
253*90c8c64dSAndroid Build Coastguard Worker
254*90c8c64dSAndroid Build Coastguard Workerdef build_pull_commands(change, branch_name, merge_opt, pick_opt):
255*90c8c64dSAndroid Build Coastguard Worker    """Build command lines for each change.  The command lines will be passed
256*90c8c64dSAndroid Build Coastguard Worker    to subprocess.run()."""
257*90c8c64dSAndroid Build Coastguard Worker
258*90c8c64dSAndroid Build Coastguard Worker    cmds = []
259*90c8c64dSAndroid Build Coastguard Worker    if branch_name is not None:
260*90c8c64dSAndroid Build Coastguard Worker        cmds.append(['repo', 'start', branch_name])
261*90c8c64dSAndroid Build Coastguard Worker    cmds.append(['git', 'fetch', change.fetch_url, change.fetch_ref])
262*90c8c64dSAndroid Build Coastguard Worker    if change.is_merge():
263*90c8c64dSAndroid Build Coastguard Worker        cmds.append(_MERGE_COMMANDS[merge_opt] + ['FETCH_HEAD'])
264*90c8c64dSAndroid Build Coastguard Worker    else:
265*90c8c64dSAndroid Build Coastguard Worker        cmds.append(_PICK_COMMANDS[pick_opt] + ['FETCH_HEAD'])
266*90c8c64dSAndroid Build Coastguard Worker    return cmds
267*90c8c64dSAndroid Build Coastguard Worker
268*90c8c64dSAndroid Build Coastguard Worker
269*90c8c64dSAndroid Build Coastguard Workerdef _sh_quote_command(cmd):
270*90c8c64dSAndroid Build Coastguard Worker    """Convert a command (an argument to subprocess.run()) to a shell command
271*90c8c64dSAndroid Build Coastguard Worker    string."""
272*90c8c64dSAndroid Build Coastguard Worker    return ' '.join(_sh_quote(x) for x in cmd)
273*90c8c64dSAndroid Build Coastguard Worker
274*90c8c64dSAndroid Build Coastguard Worker
275*90c8c64dSAndroid Build Coastguard Workerdef _sh_quote_commands(cmds):
276*90c8c64dSAndroid Build Coastguard Worker    """Convert multiple commands (arguments to subprocess.run()) to shell
277*90c8c64dSAndroid Build Coastguard Worker    command strings."""
278*90c8c64dSAndroid Build Coastguard Worker    return ' && '.join(_sh_quote_command(cmd) for cmd in cmds)
279*90c8c64dSAndroid Build Coastguard Worker
280*90c8c64dSAndroid Build Coastguard Worker
281*90c8c64dSAndroid Build Coastguard Workerdef _main_bash(args):
282*90c8c64dSAndroid Build Coastguard Worker    """Print the bash command to pull the change lists."""
283*90c8c64dSAndroid Build Coastguard Worker    repo_top = find_repo_top(os.getcwd())
284*90c8c64dSAndroid Build Coastguard Worker    project_dirs = build_project_name_dir_dict(args.manifest)
285*90c8c64dSAndroid Build Coastguard Worker    branch_name = _get_local_branch_name_from_args(args)
286*90c8c64dSAndroid Build Coastguard Worker
287*90c8c64dSAndroid Build Coastguard Worker    change_lists = _get_change_lists_from_args(args)
288*90c8c64dSAndroid Build Coastguard Worker    change_list_groups = group_and_sort_change_lists(change_lists)
289*90c8c64dSAndroid Build Coastguard Worker
290*90c8c64dSAndroid Build Coastguard Worker    print(_sh_quote_command(['pushd', repo_top]))
291*90c8c64dSAndroid Build Coastguard Worker    for changes in change_list_groups:
292*90c8c64dSAndroid Build Coastguard Worker        for change in changes:
293*90c8c64dSAndroid Build Coastguard Worker            project_dir = project_dirs.find_directory(
294*90c8c64dSAndroid Build Coastguard Worker                change.project, change.branch, change.project)
295*90c8c64dSAndroid Build Coastguard Worker            cmds = []
296*90c8c64dSAndroid Build Coastguard Worker            cmds.append(['pushd', project_dir])
297*90c8c64dSAndroid Build Coastguard Worker            cmds.extend(build_pull_commands(
298*90c8c64dSAndroid Build Coastguard Worker                change, branch_name, args.merge, args.pick))
299*90c8c64dSAndroid Build Coastguard Worker            cmds.append(['popd'])
300*90c8c64dSAndroid Build Coastguard Worker            print(_sh_quote_commands(cmds))
301*90c8c64dSAndroid Build Coastguard Worker    print(_sh_quote_command(['popd']))
302*90c8c64dSAndroid Build Coastguard Worker
303*90c8c64dSAndroid Build Coastguard Worker
304*90c8c64dSAndroid Build Coastguard Workerdef _do_pull_change_lists_for_project(task, ignore_unknown_changes):
305*90c8c64dSAndroid Build Coastguard Worker    """Pick a list of changes (usually under a project directory)."""
306*90c8c64dSAndroid Build Coastguard Worker    changes, task_opts = task
307*90c8c64dSAndroid Build Coastguard Worker
308*90c8c64dSAndroid Build Coastguard Worker    branch_name = task_opts['branch_name']
309*90c8c64dSAndroid Build Coastguard Worker    merge_opt = task_opts['merge_opt']
310*90c8c64dSAndroid Build Coastguard Worker    pick_opt = task_opts['pick_opt']
311*90c8c64dSAndroid Build Coastguard Worker    project_dirs = task_opts['project_dirs']
312*90c8c64dSAndroid Build Coastguard Worker    repo_top = task_opts['repo_top']
313*90c8c64dSAndroid Build Coastguard Worker
314*90c8c64dSAndroid Build Coastguard Worker    for i, change in enumerate(changes):
315*90c8c64dSAndroid Build Coastguard Worker        try:
316*90c8c64dSAndroid Build Coastguard Worker            cwd = project_dirs.find_directory(change.project, change.branch)
317*90c8c64dSAndroid Build Coastguard Worker        except KeyError:
318*90c8c64dSAndroid Build Coastguard Worker            err_msg = 'error: project "{}" cannot be found in manifest.xml\n'
319*90c8c64dSAndroid Build Coastguard Worker            err_msg = err_msg.format(change.project).encode('utf-8')
320*90c8c64dSAndroid Build Coastguard Worker            if ignore_unknown_changes:
321*90c8c64dSAndroid Build Coastguard Worker                print(err_msg)
322*90c8c64dSAndroid Build Coastguard Worker                continue
323*90c8c64dSAndroid Build Coastguard Worker            return (change, changes[i + 1:], [], err_msg)
324*90c8c64dSAndroid Build Coastguard Worker
325*90c8c64dSAndroid Build Coastguard Worker        print(change.commit_sha1[0:10], i + 1, cwd)
326*90c8c64dSAndroid Build Coastguard Worker        cmds = build_pull_commands(change, branch_name, merge_opt, pick_opt)
327*90c8c64dSAndroid Build Coastguard Worker        for cmd in cmds:
328*90c8c64dSAndroid Build Coastguard Worker            proc = run(cmd, cwd=os.path.join(repo_top, cwd), stderr=PIPE)
329*90c8c64dSAndroid Build Coastguard Worker            if proc.returncode != 0:
330*90c8c64dSAndroid Build Coastguard Worker                return (change, changes[i + 1:], cmd, proc.stderr)
331*90c8c64dSAndroid Build Coastguard Worker    return None
332*90c8c64dSAndroid Build Coastguard Worker
333*90c8c64dSAndroid Build Coastguard Worker
334*90c8c64dSAndroid Build Coastguard Workerdef _print_pull_failures(failures, file=sys.stderr):
335*90c8c64dSAndroid Build Coastguard Worker    """Print pull failures and tracebacks."""
336*90c8c64dSAndroid Build Coastguard Worker    # pylint: disable=redefined-builtin
337*90c8c64dSAndroid Build Coastguard Worker
338*90c8c64dSAndroid Build Coastguard Worker    separator = '=' * 78
339*90c8c64dSAndroid Build Coastguard Worker    separator_sub = '-' * 78
340*90c8c64dSAndroid Build Coastguard Worker
341*90c8c64dSAndroid Build Coastguard Worker    print(separator, file=file)
342*90c8c64dSAndroid Build Coastguard Worker    for failed_change, skipped_changes, cmd, errors in failures:
343*90c8c64dSAndroid Build Coastguard Worker        print('PROJECT:', failed_change.project, file=file)
344*90c8c64dSAndroid Build Coastguard Worker        print('FAILED COMMIT:', failed_change.commit_sha1, file=file)
345*90c8c64dSAndroid Build Coastguard Worker        for change in skipped_changes:
346*90c8c64dSAndroid Build Coastguard Worker            print('PENDING COMMIT:', change.commit_sha1, file=file)
347*90c8c64dSAndroid Build Coastguard Worker        print(separator_sub, file=sys.stderr)
348*90c8c64dSAndroid Build Coastguard Worker        print('FAILED COMMAND:', _sh_quote_command(cmd), file=file)
349*90c8c64dSAndroid Build Coastguard Worker        write_bytes(errors, file=sys.stderr)
350*90c8c64dSAndroid Build Coastguard Worker        print(separator, file=sys.stderr)
351*90c8c64dSAndroid Build Coastguard Worker
352*90c8c64dSAndroid Build Coastguard Worker
353*90c8c64dSAndroid Build Coastguard Workerdef _main_pull(args):
354*90c8c64dSAndroid Build Coastguard Worker    """Pull the change lists."""
355*90c8c64dSAndroid Build Coastguard Worker    repo_top = find_repo_top(os.getcwd())
356*90c8c64dSAndroid Build Coastguard Worker    project_dirs = build_project_name_dir_dict(args.manifest)
357*90c8c64dSAndroid Build Coastguard Worker    branch_name = _get_local_branch_name_from_args(args)
358*90c8c64dSAndroid Build Coastguard Worker
359*90c8c64dSAndroid Build Coastguard Worker    # Collect change lists
360*90c8c64dSAndroid Build Coastguard Worker    change_lists = _get_change_lists_from_args(args)
361*90c8c64dSAndroid Build Coastguard Worker    change_list_groups = group_and_sort_change_lists(change_lists)
362*90c8c64dSAndroid Build Coastguard Worker
363*90c8c64dSAndroid Build Coastguard Worker    # Build the options list for tasks
364*90c8c64dSAndroid Build Coastguard Worker    task_opts = {
365*90c8c64dSAndroid Build Coastguard Worker        'branch_name': branch_name,
366*90c8c64dSAndroid Build Coastguard Worker        'merge_opt': args.merge,
367*90c8c64dSAndroid Build Coastguard Worker        'pick_opt': args.pick,
368*90c8c64dSAndroid Build Coastguard Worker        'project_dirs': project_dirs,
369*90c8c64dSAndroid Build Coastguard Worker        'repo_top': repo_top,
370*90c8c64dSAndroid Build Coastguard Worker    }
371*90c8c64dSAndroid Build Coastguard Worker
372*90c8c64dSAndroid Build Coastguard Worker    # Run the commands to pull the change lists
373*90c8c64dSAndroid Build Coastguard Worker    if args.parallel <= 1:
374*90c8c64dSAndroid Build Coastguard Worker        results = [_do_pull_change_lists_for_project(
375*90c8c64dSAndroid Build Coastguard Worker            (changes, task_opts), args.ignore_unknown_changes)
376*90c8c64dSAndroid Build Coastguard Worker                   for changes in change_list_groups]
377*90c8c64dSAndroid Build Coastguard Worker    else:
378*90c8c64dSAndroid Build Coastguard Worker        pool = multiprocessing.Pool(processes=args.parallel)
379*90c8c64dSAndroid Build Coastguard Worker        results = pool.map(_do_pull_change_lists_for_project,
380*90c8c64dSAndroid Build Coastguard Worker                           zip(change_list_groups, itertools.repeat(task_opts)),
381*90c8c64dSAndroid Build Coastguard Worker                           args.ignore_unknown_changes)
382*90c8c64dSAndroid Build Coastguard Worker
383*90c8c64dSAndroid Build Coastguard Worker    # Print failures and tracebacks
384*90c8c64dSAndroid Build Coastguard Worker    failures = [result for result in results if result]
385*90c8c64dSAndroid Build Coastguard Worker    if failures:
386*90c8c64dSAndroid Build Coastguard Worker        _print_pull_failures(failures)
387*90c8c64dSAndroid Build Coastguard Worker        sys.exit(1)
388*90c8c64dSAndroid Build Coastguard Worker
389*90c8c64dSAndroid Build Coastguard Worker
390*90c8c64dSAndroid Build Coastguard Workerdef _parse_args():
391*90c8c64dSAndroid Build Coastguard Worker    """Parse command line options."""
392*90c8c64dSAndroid Build Coastguard Worker    parser = argparse.ArgumentParser()
393*90c8c64dSAndroid Build Coastguard Worker
394*90c8c64dSAndroid Build Coastguard Worker    parser.add_argument('command', choices=['pull', 'bash', 'json'],
395*90c8c64dSAndroid Build Coastguard Worker                        help='Commands')
396*90c8c64dSAndroid Build Coastguard Worker    add_common_parse_args(parser)
397*90c8c64dSAndroid Build Coastguard Worker
398*90c8c64dSAndroid Build Coastguard Worker
399*90c8c64dSAndroid Build Coastguard Worker    parser.add_argument('--manifest', help='Manifest')
400*90c8c64dSAndroid Build Coastguard Worker
401*90c8c64dSAndroid Build Coastguard Worker    parser.add_argument('-m', '--merge',
402*90c8c64dSAndroid Build Coastguard Worker                        choices=sorted(_MERGE_COMMANDS.keys()),
403*90c8c64dSAndroid Build Coastguard Worker                        default='merge-ff-only',
404*90c8c64dSAndroid Build Coastguard Worker                        help='Method to pull merge commits')
405*90c8c64dSAndroid Build Coastguard Worker
406*90c8c64dSAndroid Build Coastguard Worker    parser.add_argument('-p', '--pick',
407*90c8c64dSAndroid Build Coastguard Worker                        choices=sorted(_PICK_COMMANDS.keys()),
408*90c8c64dSAndroid Build Coastguard Worker                        default='pick',
409*90c8c64dSAndroid Build Coastguard Worker                        help='Method to pull merge commits')
410*90c8c64dSAndroid Build Coastguard Worker
411*90c8c64dSAndroid Build Coastguard Worker    parser.add_argument('-b', '--branch',
412*90c8c64dSAndroid Build Coastguard Worker                        help='Local branch name for `repo start`')
413*90c8c64dSAndroid Build Coastguard Worker
414*90c8c64dSAndroid Build Coastguard Worker    parser.add_argument('-j', '--parallel', default=1, type=int,
415*90c8c64dSAndroid Build Coastguard Worker                        help='Number of parallel running commands')
416*90c8c64dSAndroid Build Coastguard Worker
417*90c8c64dSAndroid Build Coastguard Worker    parser.add_argument('--current-branch', action='store_true',
418*90c8c64dSAndroid Build Coastguard Worker                        help='Pull commits to the current branch')
419*90c8c64dSAndroid Build Coastguard Worker
420*90c8c64dSAndroid Build Coastguard Worker    parser.add_argument('--ignore-unknown-changes', action='store_true',
421*90c8c64dSAndroid Build Coastguard Worker                        help='Ignore changes whose repo is not in the manifest')
422*90c8c64dSAndroid Build Coastguard Worker
423*90c8c64dSAndroid Build Coastguard Worker    return parser.parse_args()
424*90c8c64dSAndroid Build Coastguard Worker
425*90c8c64dSAndroid Build Coastguard Worker
426*90c8c64dSAndroid Build Coastguard Workerdef _get_change_lists_from_args(args):
427*90c8c64dSAndroid Build Coastguard Worker    """Query the change lists by args."""
428*90c8c64dSAndroid Build Coastguard Worker    url_opener = create_url_opener_from_args(args)
429*90c8c64dSAndroid Build Coastguard Worker    return query_change_lists(url_opener, args.gerrit, args.query, args.start,
430*90c8c64dSAndroid Build Coastguard Worker                              args.limits)
431*90c8c64dSAndroid Build Coastguard Worker
432*90c8c64dSAndroid Build Coastguard Worker
433*90c8c64dSAndroid Build Coastguard Workerdef _get_local_branch_name_from_args(args):
434*90c8c64dSAndroid Build Coastguard Worker    """Get the local branch name from args."""
435*90c8c64dSAndroid Build Coastguard Worker    if not args.branch and not args.current_branch and not _confirm(
436*90c8c64dSAndroid Build Coastguard Worker            'Do you want to continue without local branch name?', False):
437*90c8c64dSAndroid Build Coastguard Worker        print('error: `-b` or `--branch` must be specified', file=sys.stderr)
438*90c8c64dSAndroid Build Coastguard Worker        sys.exit(1)
439*90c8c64dSAndroid Build Coastguard Worker    return args.branch
440*90c8c64dSAndroid Build Coastguard Worker
441*90c8c64dSAndroid Build Coastguard Worker
442*90c8c64dSAndroid Build Coastguard Workerdef main():
443*90c8c64dSAndroid Build Coastguard Worker    """Main function"""
444*90c8c64dSAndroid Build Coastguard Worker    args = _parse_args()
445*90c8c64dSAndroid Build Coastguard Worker
446*90c8c64dSAndroid Build Coastguard Worker    if args.gerrit:
447*90c8c64dSAndroid Build Coastguard Worker        args.gerrit = normalize_gerrit_name(args.gerrit)
448*90c8c64dSAndroid Build Coastguard Worker    else:
449*90c8c64dSAndroid Build Coastguard Worker        try:
450*90c8c64dSAndroid Build Coastguard Worker            args.gerrit = find_gerrit_name()
451*90c8c64dSAndroid Build Coastguard Worker        # pylint: disable=bare-except
452*90c8c64dSAndroid Build Coastguard Worker        except:
453*90c8c64dSAndroid Build Coastguard Worker            print('gerrit instance not found, use [-g GERRIT]')
454*90c8c64dSAndroid Build Coastguard Worker            sys.exit(1)
455*90c8c64dSAndroid Build Coastguard Worker
456*90c8c64dSAndroid Build Coastguard Worker    if args.command == 'json':
457*90c8c64dSAndroid Build Coastguard Worker        _main_json(args)
458*90c8c64dSAndroid Build Coastguard Worker    elif args.command == 'bash':
459*90c8c64dSAndroid Build Coastguard Worker        _main_bash(args)
460*90c8c64dSAndroid Build Coastguard Worker    elif args.command == 'pull':
461*90c8c64dSAndroid Build Coastguard Worker        _main_pull(args)
462*90c8c64dSAndroid Build Coastguard Worker    else:
463*90c8c64dSAndroid Build Coastguard Worker        raise KeyError('unknown command')
464*90c8c64dSAndroid Build Coastguard Worker
465*90c8c64dSAndroid Build Coastguard Workerif __name__ == '__main__':
466*90c8c64dSAndroid Build Coastguard Worker    main()
467