blob: fc15c6a4dcd92b841e277b06cacc3be0abe3eac1 [file] [log] [blame]
Josip Sokcevic4de5dea2022-03-23 21:15:14 +00001#!/usr/bin/env python3
maruel@chromium.org96550942015-05-22 18:46:51 +00002# Copyright 2015 The Chromium Authors. All rights reserved.
szager@chromium.org03fd85b2014-06-09 23:43:33 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
maruel@chromium.org96550942015-05-22 18:46:51 +00005"""Rolls DEPS controlled dependency.
szager@chromium.org03fd85b2014-06-09 23:43:33 +00006
Josip Sokcevicc1a06c12022-01-12 00:44:14 +00007Works only with git checkout and git dependencies. Currently this script will
8always roll to the tip of to origin/main.
szager@chromium.org03fd85b2014-06-09 23:43:33 +00009"""
10
Raul Tambre80ee78e2019-05-06 22:41:05 +000011from __future__ import print_function
12
sbc@chromium.org30e5b232015-06-01 18:06:59 +000013import argparse
Thiago Perrottaa0892812022-09-03 11:22:46 +000014import itertools
szager@chromium.org03fd85b2014-06-09 23:43:33 +000015import os
16import re
Corentin Wallez5157fbf2020-11-12 22:31:35 +000017import subprocess2
szager@chromium.org03fd85b2014-06-09 23:43:33 +000018import sys
Bruce Dawson03af44a2022-12-27 19:37:58 +000019import tempfile
szager@chromium.org03fd85b2014-06-09 23:43:33 +000020
scottmg@chromium.orgc20f4702015-05-23 00:44:46 +000021NEED_SHELL = sys.platform.startswith('win')
Mike Frysinger124bb8e2023-09-06 05:48:55 +000022GCLIENT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
23 'gclient.py')
scottmg@chromium.orgc20f4702015-05-23 00:44:46 +000024
Marc-Antoine Ruel1e2cb152019-04-17 17:32:52 +000025# Commit subject that will be considered a roll. In the format generated by the
26# git log used, so it's "<year>-<month>-<day> <author> <subject>"
27_ROLL_SUBJECT = re.compile(
28 # Date
29 r'^\d\d\d\d-\d\d-\d\d '
30 # Author
31 r'[^ ]+ '
32 # Subject
33 r'('
Mike Frysinger124bb8e2023-09-06 05:48:55 +000034 # Generated by
35 # https://skia.googlesource.com/buildbot/+/HEAdA/autoroll/go/repo_manager/deps_repo_manager.go
36 r'Roll [^ ]+ [a-f0-9]+\.\.[a-f0-9]+ \(\d+ commits\)'
37 r'|'
38 # Generated by
39 # https://chromium.googlesource.com/infra/infra/+/HEAD/recipes/recipe_modules/recipe_autoroller/api.py
40 r'Roll recipe dependencies \(trivial\)\.'
Marc-Antoine Ruel1e2cb152019-04-17 17:32:52 +000041 r')$')
42
43
sbc@chromium.orge5d984b2015-05-29 22:09:39 +000044class Error(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000045 pass
sbc@chromium.orge5d984b2015-05-29 22:09:39 +000046
47
smut7036c4f2016-06-09 14:28:48 -070048class AlreadyRolledError(Error):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000049 pass
smut7036c4f2016-06-09 14:28:48 -070050
51
scottmg@chromium.orgc20f4702015-05-23 00:44:46 +000052def check_output(*args, **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000053 """subprocess2.check_output() passing shell=True on Windows for git."""
54 kwargs.setdefault('shell', NEED_SHELL)
55 return subprocess2.check_output(*args, **kwargs).decode('utf-8')
scottmg@chromium.orgc20f4702015-05-23 00:44:46 +000056
57
58def check_call(*args, **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000059 """subprocess2.check_call() passing shell=True on Windows for git."""
60 kwargs.setdefault('shell', NEED_SHELL)
61 subprocess2.check_call(*args, **kwargs)
scottmg@chromium.orgc20f4702015-05-23 00:44:46 +000062
63
Corentin Wallez5157fbf2020-11-12 22:31:35 +000064def return_code(*args, **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000065 """subprocess2.call() passing shell=True on Windows for git and
Edward Lesmescf06cad2020-12-14 22:03:23 +000066 subprocess2.DEVNULL for stdout and stderr."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +000067 kwargs.setdefault('shell', NEED_SHELL)
68 kwargs.setdefault('stdout', subprocess2.DEVNULL)
69 kwargs.setdefault('stderr', subprocess2.DEVNULL)
70 return subprocess2.call(*args, **kwargs)
Corentin Wallez5157fbf2020-11-12 22:31:35 +000071
72
73def is_pristine(root):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000074 """Returns True if a git checkout is pristine."""
75 # `git rev-parse --verify` has a non-zero return code if the revision
76 # doesn't exist.
77 diff_cmd = ['git', 'diff', '--ignore-submodules', 'origin/main']
78 return (not check_output(diff_cmd, cwd=root).strip()
79 and not check_output(diff_cmd + ['--cached'], cwd=root).strip())
Corentin Wallez5157fbf2020-11-12 22:31:35 +000080
szager@chromium.org03fd85b2014-06-09 23:43:33 +000081
Josip Sokcevic9c0dc302020-11-20 18:41:25 +000082def get_log_url(upstream_url, head, tot):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000083 """Returns an URL to read logs via a Web UI if applicable."""
84 if re.match(r'https://[^/]*\.googlesource\.com/', upstream_url):
85 # gitiles
86 return '%s/+log/%s..%s' % (upstream_url, head[:12], tot[:12])
87 if upstream_url.startswith('https://github.com/'):
88 upstream_url = upstream_url.rstrip('/')
89 if upstream_url.endswith('.git'):
90 upstream_url = upstream_url[:-len('.git')]
91 return '%s/compare/%s...%s' % (upstream_url, head[:12], tot[:12])
92 return None
maruel@chromium.org398ed342015-09-29 12:27:25 +000093
94
95def should_show_log(upstream_url):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000096 """Returns True if a short log should be included in the tree."""
97 # Skip logs for very active projects.
98 if upstream_url.endswith('/v8/v8.git'):
99 return False
100 if 'webrtc' in upstream_url:
101 return False
102 return True
maruel@chromium.org398ed342015-09-29 12:27:25 +0000103
104
Edward Lemurfaae42e2018-11-26 18:34:30 +0000105def gclient(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000106 """Executes gclient with the given args and returns the stdout."""
107 return check_output([sys.executable, GCLIENT_PATH] + args).strip()
sbc@chromium.org98201122015-04-22 20:21:34 +0000108
maruel@chromium.org96550942015-05-22 18:46:51 +0000109
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000110def generate_commit_message(full_dir, dependency, head, roll_to, no_log,
111 log_limit):
112 """Creates the commit message for this specific roll."""
113 commit_range = '%s..%s' % (head, roll_to)
114 commit_range_for_header = '%s..%s' % (head[:9], roll_to[:9])
115 upstream_url = check_output(['git', 'config', 'remote.origin.url'],
116 cwd=full_dir).strip()
117 log_url = get_log_url(upstream_url, head, roll_to)
118 cmd = ['git', 'log', commit_range, '--date=short', '--no-merges']
119 logs = check_output(
120 # Args with '=' are automatically quoted.
121 cmd + ['--format=%ad %ae %s', '--'],
122 cwd=full_dir).rstrip()
123 logs = re.sub(r'(?m)^(\d\d\d\d-\d\d-\d\d [^@]+)@[^ ]+( .*)$', r'\1\2', logs)
124 lines = logs.splitlines()
125 cleaned_lines = [l for l in lines if not _ROLL_SUBJECT.match(l)]
126 logs = '\n'.join(cleaned_lines) + '\n'
Marc-Antoine Ruel85a8c102017-12-12 15:42:25 -0500127
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000128 nb_commits = len(lines)
129 rolls = nb_commits - len(cleaned_lines)
130 header = 'Roll %s/ %s (%d commit%s%s)\n\n' % (
131 dependency, commit_range_for_header, nb_commits,
132 's' if nb_commits > 1 else '',
133 ('; %s trivial rolls' % rolls) if rolls else '')
134 log_section = ''
135 if log_url:
136 log_section = log_url + '\n\n'
137 log_section += '$ %s ' % ' '.join(cmd)
138 log_section += '--format=\'%ad %ae %s\'\n'
139 log_section = log_section.replace(commit_range, commit_range_for_header)
140 # It is important that --no-log continues to work, as it is used by
141 # internal -> external rollers. Please do not remove or break it.
142 if not no_log and should_show_log(upstream_url):
143 if len(cleaned_lines) > log_limit:
144 # Keep the first N/2 log entries and last N/2 entries.
145 lines = logs.splitlines(True)
146 lines = lines[:log_limit // 2] + ['(...)\n'
147 ] + lines[-log_limit // 2:]
148 logs = ''.join(lines)
149 log_section += logs
150 return header + log_section
maruel@chromium.org398ed342015-09-29 12:27:25 +0000151
maruel@chromium.org96550942015-05-22 18:46:51 +0000152
Aravind Vasudevan474e28e2023-01-18 20:29:51 +0000153def is_submoduled():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000154 """Returns true if gclient root has submodules"""
155 return os.path.isfile(os.path.join(gclient(['root']), ".gitmodules"))
Aravind Vasudevan474e28e2023-01-18 20:29:51 +0000156
157
158def get_submodule_rev(submodule):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000159 """Returns revision of the given submodule path"""
160 rev_output = check_output(['git', 'submodule', 'status', submodule],
161 cwd=gclient(['root'])).strip()
Aravind Vasudevan474e28e2023-01-18 20:29:51 +0000162
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000163 # git submodule status <path> returns all submodules with its rev in the
164 # pattern: `(+|-| )(<revision>) (submodule.path)`
165 revision = rev_output.split(' ')[0]
166 return revision[1:] if revision[0] in ('+', '-') else revision
Aravind Vasudevan474e28e2023-01-18 20:29:51 +0000167
168
Edward Lemurfaae42e2018-11-26 18:34:30 +0000169def calculate_roll(full_dir, dependency, roll_to):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000170 """Calculates the roll for a dependency by processing gclient_dict, and
Marc-Antoine Ruel85a8c102017-12-12 15:42:25 -0500171 fetching the dependency via git.
172 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000173 # if the super-project uses submodules, get rev directly using git.
174 if is_submoduled():
175 head = get_submodule_rev(dependency)
176 else:
177 head = gclient(['getdep', '-r', dependency])
178 if not head:
179 raise Error('%s is unpinned.' % dependency)
180 check_call(['git', 'fetch', 'origin', '--quiet'], cwd=full_dir)
181 if roll_to == 'origin/HEAD':
182 check_output(['git', 'remote', 'set-head', 'origin', '-a'],
183 cwd=full_dir)
Josip Sokcevic69902d02021-02-02 21:57:55 +0000184
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000185 roll_to = check_output(['git', 'rev-parse', roll_to], cwd=full_dir).strip()
186 return head, roll_to
Marc-Antoine Ruel85a8c102017-12-12 15:42:25 -0500187
Josip Sokcevic69902d02021-02-02 21:57:55 +0000188
Robert Iannuccic1e65942018-10-18 17:59:45 +0000189def gen_commit_msg(logs, cmdline, reviewers, bug):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000190 """Returns the final commit message."""
191 commit_msg = ''
192 if len(logs) > 1:
193 commit_msg = 'Rolling %d dependencies\n\n' % len(logs)
194 commit_msg += '\n\n'.join(logs)
195 commit_msg += '\nCreated with:\n ' + cmdline + '\n'
196 commit_msg += 'R=%s\n' % ','.join(reviewers) if reviewers else ''
197 commit_msg += '\nBug: %s\n' % bug if bug else ''
198 return commit_msg
Marc-Antoine Ruel85a8c102017-12-12 15:42:25 -0500199
200
Edward Lemurfaae42e2018-11-26 18:34:30 +0000201def finalize(commit_msg, current_dir, rolls):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000202 """Commits changes to the DEPS file, then uploads a CL."""
203 print('Commit message:')
204 print('\n'.join(' ' + i for i in commit_msg.splitlines()))
Marc-Antoine Ruel85a8c102017-12-12 15:42:25 -0500205
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000206 # Pull the dependency to the right revision. This is surprising to users
207 # otherwise. The revision update is done before commiting to update
208 # submodule revision if present.
209 for dependency, (_head, roll_to, full_dir) in sorted(rolls.items()):
210 check_call(['git', 'checkout', '--quiet', roll_to], cwd=full_dir)
Aravind Vasudevan474e28e2023-01-18 20:29:51 +0000211
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000212 # This adds the submodule revision update to the commit.
213 if is_submoduled():
214 check_call([
215 'git', 'update-index', '--add', '--cacheinfo',
216 '160000,{},{}'.format(roll_to, dependency)
217 ],
218 cwd=current_dir)
Aravind Vasudevan474e28e2023-01-18 20:29:51 +0000219
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000220 check_call(['git', 'add', 'DEPS'], cwd=current_dir)
221 # We have to set delete=False and then let the object go out of scope so
222 # that the file can be opened by name on Windows.
223 with tempfile.NamedTemporaryFile('w+', newline='', delete=False) as f:
224 commit_filename = f.name
225 f.write(commit_msg)
226 check_call(['git', 'commit', '--quiet', '--file', commit_filename],
227 cwd=current_dir)
228 os.remove(commit_filename)
maruel@chromium.org398ed342015-09-29 12:27:25 +0000229
szager@chromium.org03fd85b2014-06-09 23:43:33 +0000230
maruel@chromium.org96550942015-05-22 18:46:51 +0000231def main():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000232 parser = argparse.ArgumentParser(description=__doc__)
233 parser.add_argument('--ignore-dirty-tree',
234 action='store_true',
235 help='Roll anyways, even if there is a diff.')
236 parser.add_argument(
237 '-r',
238 '--reviewer',
239 action='append',
240 help='To specify multiple reviewers, either use a comma separated '
241 'list, e.g. -r joe,jane,john or provide the flag multiple times, e.g. '
242 '-r joe -r jane. Defaults to @chromium.org')
243 parser.add_argument('-b',
244 '--bug',
245 help='Associate a bug number to the roll')
246 # It is important that --no-log continues to work, as it is used by
247 # internal -> external rollers. Please do not remove or break it.
248 parser.add_argument(
249 '--no-log',
250 action='store_true',
251 help='Do not include the short log in the commit message')
252 parser.add_argument('--log-limit',
253 type=int,
254 default=100,
255 help='Trim log after N commits (default: %(default)s)')
256 parser.add_argument(
257 '--roll-to',
258 default='origin/HEAD',
259 help='Specify the new commit to roll to (default: %(default)s)')
260 parser.add_argument('--key',
261 action='append',
262 default=[],
263 help='Regex(es) for dependency in DEPS file')
264 parser.add_argument('dep_path', nargs='+', help='Path(s) to dependency')
265 args = parser.parse_args()
maruel@chromium.org96550942015-05-22 18:46:51 +0000266
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000267 if len(args.dep_path) > 1:
268 if args.roll_to != 'origin/HEAD':
269 parser.error(
270 'Can\'t use multiple paths to roll simultaneously and --roll-to'
271 )
272 if args.key:
273 parser.error(
274 'Can\'t use multiple paths to roll simultaneously and --key')
275 reviewers = None
276 if args.reviewer:
277 reviewers = list(itertools.chain(*[r.split(',')
278 for r in args.reviewer]))
279 for i, r in enumerate(reviewers):
280 if not '@' in r:
281 reviewers[i] = r + '@chromium.org'
maruel@chromium.org96550942015-05-22 18:46:51 +0000282
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000283 gclient_root = gclient(['root'])
284 current_dir = os.getcwd()
285 dependencies = sorted(
286 d.replace('\\', '/').rstrip('/') for d in args.dep_path)
287 cmdline = 'roll-dep ' + ' '.join(dependencies) + ''.join(' --key ' + k
288 for k in args.key)
289 try:
290 if not args.ignore_dirty_tree and not is_pristine(current_dir):
291 raise Error('Ensure %s is clean first (no non-merged commits).' %
292 current_dir)
293 # First gather all the information without modifying anything, except
294 # for a git fetch.
295 rolls = {}
296 for dependency in dependencies:
297 full_dir = os.path.normpath(os.path.join(gclient_root, dependency))
298 if not os.path.isdir(full_dir):
299 print('Dependency %s not found at %s' % (dependency, full_dir))
300 full_dir = os.path.normpath(
301 os.path.join(current_dir, dependency))
302 print('Will look for relative dependency at %s' % full_dir)
303 if not os.path.isdir(full_dir):
304 raise Error('Directory not found: %s (%s)' %
305 (dependency, full_dir))
Edward Lemurfaae42e2018-11-26 18:34:30 +0000306
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000307 head, roll_to = calculate_roll(full_dir, dependency, args.roll_to)
308 if roll_to == head:
309 if len(dependencies) == 1:
310 raise AlreadyRolledError('No revision to roll!')
311 print('%s: Already at latest commit %s' % (dependency, roll_to))
312 else:
313 print('%s: Rolling from %s to %s' %
314 (dependency, head[:10], roll_to[:10]))
315 rolls[dependency] = (head, roll_to, full_dir)
sbc@chromium.orge5d984b2015-05-29 22:09:39 +0000316
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000317 logs = []
318 setdep_args = []
319 for dependency, (head, roll_to, full_dir) in sorted(rolls.items()):
320 log = generate_commit_message(full_dir, dependency, head, roll_to,
321 args.no_log, args.log_limit)
322 logs.append(log)
323 setdep_args.extend(['-r', '{}@{}'.format(dependency, roll_to)])
Edward Lesmesc772cf72018-04-03 14:47:30 -0400324
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000325 # DEPS is updated even if the repository uses submodules.
326 gclient(['setdep'] + setdep_args)
Marc-Antoine Ruel85a8c102017-12-12 15:42:25 -0500327
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000328 commit_msg = gen_commit_msg(logs, cmdline, reviewers, args.bug)
329 finalize(commit_msg, current_dir, rolls)
330 except Error as e:
331 sys.stderr.write('error: %s\n' % e)
332 return 2 if isinstance(e, AlreadyRolledError) else 1
333 except subprocess2.CalledProcessError:
334 return 1
sbc@chromium.orge5d984b2015-05-29 22:09:39 +0000335
Marc-Antoine Ruel85a8c102017-12-12 15:42:25 -0500336 print('')
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000337 if not reviewers:
338 print('You forgot to pass -r, make sure to insert a R=foo@example.com '
339 'line')
340 print('to the commit description before emailing.')
341 print('')
342 print('Run:')
343 print(' git cl upload --send-mail')
344 return 0
szager@chromium.org03fd85b2014-06-09 23:43:33 +0000345
sbc@chromium.org98201122015-04-22 20:21:34 +0000346
szager@chromium.org03fd85b2014-06-09 23:43:33 +0000347if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000348 sys.exit(main())