1 # Copyright 2014 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """This module contains the SourceControl class and related functions."""
11 CROS_VERSION_PATTERN = 'new version number from %s'
14 def DetermineAndCreateSourceControl(opts):
15 """Attempts to determine the underlying source control workflow and returns
16 a SourceControl object.
19 An instance of a SourceControl object, or None if the current workflow
22 (output, _) = bisect_utils.RunGit(['rev-parse', '--is-inside-work-tree'])
24 if output.strip() == 'true':
25 return GitSourceControl(opts)
30 # TODO(qyearsley): Almost all of the methods below could be top-level functions
31 # (or class methods). Refactoring may make this simpler.
32 # pylint: disable=R0201
33 class SourceControl(object):
34 """SourceControl is an abstraction over the source control system."""
37 super(SourceControl, self).__init__()
39 def SyncToRevisionWithGClient(self, revision):
40 """Uses gclient to sync to the specified revision.
42 This is like running gclient sync --revision <revision>.
45 revision: A git SHA1 hash or SVN revision number (depending on workflow).
48 The return code of the call.
50 return bisect_utils.RunGClient(['sync', '--verbose', '--reset', '--force',
51 '--delete_unversioned_trees', '--nohooks', '--revision', revision])
53 def SyncToRevisionWithRepo(self, timestamp):
54 """Uses the repo command to sync all the underlying git depots to the
58 timestamp: The Unix timestamp to sync to.
61 The return code of the call.
63 return bisect_utils.RunRepoSyncAtTimestamp(timestamp)
66 class GitSourceControl(SourceControl):
67 """GitSourceControl is used to query the underlying source control."""
69 def __init__(self, opts):
70 super(GitSourceControl, self).__init__()
76 def GetRevisionList(self, revision_range_end, revision_range_start, cwd=None):
77 """Retrieves a list of revisions between |revision_range_start| and
81 revision_range_end: The SHA1 for the end of the range.
82 revision_range_start: The SHA1 for the beginning of the range.
85 A list of the revisions between |revision_range_start| and
86 |revision_range_end| (inclusive).
88 revision_range = '%s..%s' % (revision_range_start, revision_range_end)
89 cmd = ['log', '--format=%H', '-10000', '--first-parent', revision_range]
90 log_output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
92 revision_hash_list = log_output.split()
93 revision_hash_list.append(revision_range_start)
95 return revision_hash_list
97 def SyncToRevision(self, revision, sync_client=None):
98 """Syncs to the specified revision.
101 revision: The revision to sync to.
102 use_gclient: Specifies whether or not we should sync using gclient or
103 just use source control directly.
110 results = bisect_utils.RunGit(['checkout', revision])[1]
111 elif sync_client == 'gclient':
112 results = self.SyncToRevisionWithGClient(revision)
113 elif sync_client == 'repo':
114 results = self.SyncToRevisionWithRepo(revision)
118 def ResolveToRevision(self, revision_to_check, depot, depot_deps_dict,
120 """Tries to resolve an SVN revision or commit position to a git SHA1.
123 revision_to_check: The user supplied revision string that may need to be
124 resolved to a git SHA1.
125 depot: The depot the revision_to_check is from.
126 depot_deps_dict: A dictionary with information about different depots.
127 search: The number of changelists to try if the first fails to resolve
128 to a git hash. If the value is negative, the function will search
129 backwards chronologically, otherwise it will search forward.
132 A string containing a git SHA1 hash, otherwise None.
134 # Android-chrome is git only, so no need to resolve this to anything else.
135 if depot == 'android-chrome':
136 return revision_to_check
139 if not bisect_utils.IsStringInt(revision_to_check):
140 return revision_to_check
142 depot_svn = 'svn://svn.chromium.org/chrome/trunk/src'
144 if depot != 'chromium':
145 depot_svn = depot_deps_dict[depot]['svn']
147 svn_revision = int(revision_to_check)
151 search_range = xrange(svn_revision, svn_revision + search, 1)
153 search_range = xrange(svn_revision, svn_revision + search, -1)
155 for i in search_range:
156 svn_pattern = 'git-svn-id: %s@%d' % (depot_svn, i)
157 commit_position_pattern = '^Cr-Commit-Position: .*@{#%d}' % i
158 cmd = ['log', '--format=%H', '-1', '--grep', svn_pattern,
159 '--grep', commit_position_pattern, 'origin/master']
161 (log_output, return_code) = bisect_utils.RunGit(cmd, cwd=cwd)
163 assert not return_code, 'An error occurred while running'\
164 ' "git %s"' % ' '.join(cmd)
167 log_output = log_output.strip()
170 git_revision = log_output
176 if bisect_utils.IsStringInt(revision_to_check):
177 return int(revision_to_check)
180 os.chdir(os.path.join(os.getcwd(), 'src', 'third_party',
181 'chromiumos-overlay'))
182 pattern = CROS_VERSION_PATTERN % revision_to_check
183 cmd = ['log', '--format=%ct', '-1', '--grep', pattern]
187 log_output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
189 git_revision = log_output
190 git_revision = int(log_output.strip())
195 def IsInProperBranch(self):
196 """Confirms they're in the master branch for performing the bisection.
197 This is needed or gclient will fail to sync properly.
200 True if the current branch on src is 'master'
202 cmd = ['rev-parse', '--abbrev-ref', 'HEAD']
203 log_output = bisect_utils.CheckRunGit(cmd)
204 log_output = log_output.strip()
206 return log_output == "master"
208 def GetCommitPosition(self, git_revision, cwd=None):
209 """Finds git commit postion for the given git hash.
211 This function executes "git footer --position-num <git hash>" command to get
212 commit position the given revision.
215 git_revision: The git SHA1 to use.
216 cwd: Working directory to run the command from.
219 Git commit position as integer or None.
221 cmd = ['footers', '--position-num', git_revision]
222 output = bisect_utils.CheckRunGit(cmd, cwd)
223 commit_position = output.strip()
225 if bisect_utils.IsStringInt(commit_position):
226 return int(commit_position)
230 def QueryRevisionInfo(self, revision, cwd=None):
231 """Gathers information on a particular revision, such as author's name,
232 email, subject, and date.
235 revision: Revision you want to gather information on.
238 A dict in the following format:
249 formats = ['%aN', '%aE', '%s', '%cD', '%b']
250 targets = ['author', 'email', 'subject', 'date', 'body']
252 for i in xrange(len(formats)):
253 cmd = ['log', '--format=%s' % formats[i], '-1', revision]
254 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
255 commit_info[targets[i]] = output.rstrip()
259 def CheckoutFileAtRevision(self, file_name, revision, cwd=None):
260 """Performs a checkout on a file at the given revision.
265 return not bisect_utils.RunGit(
266 ['checkout', revision, file_name], cwd=cwd)[1]
268 def RevertFileToHead(self, file_name):
269 """Un-stages a file and resets the file's state to HEAD.
274 # Reset doesn't seem to return 0 on success.
275 bisect_utils.RunGit(['reset', 'HEAD', file_name])
277 return not bisect_utils.RunGit(['checkout', bisect_utils.FILE_DEPS_GIT])[1]
279 def QueryFileRevisionHistory(self, filename, revision_start, revision_end):
280 """Returns a list of commits that modified this file.
283 filename: Name of file.
284 revision_start: Start of revision range.
285 revision_end: End of revision range.
288 Returns a list of commits that touched this file.
290 cmd = ['log', '--format=%H', '%s~1..%s' % (revision_start, revision_end),
292 output = bisect_utils.CheckRunGit(cmd)
294 return [o for o in output.split('\n') if o]