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.
16 SCRIPT_DIR
= os
.path
.dirname(os
.path
.realpath(__file__
))
17 SRC_DIR
= os
.path
.abspath(os
.path
.join(SCRIPT_DIR
, os
.pardir
))
18 import find_depot_tools
19 find_depot_tools
.add_depot_tools_to_path()
21 from gclient
import GClientKeywords
22 from third_party
import upload
24 # Avoid depot_tools/third_party/upload.py print verbose messages.
25 upload
.verbosity
= 0 # Errors only.
27 CHROMIUM_GIT_URL
= 'https://chromium.googlesource.com/chromium/src.git'
28 CL_ISSUE_RE
= re
.compile('^Issue number: ([0-9]+) \((.*)\)$')
29 RIETVELD_URL_RE
= re
.compile('^https?://(.*)/(.*)')
30 ROLL_BRANCH_NAME
= 'special_angle_roll_branch'
31 TRYJOB_STATUS_SLEEP_SECONDS
= 30
33 # Use a shell for subcommands on Windows to get a PATH search.
34 USE_SHELL
= sys
.platform
.startswith('win')
35 ANGLE_PATH
= 'third_party/angle'
37 CommitInfo
= collections
.namedtuple('CommitInfo', ['git_commit',
39 CLInfo
= collections
.namedtuple('CLInfo', ['issue', 'url', 'rietveld_server'])
42 def _ParseGitCommitHash(description
):
43 for line
in description
.splitlines():
44 if line
.startswith('commit '):
45 return line
.split()[1]
46 logging
.error('Failed to parse git commit id from:\n%s\n', description
)
51 def _ParseDepsFile(filename
):
52 with
open(filename
, 'rb') as f
:
53 deps_content
= f
.read()
54 return _ParseDepsDict(deps_content
)
57 def _ParseDepsDict(deps_content
):
59 var
= GClientKeywords
.VarImpl({}, local_scope
)
61 'File': GClientKeywords
.FileImpl
,
62 'From': GClientKeywords
.FromImpl
,
66 exec(deps_content
, global_scope
, local_scope
)
70 def _GenerateCLDescription(angle_current
, angle_new
):
73 def GetChangeString(current_hash
, new_hash
):
74 return '%s..%s' % (current_hash
[0:7], new_hash
[0:7]);
76 def GetChangeLogURL(git_repo_url
, change_string
):
77 return '%s/+log/%s' % (git_repo_url
, change_string
)
79 if angle_current
.git_commit
!= angle_new
.git_commit
:
80 change_str
= GetChangeString(angle_current
.git_commit
,
82 changelog_url
= GetChangeLogURL(angle_current
.git_repo_url
,
85 description
= 'Roll ANGLE ' + change_str
+ '\n\n'
86 description
+= '%s\n\n' % changelog_url
87 description
+= 'BUG=\nTEST=bots\n'
91 class AutoRoller(object):
92 def __init__(self
, chromium_src
):
93 self
._chromium
_src
= chromium_src
95 def _RunCommand(self
, command
, working_dir
=None, ignore_exit_code
=False,
97 """Runs a command and returns the stdout from that command.
99 If the command fails (exit code != 0), the function will exit the process.
101 working_dir
= working_dir
or self
._chromium
_src
102 logging
.debug('cmd: %s cwd: %s', ' '.join(command
), working_dir
)
103 env
= os
.environ
.copy()
105 logging
.debug('extra env: %s', extra_env
)
106 env
.update(extra_env
)
107 p
= subprocess
.Popen(command
, stdout
=subprocess
.PIPE
,
108 stderr
=subprocess
.PIPE
, shell
=USE_SHELL
, env
=env
,
109 cwd
=working_dir
, universal_newlines
=True)
110 output
= p
.stdout
.read()
115 if not ignore_exit_code
and p
.returncode
!= 0:
116 logging
.error('Command failed: %s\n%s', str(command
), output
)
117 sys
.exit(p
.returncode
)
120 def _GetCommitInfo(self
, path_below_src
, git_hash
=None, git_repo_url
=None):
121 working_dir
= os
.path
.join(self
._chromium
_src
, path_below_src
)
122 self
._RunCommand
(['git', 'fetch', 'origin'], working_dir
=working_dir
)
123 revision_range
= git_hash
or 'origin'
124 ret
= self
._RunCommand
(
125 ['git', '--no-pager', 'log', revision_range
, '--pretty=full', '-1'],
126 working_dir
=working_dir
)
127 return CommitInfo(_ParseGitCommitHash(ret
), git_repo_url
)
129 def _GetDepsCommitInfo(self
, deps_dict
, path_below_src
):
130 entry
= deps_dict
['deps']['src/%s' % path_below_src
]
131 at_index
= entry
.find('@')
132 git_repo_url
= entry
[:at_index
]
133 git_hash
= entry
[at_index
+ 1:]
134 return self
._GetCommitInfo
(path_below_src
, git_hash
, git_repo_url
)
136 def _GetCLInfo(self
):
137 cl_output
= self
._RunCommand
(['git', 'cl', 'issue'])
138 m
= CL_ISSUE_RE
.match(cl_output
.strip())
140 logging
.error('Cannot find any CL info. Output was:\n%s', cl_output
)
142 issue_number
= int(m
.group(1))
145 # Parse the Rietveld host from the URL.
146 m
= RIETVELD_URL_RE
.match(url
)
148 logging
.error('Cannot parse Rietveld host from URL: %s', url
)
150 rietveld_server
= m
.group(1)
151 return CLInfo(issue_number
, url
, rietveld_server
)
153 def _GetCurrentBranchName(self
):
154 return self
._RunCommand
(
155 ['git', 'rev-parse', '--abbrev-ref', 'HEAD']).splitlines()[0]
157 def _IsTreeClean(self
):
158 lines
= self
._RunCommand
(
159 ['git', 'status', '--porcelain', '-uno']).splitlines()
163 logging
.debug('Dirty/unversioned files:\n%s', '\n'.join(lines
))
166 def _UpdateReadmeFile(self
, readme_path
, new_revision
):
167 readme
= open(os
.path
.join(self
._chromium
_src
, readme_path
), 'r+')
169 m
= re
.sub(re
.compile('.*^Revision\: ([0-9]*).*', re
.MULTILINE
),
170 ('Revision: %s' % new_revision
), txt
)
175 def PrepareRoll(self
, ignore_checks
):
176 # TODO(kjellander): use os.path.normcase, os.path.join etc for all paths for
177 # cross platform compatibility.
179 if not ignore_checks
:
180 if self
._GetCurrentBranchName
() != 'master':
181 logging
.error('Please checkout the master branch.')
183 if not self
._IsTreeClean
():
184 logging
.error('Please make sure you don\'t have any modified files.')
187 # Always clean up any previous roll.
190 logging
.debug('Pulling latest changes')
191 if not ignore_checks
:
192 self
._RunCommand
(['git', 'pull'])
194 self
._RunCommand
(['git', 'checkout', '-b', ROLL_BRANCH_NAME
])
196 # Modify Chromium's DEPS file.
198 # Parse current hashes.
199 deps_filename
= os
.path
.join(self
._chromium
_src
, 'DEPS')
200 deps
= _ParseDepsFile(deps_filename
)
201 angle_current
= self
._GetDepsCommitInfo
(deps
, ANGLE_PATH
)
203 # Find ToT revisions.
204 angle_latest
= self
._GetCommitInfo
(ANGLE_PATH
)
206 self
._UpdateDep
(deps_filename
, ANGLE_PATH
, angle_latest
)
208 if self
._IsTreeClean
():
209 logging
.debug('Tree is clean - no changes detected.')
210 self
._DeleteRollBranch
()
212 description
= _GenerateCLDescription(angle_current
, angle_latest
)
213 logging
.debug('Committing changes locally.')
214 self
._RunCommand
(['git', 'add', '--update', '.'])
215 self
._RunCommand
(['git', 'commit', '-m', description
])
216 logging
.debug('Uploading changes...')
217 self
._RunCommand
(['git', 'cl', 'upload', '-m', description
],
218 extra_env
={'EDITOR': 'true'})
219 cl_info
= self
._GetCLInfo
()
220 print 'Issue: %d URL: %s' % (cl_info
.issue
, cl_info
.url
)
222 # Checkout master again.
223 self
._RunCommand
(['git', 'checkout', 'master'])
224 print 'Roll branch left as ' + ROLL_BRANCH_NAME
227 def _UpdateDep(self
, deps_filename
, dep_relative_to_src
, commit_info
):
228 dep_name
= os
.path
.join('src', dep_relative_to_src
)
230 # roll_dep_svn.py relies on cwd being the Chromium checkout, so let's
231 # temporarily change the working directory and then change back.
233 os
.chdir(os
.path
.dirname(deps_filename
))
234 roll_dep_svn
.update_deps(deps_filename
, dep_relative_to_src
, dep_name
,
235 commit_info
.git_commit
, '')
238 def _DeleteRollBranch(self
):
239 self
._RunCommand
(['git', 'checkout', 'master'])
240 self
._RunCommand
(['git', 'branch', '-D', ROLL_BRANCH_NAME
])
241 logging
.debug('Deleted the local roll branch (%s)', ROLL_BRANCH_NAME
)
244 def _GetBranches(self
):
245 """Returns a tuple of active,branches.
247 The 'active' is the name of the currently active branch and 'branches' is a
248 list of all branches.
250 lines
= self
._RunCommand
(['git', 'branch']).split('\n')
255 # The assumption is that the first char will always be the '*'.
256 active
= l
[1:].strip()
257 branches
.append(active
)
262 return (active
, branches
)
265 active_branch
, branches
= self
._GetBranches
()
266 if active_branch
== ROLL_BRANCH_NAME
:
267 active_branch
= 'master'
268 if ROLL_BRANCH_NAME
in branches
:
269 print 'Aborting pending roll.'
270 self
._RunCommand
(['git', 'checkout', ROLL_BRANCH_NAME
])
271 # Ignore an error here in case an issue wasn't created for some reason.
272 self
._RunCommand
(['git', 'cl', 'set_close'], ignore_exit_code
=True)
273 self
._RunCommand
(['git', 'checkout', active_branch
])
274 self
._RunCommand
(['git', 'branch', '-D', ROLL_BRANCH_NAME
])
279 if sys
.platform
in ('win32', 'cygwin'):
280 logging
.error('Only Linux and Mac platforms are supported right now.')
283 parser
= argparse
.ArgumentParser(
284 description
='Auto-generates a CL containing an ANGLE roll.')
285 parser
.add_argument('--abort',
286 help=('Aborts a previously prepared roll. '
287 'Closes any associated issues and deletes the roll branches'),
289 parser
.add_argument('--ignore-checks', action
='store_true', default
=False,
290 help=('Skips checks for being on the master branch, dirty workspaces and '
291 'the updating of the checkout. Will still delete and create local '
293 parser
.add_argument('-v', '--verbose', action
='store_true', default
=False,
294 help='Be extra verbose in printing of log messages.')
295 args
= parser
.parse_args()
298 logging
.basicConfig(level
=logging
.DEBUG
)
300 logging
.basicConfig(level
=logging
.ERROR
)
302 autoroller
= AutoRoller(SRC_DIR
)
304 return autoroller
.Abort()
306 return autoroller
.PrepareRoll(args
.ignore_checks
)
308 if __name__
== '__main__':