2 # Copyright 2015 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
19 SCRIPT_DIR
= os
.path
.dirname(os
.path
.realpath(__file__
))
20 SRC_DIR
= os
.path
.abspath(os
.path
.join(SCRIPT_DIR
, os
.pardir
))
21 import find_depot_tools
22 find_depot_tools
.add_depot_tools_to_path()
24 from gclient
import GClientKeywords
25 from third_party
import upload
27 # Avoid depot_tools/third_party/upload.py print verbose messages.
28 upload
.verbosity
= 0 # Errors only.
30 CHROMIUM_GIT_URL
= 'https://chromium.googlesource.com/chromium/src.git'
31 CL_ISSUE_RE
= re
.compile('^Issue number: ([0-9]+) \((.*)\)$')
32 RIETVELD_URL_RE
= re
.compile('^https?://(.*)/(.*)')
33 ROLL_BRANCH_NAME
= 'special_angle_roll_branch'
34 TRYJOB_STATUS_SLEEP_SECONDS
= 30
36 # Use a shell for subcommands on Windows to get a PATH search.
37 IS_WIN
= sys
.platform
.startswith('win')
38 ANGLE_PATH
= os
.path
.join('third_party', 'angle')
40 CommitInfo
= collections
.namedtuple('CommitInfo', ['git_commit',
42 CLInfo
= collections
.namedtuple('CLInfo', ['issue', 'url', 'rietveld_server'])
45 """Convert a possibly-Windows path to a posix-style path."""
46 (_
, path
) = os
.path
.splitdrive(path
)
47 return path
.replace(os
.sep
, '/')
49 def _ParseGitCommitHash(description
):
50 for line
in description
.splitlines():
51 if line
.startswith('commit '):
52 return line
.split()[1]
53 logging
.error('Failed to parse git commit id from:\n%s\n', description
)
58 def _ParseDepsFile(filename
):
59 with
open(filename
, 'rb') as f
:
60 deps_content
= f
.read()
61 return _ParseDepsDict(deps_content
)
64 def _ParseDepsDict(deps_content
):
66 var
= GClientKeywords
.VarImpl({}, local_scope
)
68 'File': GClientKeywords
.FileImpl
,
69 'From': GClientKeywords
.FromImpl
,
73 exec(deps_content
, global_scope
, local_scope
)
77 def _GenerateCLDescriptionCommand(angle_current
, angle_new
, bugs
):
78 def GetChangeString(current_hash
, new_hash
):
79 return '%s..%s' % (current_hash
[0:7], new_hash
[0:7]);
81 def GetChangeLogURL(git_repo_url
, change_string
):
82 return '%s/+log/%s' % (git_repo_url
, change_string
)
84 def GetBugString(bugs
):
87 bug_str
+= str(bug
) + ','
88 return bug_str
.rstrip(',')
90 if angle_current
.git_commit
!= angle_new
.git_commit
:
91 change_str
= GetChangeString(angle_current
.git_commit
,
93 changelog_url
= GetChangeLogURL(angle_current
.git_repo_url
,
97 '-m', 'Roll ANGLE ' + change_str
,
98 '-m', '%s' % changelog_url
,
99 '-m', GetBugString(bugs
),
104 class AutoRoller(object):
105 def __init__(self
, chromium_src
):
106 self
._chromium
_src
= chromium_src
108 def _RunCommand(self
, command
, working_dir
=None, ignore_exit_code
=False,
110 """Runs a command and returns the stdout from that command.
112 If the command fails (exit code != 0), the function will exit the process.
114 working_dir
= working_dir
or self
._chromium
_src
115 logging
.debug('cmd: %s cwd: %s', ' '.join(command
), working_dir
)
116 env
= os
.environ
.copy()
118 logging
.debug('extra env: %s', extra_env
)
119 env
.update(extra_env
)
120 p
= subprocess
.Popen(command
, stdout
=subprocess
.PIPE
,
121 stderr
=subprocess
.PIPE
, shell
=IS_WIN
, env
=env
,
122 cwd
=working_dir
, universal_newlines
=True)
123 output
= p
.stdout
.read()
128 if not ignore_exit_code
and p
.returncode
!= 0:
129 logging
.error('Command failed: %s\n%s', str(command
), output
)
130 sys
.exit(p
.returncode
)
133 def _GetCommitInfo(self
, path_below_src
, git_hash
=None, git_repo_url
=None):
134 working_dir
= os
.path
.join(self
._chromium
_src
, path_below_src
)
135 self
._RunCommand
(['git', 'fetch', 'origin'], working_dir
=working_dir
)
136 revision_range
= git_hash
or 'origin'
137 ret
= self
._RunCommand
(
138 ['git', '--no-pager', 'log', revision_range
, '--pretty=full', '-1'],
139 working_dir
=working_dir
)
140 return CommitInfo(_ParseGitCommitHash(ret
), git_repo_url
)
142 def _GetDepsCommitInfo(self
, deps_dict
, path_below_src
):
143 entry
= deps_dict
['deps'][_PosixPath('src/%s' % path_below_src
)]
144 at_index
= entry
.find('@')
145 git_repo_url
= entry
[:at_index
]
146 git_hash
= entry
[at_index
+ 1:]
147 return self
._GetCommitInfo
(path_below_src
, git_hash
, git_repo_url
)
149 def _GetCLInfo(self
):
150 cl_output
= self
._RunCommand
(['git', 'cl', 'issue'])
151 m
= CL_ISSUE_RE
.match(cl_output
.strip())
153 logging
.error('Cannot find any CL info. Output was:\n%s', cl_output
)
155 issue_number
= int(m
.group(1))
158 # Parse the Rietveld host from the URL.
159 m
= RIETVELD_URL_RE
.match(url
)
161 logging
.error('Cannot parse Rietveld host from URL: %s', url
)
163 rietveld_server
= m
.group(1)
164 return CLInfo(issue_number
, url
, rietveld_server
)
166 def _GetCurrentBranchName(self
):
167 return self
._RunCommand
(
168 ['git', 'rev-parse', '--abbrev-ref', 'HEAD']).splitlines()[0]
170 def _IsTreeClean(self
):
171 lines
= self
._RunCommand
(
172 ['git', 'status', '--porcelain', '-uno']).splitlines()
176 logging
.debug('Dirty/unversioned files:\n%s', '\n'.join(lines
))
179 def _GetBugList(self
, path_below_src
, angle_current
, angle_new
):
180 working_dir
= os
.path
.join(self
._chromium
_src
, path_below_src
)
181 lines
= self
._RunCommand
(
183 '%s..%s' % (angle_current
.git_commit
, angle_new
.git_commit
)],
184 working_dir
=working_dir
).split('\n')
189 if line
.startswith(bug_prefix
):
190 bugs_strings
= line
[len(bug_prefix
):].split(',')
191 for bug_string
in bugs_strings
:
193 bugs
.add(int(bug_string
))
195 # skip this, it may be a project specific bug such as
196 # "angleproject:X" or an ill-formed BUG= message
200 def _UpdateReadmeFile(self
, readme_path
, new_revision
):
201 readme
= open(os
.path
.join(self
._chromium
_src
, readme_path
), 'r+')
203 m
= re
.sub(re
.compile('.*^Revision\: ([0-9]*).*', re
.MULTILINE
),
204 ('Revision: %s' % new_revision
), txt
)
209 def PrepareRoll(self
, ignore_checks
):
210 # TODO(kjellander): use os.path.normcase, os.path.join etc for all paths for
211 # cross platform compatibility.
213 if not ignore_checks
:
214 if self
._GetCurrentBranchName
() != 'master':
215 logging
.error('Please checkout the master branch.')
217 if not self
._IsTreeClean
():
218 logging
.error('Please make sure you don\'t have any modified files.')
221 # Always clean up any previous roll.
224 logging
.debug('Pulling latest changes')
225 if not ignore_checks
:
226 self
._RunCommand
(['git', 'pull'])
228 self
._RunCommand
(['git', 'checkout', '-b', ROLL_BRANCH_NAME
])
230 # Modify Chromium's DEPS file.
232 # Parse current hashes.
233 deps_filename
= os
.path
.join(self
._chromium
_src
, 'DEPS')
234 deps
= _ParseDepsFile(deps_filename
)
235 angle_current
= self
._GetDepsCommitInfo
(deps
, ANGLE_PATH
)
237 # Find ToT revisions.
238 angle_latest
= self
._GetCommitInfo
(ANGLE_PATH
)
241 # Make sure the roll script doesn't use windows line endings
242 self
._RunCommand
(['git', 'config', 'core.autocrlf', 'true'])
244 self
._UpdateDep
(deps_filename
, ANGLE_PATH
, angle_latest
)
246 if self
._IsTreeClean
():
247 logging
.debug('Tree is clean - no changes detected.')
248 self
._DeleteRollBranch
()
250 bugs
= self
._GetBugList
(ANGLE_PATH
, angle_current
, angle_latest
)
251 description
= _GenerateCLDescriptionCommand(
252 angle_current
, angle_latest
, bugs
)
253 logging
.debug('Committing changes locally.')
254 self
._RunCommand
(['git', 'add', '--update', '.'])
255 self
._RunCommand
(['git', 'commit'] + description
)
256 logging
.debug('Uploading changes...')
257 self
._RunCommand
(['git', 'cl', 'upload'],
258 extra_env
={'EDITOR': 'true'})
260 # Run the default trybots
261 base_try_cmd
= ['git', 'cl', 'try']
262 self
._RunCommand
(base_try_cmd
)
265 # Run additional tryjobs
267 for extra_trybot
in extra_trybots
:
268 extra_try_args
+= ['-b', extra_trybot
]
269 self
._RunCommand
(base_try_cmd
+ extra_try_args
)
271 cl_info
= self
._GetCLInfo
()
272 print 'Issue: %d URL: %s' % (cl_info
.issue
, cl_info
.url
)
274 # Checkout master again.
275 self
._RunCommand
(['git', 'checkout', 'master'])
276 print 'Roll branch left as ' + ROLL_BRANCH_NAME
279 def _UpdateDep(self
, deps_filename
, dep_relative_to_src
, commit_info
):
280 dep_name
= _PosixPath(os
.path
.join('src', dep_relative_to_src
))
282 # roll_dep_svn.py relies on cwd being the Chromium checkout, so let's
283 # temporarily change the working directory and then change back.
285 os
.chdir(os
.path
.dirname(deps_filename
))
286 roll_dep_svn
.update_deps(deps_filename
, dep_relative_to_src
, dep_name
,
287 commit_info
.git_commit
, '')
290 def _DeleteRollBranch(self
):
291 self
._RunCommand
(['git', 'checkout', 'master'])
292 self
._RunCommand
(['git', 'branch', '-D', ROLL_BRANCH_NAME
])
293 logging
.debug('Deleted the local roll branch (%s)', ROLL_BRANCH_NAME
)
296 def _GetBranches(self
):
297 """Returns a tuple of active,branches.
299 The 'active' is the name of the currently active branch and 'branches' is a
300 list of all branches.
302 lines
= self
._RunCommand
(['git', 'branch']).split('\n')
307 # The assumption is that the first char will always be the '*'.
308 active
= l
[1:].strip()
309 branches
.append(active
)
314 return (active
, branches
)
317 active_branch
, branches
= self
._GetBranches
()
318 if active_branch
== ROLL_BRANCH_NAME
:
319 active_branch
= 'master'
320 if ROLL_BRANCH_NAME
in branches
:
321 print 'Aborting pending roll.'
322 self
._RunCommand
(['git', 'checkout', ROLL_BRANCH_NAME
])
323 # Ignore an error here in case an issue wasn't created for some reason.
324 self
._RunCommand
(['git', 'cl', 'set_close'], ignore_exit_code
=True)
325 self
._RunCommand
(['git', 'checkout', active_branch
])
326 self
._RunCommand
(['git', 'branch', '-D', ROLL_BRANCH_NAME
])
331 parser
= argparse
.ArgumentParser(
332 description
='Auto-generates a CL containing an ANGLE roll.')
333 parser
.add_argument('--abort',
334 help=('Aborts a previously prepared roll. '
335 'Closes any associated issues and deletes the roll branches'),
337 parser
.add_argument('--ignore-checks', action
='store_true', default
=False,
338 help=('Skips checks for being on the master branch, dirty workspaces and '
339 'the updating of the checkout. Will still delete and create local '
341 parser
.add_argument('-v', '--verbose', action
='store_true', default
=False,
342 help='Be extra verbose in printing of log messages.')
343 args
= parser
.parse_args()
346 logging
.basicConfig(level
=logging
.DEBUG
)
348 logging
.basicConfig(level
=logging
.ERROR
)
350 autoroller
= AutoRoller(SRC_DIR
)
352 return autoroller
.Abort()
354 return autoroller
.PrepareRoll(args
.ignore_checks
)
356 if __name__
== '__main__':