blob: 7e91afcb99ae53015b41e137fdddd0ca4e728d00 [file] [log] [blame]
Raul Tambrea04028c2019-05-13 17:23:36 +00001# coding=utf-8
maruel@chromium.org9799a072012-01-11 00:26:25 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Manages a project checkout.
6
agablec972d182016-10-24 16:59:13 -07007Includes support only for git.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +00008"""
9
Raul Tambre80ee78e2019-05-06 22:41:05 +000010from __future__ import print_function
11
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000012import fnmatch
13import logging
14import os
15import re
maruel@chromium.org5e975632011-09-29 18:07:06 +000016import shutil
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000017import subprocess
18import sys
19import tempfile
20
vapier9f343712016-06-22 07:13:20 -070021# The configparser module was renamed in Python 3.
22try:
23 import configparser
24except ImportError:
25 import ConfigParser as configparser
26
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000027import patch
28import scm
29import subprocess2
30
31
csharp@chromium.org9af0a112013-03-20 20:21:35 +000032if sys.platform in ('cygwin', 'win32'):
33 # Disable timeouts on Windows since we can't have shells with timeouts.
34 GLOBAL_TIMEOUT = None
35 FETCH_TIMEOUT = None
36else:
37 # Default timeout of 15 minutes.
38 GLOBAL_TIMEOUT = 15*60
39 # Use a larger timeout for checkout since it can be a genuinely slower
40 # operation.
41 FETCH_TIMEOUT = 30*60
42
43
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000044def get_code_review_setting(path, key,
45 codereview_settings_file='codereview.settings'):
46 """Parses codereview.settings and return the value for the key if present.
47
48 Don't cache the values in case the file is changed."""
49 # TODO(maruel): Do not duplicate code.
50 settings = {}
51 try:
52 settings_file = open(os.path.join(path, codereview_settings_file), 'r')
53 try:
54 for line in settings_file.readlines():
55 if not line or line.startswith('#'):
56 continue
57 if not ':' in line:
58 # Invalid file.
59 return None
60 k, v = line.split(':', 1)
61 settings[k.strip()] = v.strip()
62 finally:
63 settings_file.close()
maruel@chromium.org004fb712011-06-21 20:02:16 +000064 except IOError:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000065 return None
66 return settings.get(key, None)
67
68
maruel@chromium.org4dd9f722012-10-01 16:23:03 +000069def align_stdout(stdout):
70 """Returns the aligned output of multiple stdouts."""
71 output = ''
72 for item in stdout:
73 item = item.strip()
74 if not item:
75 continue
76 output += ''.join(' %s\n' % line for line in item.splitlines())
77 return output
78
79
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000080class PatchApplicationFailed(Exception):
81 """Patch failed to be applied."""
skobes2f3f1372016-10-25 08:08:27 -070082 def __init__(self, errors, verbose):
83 super(PatchApplicationFailed, self).__init__(errors, verbose)
84 self.errors = errors
85 self.verbose = verbose
maruel@chromium.org34f68552012-05-09 19:18:36 +000086
87 def __str__(self):
88 out = []
skobes2f3f1372016-10-25 08:08:27 -070089 for e in self.errors:
90 p, status = e
91 if p and p.filename:
92 out.append('Failed to apply patch for %s:' % p.filename)
93 if status:
94 out.append(status)
95 if p and self.verbose:
96 out.append('Patch: %s' % p.dump())
maruel@chromium.org34f68552012-05-09 19:18:36 +000097 return '\n'.join(out)
98
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000099
100class CheckoutBase(object):
101 # Set to None to have verbose output.
102 VOID = subprocess2.VOID
103
maruel@chromium.org6ed8b502011-06-12 01:05:35 +0000104 def __init__(self, root_dir, project_name, post_processors):
105 """
106 Args:
107 post_processor: list of lambda(checkout, patches) to call on each of the
108 modified files.
109 """
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000110 super(CheckoutBase, self).__init__()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000111 self.root_dir = root_dir
112 self.project_name = project_name
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000113 if self.project_name is None:
114 self.project_path = self.root_dir
115 else:
116 self.project_path = os.path.join(self.root_dir, self.project_name)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000117 # Only used for logging purposes.
118 self._last_seen_revision = None
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000119 self.post_processors = post_processors
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000120 assert self.root_dir
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000121 assert self.project_path
maruel@chromium.org0aca0f92012-10-01 16:39:45 +0000122 assert os.path.isabs(self.project_path)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000123
124 def get_settings(self, key):
125 return get_code_review_setting(self.project_path, key)
126
maruel@chromium.org51919772011-06-12 01:27:42 +0000127 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000128 """Checks out a clean copy of the tree and removes any local modification.
129
130 This function shouldn't throw unless the remote repository is inaccessible,
131 there is no free disk space or hard issues like that.
maruel@chromium.org51919772011-06-12 01:27:42 +0000132
133 Args:
134 revision: The revision it should sync to, SCM specific.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000135 """
136 raise NotImplementedError()
137
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000138 def apply_patch(self, patches, post_processors=None, verbose=False):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000139 """Applies a patch and returns the list of modified files.
140
141 This function should throw patch.UnsupportedPatchFormat or
142 PatchApplicationFailed when relevant.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000143
144 Args:
145 patches: patch.PatchSet object.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000146 """
147 raise NotImplementedError()
148
149 def commit(self, commit_message, user):
150 """Commits the patch upstream, while impersonating 'user'."""
151 raise NotImplementedError()
152
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000153 def revisions(self, rev1, rev2):
154 """Returns the count of revisions from rev1 to rev2, e.g. len(]rev1, rev2]).
155
156 If rev2 is None, it means 'HEAD'.
157
158 Returns None if there is no link between the two.
159 """
160 raise NotImplementedError()
161
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000162
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000163class GitCheckout(CheckoutBase):
164 """Manages a git checkout."""
165 def __init__(self, root_dir, project_name, remote_branch, git_url,
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000166 commit_user, post_processors=None):
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000167 super(GitCheckout, self).__init__(root_dir, project_name, post_processors)
168 self.git_url = git_url
169 self.commit_user = commit_user
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000170 self.remote_branch = remote_branch
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000171 # The working branch where patches will be applied. It will track the
172 # remote branch.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000173 self.working_branch = 'working_branch'
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000174 # There is no reason to not hardcode origin.
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000175 self.remote = 'origin'
176 # There is no reason to not hardcode master.
177 self.master_branch = 'master'
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000178
maruel@chromium.org51919772011-06-12 01:27:42 +0000179 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000180 """Resets the git repository in a clean state.
181
182 Checks it out if not present and deletes the working branch.
183 """
agable@chromium.org7dc11442014-03-12 22:37:32 +0000184 assert self.remote_branch
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000185 assert self.git_url
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000186
187 if not os.path.isdir(self.project_path):
188 # Clone the repo if the directory is not present.
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000189 logging.info(
190 'Checking out %s in %s', self.project_name, self.project_path)
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000191 self._check_call_git(
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000192 ['clone', self.git_url, '-b', self.remote_branch, self.project_path],
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000193 cwd=None, timeout=FETCH_TIMEOUT)
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000194 else:
195 # Throw away all uncommitted changes in the existing checkout.
196 self._check_call_git(['checkout', self.remote_branch])
197 self._check_call_git(
198 ['reset', '--hard', '--quiet',
199 '%s/%s' % (self.remote, self.remote_branch)])
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000200
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000201 if revision:
202 try:
203 # Look if the commit hash already exist. If so, we can skip a
204 # 'git fetch' call.
halton.huo@intel.com323ec372014-06-17 01:50:37 +0000205 revision = self._check_output_git(['rev-parse', revision]).rstrip()
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000206 except subprocess.CalledProcessError:
207 self._check_call_git(
208 ['fetch', self.remote, self.remote_branch, '--quiet'])
halton.huo@intel.com323ec372014-06-17 01:50:37 +0000209 revision = self._check_output_git(['rev-parse', revision]).rstrip()
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000210 self._check_call_git(['checkout', '--force', '--quiet', revision])
211 else:
212 branches, active = self._branches()
213 if active != self.master_branch:
214 self._check_call_git(
215 ['checkout', '--force', '--quiet', self.master_branch])
216 self._sync_remote_branch()
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000217
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000218 if self.working_branch in branches:
219 self._call_git(['branch', '-D', self.working_branch])
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000220 return self._get_head_commit_hash()
221
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000222 def _sync_remote_branch(self):
223 """Syncs the remote branch."""
224 # We do a 'git pull origin master:refs/remotes/origin/master' instead of
hinoka@google.comdabbea22014-04-21 23:58:11 +0000225 # 'git pull origin master' because from the manpage for git-pull:
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000226 # A parameter <ref> without a colon is equivalent to <ref>: when
227 # pulling/fetching, so it merges <ref> into the current branch without
228 # storing the remote branch anywhere locally.
229 remote_tracked_path = 'refs/remotes/%s/%s' % (
230 self.remote, self.remote_branch)
231 self._check_call_git(
232 ['pull', self.remote,
233 '%s:%s' % (self.remote_branch, remote_tracked_path),
234 '--quiet'])
235
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000236 def _get_head_commit_hash(self):
rmistry@google.com11145db2013-10-03 12:43:40 +0000237 """Gets the current revision (in unicode) from the local branch."""
238 return unicode(self._check_output_git(['rev-parse', 'HEAD']).strip())
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000239
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000240 def apply_patch(self, patches, post_processors=None, verbose=False):
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000241 """Applies a patch on 'working_branch' and switches to it.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000242
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000243 The changes remain staged on the current branch.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000244 """
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000245 post_processors = post_processors or self.post_processors or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000246 # It this throws, the checkout is corrupted. Maybe worth deleting it and
247 # trying again?
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000248 if self.remote_branch:
249 self._check_call_git(
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000250 ['checkout', '-b', self.working_branch, '-t', self.remote_branch,
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000251 '--quiet'])
252
skobes2f3f1372016-10-25 08:08:27 -0700253 errors = []
maruel@chromium.org5e975632011-09-29 18:07:06 +0000254 for index, p in enumerate(patches):
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000255 stdout = []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000256 try:
Edward Lesmesf8792072017-09-13 08:05:12 +0000257 filepath = os.path.join(self.project_path, p.filename)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000258 if p.is_delete:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000259 if (not os.path.exists(filepath) and
maruel@chromium.org5e975632011-09-29 18:07:06 +0000260 any(p1.source_filename == p.filename for p1 in patches[0:index])):
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000261 # The file was already deleted if a prior patch with file rename
262 # was already processed because 'git apply' did it for us.
maruel@chromium.org5e975632011-09-29 18:07:06 +0000263 pass
264 else:
Edward Lesmesf8792072017-09-13 08:05:12 +0000265 stdout.append(self._check_output_git(['rm', p.filename]))
phajdan.jr@chromium.orgd9eb69e2014-06-05 20:33:37 +0000266 assert(not os.path.exists(filepath))
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000267 stdout.append('Deleted.')
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000268 else:
Edward Lesmesf8792072017-09-13 08:05:12 +0000269 dirname = os.path.dirname(p.filename)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000270 full_dir = os.path.join(self.project_path, dirname)
271 if dirname and not os.path.isdir(full_dir):
272 os.makedirs(full_dir)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000273 stdout.append('Created missing directory %s.' % dirname)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000274 if p.is_binary:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000275 content = p.get()
276 with open(filepath, 'wb') as f:
277 f.write(content)
278 stdout.append('Added binary file %d bytes' % len(content))
Edward Lesmesf8792072017-09-13 08:05:12 +0000279 cmd = ['add', p.filename]
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000280 if verbose:
281 cmd.append('--verbose')
282 stdout.append(self._check_output_git(cmd))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000283 else:
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000284 # No need to do anything special with p.is_new or if not
285 # p.diff_hunks. git apply manages all that already.
primiano@chromium.org49dfcde2014-09-23 08:14:39 +0000286 cmd = ['apply', '--index', '-3', '-p%s' % p.patchlevel]
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000287 if verbose:
288 cmd.append('--verbose')
289 stdout.append(self._check_output_git(cmd, stdin=p.get(True)))
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000290 for post in post_processors:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000291 post(self, p)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000292 if verbose:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000293 print(p.filename)
294 print(align_stdout(stdout))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000295 except OSError, e:
skobes2f3f1372016-10-25 08:08:27 -0700296 errors.append((p, '%s%s' % (align_stdout(stdout), e)))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000297 except subprocess.CalledProcessError, e:
skobes2f3f1372016-10-25 08:08:27 -0700298 errors.append((p,
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000299 'While running %s;\n%s%s' % (
300 ' '.join(e.cmd),
301 align_stdout(stdout),
skobes2f3f1372016-10-25 08:08:27 -0700302 align_stdout([getattr(e, 'stdout', '')]))))
303 if errors:
304 raise PatchApplicationFailed(errors, verbose)
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000305 found_files = self._check_output_git(
Aaron Gablef4068aa2017-12-12 15:14:09 -0800306 ['-c', 'core.quotePath=false', 'diff', '--ignore-submodules',
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000307 '--name-only', '--staged']).splitlines(False)
hinoka@chromium.orgdc6a1d02014-05-10 04:42:48 +0000308 if sorted(patches.filenames) != sorted(found_files):
309 extra_files = sorted(set(found_files) - set(patches.filenames))
310 unpatched_files = sorted(set(patches.filenames) - set(found_files))
311 if extra_files:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000312 print('Found extra files: %r' % extra_files)
hinoka@chromium.orgdc6a1d02014-05-10 04:42:48 +0000313 if unpatched_files:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000314 print('Found unpatched files: %r' % unpatched_files)
hinoka@chromium.orgdc6a1d02014-05-10 04:42:48 +0000315
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000316
317 def commit(self, commit_message, user):
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000318 """Commits, updates the commit message and pushes."""
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000319 # TODO(hinoka): CQ no longer uses this, I think its deprecated.
320 # Delete this.
rmistry@google.combb050f62013-10-03 16:53:54 +0000321 assert self.commit_user
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000322 assert isinstance(commit_message, unicode)
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000323 current_branch = self._check_output_git(
324 ['rev-parse', '--abbrev-ref', 'HEAD']).strip()
325 assert current_branch == self.working_branch
hinoka@google.comdabbea22014-04-21 23:58:11 +0000326
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000327 commit_cmd = ['commit', '-m', commit_message]
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000328 if user and user != self.commit_user:
329 # We do not have the first or last name of the user, grab the username
330 # from the email and call it the original author's name.
331 # TODO(rmistry): Do not need the below if user is already in
332 # "Name <email>" format.
333 name = user.split('@')[0]
334 commit_cmd.extend(['--author', '%s <%s>' % (name, user)])
335 self._check_call_git(commit_cmd)
336
337 # Push to the remote repository.
338 self._check_call_git(
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000339 ['push', 'origin', '%s:%s' % (self.working_branch, self.remote_branch),
agable@chromium.org39262282014-03-19 21:07:38 +0000340 '--quiet'])
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000341 # Get the revision after the push.
342 revision = self._get_head_commit_hash()
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000343 # Switch back to the remote_branch and sync it.
344 self._check_call_git(['checkout', self.remote_branch])
345 self._sync_remote_branch()
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000346 # Delete the working branch since we are done with it.
347 self._check_call_git(['branch', '-D', self.working_branch])
348
349 return revision
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000350
351 def _check_call_git(self, args, **kwargs):
352 kwargs.setdefault('cwd', self.project_path)
353 kwargs.setdefault('stdout', self.VOID)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000354 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000355 return subprocess2.check_call_out(['git'] + args, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000356
357 def _call_git(self, args, **kwargs):
358 """Like check_call but doesn't throw on failure."""
359 kwargs.setdefault('cwd', self.project_path)
360 kwargs.setdefault('stdout', self.VOID)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000361 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000362 return subprocess2.call(['git'] + args, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000363
364 def _check_output_git(self, args, **kwargs):
365 kwargs.setdefault('cwd', self.project_path)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000366 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000367 return subprocess2.check_output(
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000368 ['git'] + args, stderr=subprocess2.STDOUT, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000369
370 def _branches(self):
371 """Returns the list of branches and the active one."""
372 out = self._check_output_git(['branch']).splitlines(False)
373 branches = [l[2:] for l in out]
374 active = None
375 for l in out:
376 if l.startswith('*'):
377 active = l[2:]
378 break
379 return branches, active
380
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000381 def revisions(self, rev1, rev2):
382 """Returns the number of actual commits between both hash."""
383 self._fetch_remote()
384
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000385 rev2 = rev2 or '%s/%s' % (self.remote, self.remote_branch)
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000386 # Revision range is ]rev1, rev2] and ordering matters.
387 try:
388 out = self._check_output_git(
389 ['log', '--format="%H"' , '%s..%s' % (rev1, rev2)])
390 except subprocess.CalledProcessError:
391 return None
392 return len(out.splitlines())
393
394 def _fetch_remote(self):
395 """Fetches the remote without rebasing."""
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000396 # git fetch is always verbose even with -q, so redirect its output.
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000397 self._check_output_git(['fetch', self.remote, self.remote_branch],
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000398 timeout=FETCH_TIMEOUT)
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000399
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000400
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000401class ReadOnlyCheckout(object):
402 """Converts a checkout into a read-only one."""
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000403 def __init__(self, checkout, post_processors=None):
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000404 super(ReadOnlyCheckout, self).__init__()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000405 self.checkout = checkout
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000406 self.post_processors = (post_processors or []) + (
407 self.checkout.post_processors or [])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000408
maruel@chromium.org51919772011-06-12 01:27:42 +0000409 def prepare(self, revision):
410 return self.checkout.prepare(revision)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000411
412 def get_settings(self, key):
413 return self.checkout.get_settings(key)
414
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000415 def apply_patch(self, patches, post_processors=None, verbose=False):
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000416 return self.checkout.apply_patch(
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000417 patches, post_processors or self.post_processors, verbose)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000418
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800419 def commit(self, message, user): # pylint: disable=no-self-use
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000420 logging.info('Would have committed for %s with message: %s' % (
421 user, message))
422 return 'FAKE'
423
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000424 def revisions(self, rev1, rev2):
425 return self.checkout.revisions(rev1, rev2)
426
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000427 @property
428 def project_name(self):
429 return self.checkout.project_name
430
431 @property
432 def project_path(self):
433 return self.checkout.project_path