blob: 30bb3d17ea49bd74c0a83af490bbc486d60e3f41 [file] [log] [blame]
maruel@chromium.org7d654672012-01-05 19:07:23 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00005"""SCM-specific utility classes."""
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00006
maruel@chromium.org3c55d982010-05-06 14:25:44 +00007import cStringIO
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +00008import glob
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00009import logging
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000010import os
11import re
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000012import sys
pkasting@chromium.org4755b582013-04-18 21:38:40 +000013import tempfile
maruel@chromium.orgfd876172010-04-30 14:01:05 +000014import time
maruel@chromium.orgade9c592011-04-07 15:59:11 +000015from xml.etree import ElementTree
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000016
17import gclient_utils
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000018import subprocess2
19
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000020
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000021def ValidateEmail(email):
maruel@chromium.org6e29d572010-06-04 17:32:20 +000022 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
23 is not None)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000024
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000025
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000026def GetCasedPath(path):
27 """Elcheapos way to get the real path case on Windows."""
28 if sys.platform.startswith('win') and os.path.exists(path):
29 # Reconstruct the path.
30 path = os.path.abspath(path)
31 paths = path.split('\\')
32 for i in range(len(paths)):
33 if i == 0:
34 # Skip drive letter.
35 continue
36 subpath = '\\'.join(paths[:i+1])
37 prev = len('\\'.join(paths[:i]))
38 # glob.glob will return the cased path for the last item only. This is why
39 # we are calling it in a loop. Extract the data we want and put it back
40 # into the list.
41 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)]
42 path = '\\'.join(paths)
43 return path
44
45
maruel@chromium.org3c55d982010-05-06 14:25:44 +000046def GenFakeDiff(filename):
47 """Generates a fake diff from a file."""
48 file_content = gclient_utils.FileRead(filename, 'rb').splitlines(True)
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +000049 filename = filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +000050 nb_lines = len(file_content)
51 # We need to use / since patch on unix will fail otherwise.
52 data = cStringIO.StringIO()
53 data.write("Index: %s\n" % filename)
54 data.write('=' * 67 + '\n')
55 # Note: Should we use /dev/null instead?
56 data.write("--- %s\n" % filename)
57 data.write("+++ %s\n" % filename)
58 data.write("@@ -0,0 +1,%d @@\n" % nb_lines)
59 # Prepend '+' to every lines.
60 for line in file_content:
61 data.write('+')
62 data.write(line)
63 result = data.getvalue()
64 data.close()
65 return result
66
67
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000068def determine_scm(root):
69 """Similar to upload.py's version but much simpler.
70
71 Returns 'svn', 'git' or None.
72 """
73 if os.path.isdir(os.path.join(root, '.svn')):
74 return 'svn'
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000075 elif os.path.isdir(os.path.join(root, '.git')):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000076 return 'git'
77 else:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000078 try:
maruel@chromium.org91def9b2011-09-14 16:28:07 +000079 subprocess2.check_call(
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000080 ['git', 'rev-parse', '--show-cdup'],
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000081 stdout=subprocess2.VOID,
maruel@chromium.org87e6d332011-09-09 19:01:28 +000082 stderr=subprocess2.VOID,
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000083 cwd=root)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000084 return 'git'
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000085 except (OSError, subprocess2.CalledProcessError):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000086 return None
87
88
maruel@chromium.org36ac2392011-10-12 16:36:11 +000089def only_int(val):
90 if val.isdigit():
91 return int(val)
92 else:
93 return 0
94
95
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000096class GIT(object):
maruel@chromium.org36ac2392011-10-12 16:36:11 +000097 current_version = None
98
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000099 @staticmethod
ilevy@chromium.org4380c802013-07-12 23:38:41 +0000100 def Capture(args, cwd, strip_out=True, **kwargs):
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000101 env = os.environ.copy()
102 # 'cat' is a magical git string that disables pagers on all platforms.
103 env['GIT_PAGER'] = 'cat'
ilevy@chromium.org4380c802013-07-12 23:38:41 +0000104 output = subprocess2.check_output(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000105 ['git'] + args,
ilevy@chromium.org4380c802013-07-12 23:38:41 +0000106 cwd=cwd, stderr=subprocess2.PIPE, env=env, **kwargs)
107 return output.strip() if strip_out else output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000108
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000109 @staticmethod
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000110 def CaptureStatus(files, cwd, upstream_branch):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000111 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000112
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000113 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000114
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000115 Returns an array of (status, file) tuples."""
msb@chromium.org786fb682010-06-02 15:16:23 +0000116 if upstream_branch is None:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000117 upstream_branch = GIT.GetUpstreamBranch(cwd)
msb@chromium.org786fb682010-06-02 15:16:23 +0000118 if upstream_branch is None:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000119 raise gclient_utils.Error('Cannot determine upstream branch')
mcgrathr@chromium.org9249f642013-06-03 21:36:18 +0000120 command = ['diff', '--name-status', '--no-renames',
121 '-r', '%s...' % upstream_branch]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000122 if not files:
123 pass
124 elif isinstance(files, basestring):
125 command.append(files)
126 else:
127 command.extend(files)
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000128 status = GIT.Capture(command, cwd)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000129 results = []
130 if status:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000131 for statusline in status.splitlines():
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +0000132 # 3-way merges can cause the status can be 'MMM' instead of 'M'. This
133 # can happen when the user has 2 local branches and he diffs between
134 # these 2 branches instead diffing to upstream.
135 m = re.match('^(\w)+\t(.+)$', statusline)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000136 if not m:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000137 raise gclient_utils.Error(
138 'status currently unsupported: %s' % statusline)
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +0000139 # Only grab the first letter.
140 results.append(('%s ' % m.group(1)[0], m.group(2)))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000141 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000142
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000143 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000144 def GetEmail(cwd):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000145 """Retrieves the user email address if known."""
146 # We could want to look at the svn cred when it has a svn remote but it
147 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000148 try:
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000149 return GIT.Capture(['config', 'user.email'], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000150 except subprocess2.CalledProcessError:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000151 return ''
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000152
153 @staticmethod
154 def ShortBranchName(branch):
155 """Converts a name like 'refs/heads/foo' to just 'foo'."""
156 return branch.replace('refs/heads/', '')
157
158 @staticmethod
159 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000160 """Returns the full branch reference, e.g. 'refs/heads/master'."""
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000161 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000162
163 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000164 def GetBranch(cwd):
165 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000166 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000167
168 @staticmethod
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000169 def IsGitSvn(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000170 """Returns true if this repo looks like it's using git-svn."""
171 # If you have any "svn-remote.*" config keys, we think you're using svn.
172 try:
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000173 GIT.Capture(['config', '--local', '--get-regexp', r'^svn-remote\.'],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000174 cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000175 return True
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000176 except subprocess2.CalledProcessError:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000177 return False
178
179 @staticmethod
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000180 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
181 """Return the corresponding git ref if |base_url| together with |glob_spec|
182 matches the full |url|.
183
184 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
185 """
186 fetch_suburl, as_ref = glob_spec.split(':')
187 if allow_wildcards:
188 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
189 if glob_match:
190 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
191 # "branches/{472,597,648}/src:refs/remotes/svn/*".
192 branch_re = re.escape(base_url)
193 if glob_match.group(1):
194 branch_re += '/' + re.escape(glob_match.group(1))
195 wildcard = glob_match.group(2)
196 if wildcard == '*':
197 branch_re += '([^/]*)'
198 else:
199 # Escape and replace surrounding braces with parentheses and commas
200 # with pipe symbols.
201 wildcard = re.escape(wildcard)
202 wildcard = re.sub('^\\\\{', '(', wildcard)
203 wildcard = re.sub('\\\\,', '|', wildcard)
204 wildcard = re.sub('\\\\}$', ')', wildcard)
205 branch_re += wildcard
206 if glob_match.group(3):
207 branch_re += re.escape(glob_match.group(3))
208 match = re.match(branch_re, url)
209 if match:
210 return re.sub('\*$', match.group(1), as_ref)
211
212 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
213 if fetch_suburl:
214 full_url = base_url + '/' + fetch_suburl
215 else:
216 full_url = base_url
217 if full_url == url:
218 return as_ref
219 return None
220
221 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000222 def GetSVNBranch(cwd):
223 """Returns the svn branch name if found."""
224 # Try to figure out which remote branch we're based on.
225 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000226 # 1) iterate through our branch history and find the svn URL.
227 # 2) find the svn-remote that fetches from the URL.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000228
229 # regexp matching the git-svn line that contains the URL.
230 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
231
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000232 # We don't want to go through all of history, so read a line from the
233 # pipe at a time.
234 # The -100 is an arbitrary limit so we don't search forever.
235 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.orgf94e3f12011-12-13 21:03:46 +0000236 proc = subprocess2.Popen(cmd, cwd=cwd, stdout=subprocess2.PIPE)
maruel@chromium.orge8c28622011-04-05 14:41:44 +0000237 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000238 for line in proc.stdout:
239 match = git_svn_re.match(line)
240 if match:
241 url = match.group(1)
242 proc.stdout.close() # Cut pipe.
243 break
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000244
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000245 if url:
246 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000247 remotes = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000248 ['config', '--local', '--get-regexp', r'^svn-remote\..*\.url'],
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000249 cwd=cwd).splitlines()
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000250 for remote in remotes:
251 match = svn_remote_re.match(remote)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000252 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000253 remote = match.group(1)
254 base_url = match.group(2)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000255 try:
256 fetch_spec = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000257 ['config', '--local', 'svn-remote.%s.fetch' % remote],
258 cwd=cwd)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000259 branch = GIT.MatchSvnGlob(url, base_url, fetch_spec, False)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000260 except subprocess2.CalledProcessError:
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000261 branch = None
262 if branch:
263 return branch
264 try:
265 branch_spec = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000266 ['config', '--local', 'svn-remote.%s.branches' % remote],
267 cwd=cwd)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000268 branch = GIT.MatchSvnGlob(url, base_url, branch_spec, True)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000269 except subprocess2.CalledProcessError:
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000270 branch = None
271 if branch:
272 return branch
273 try:
274 tag_spec = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000275 ['config', '--local', 'svn-remote.%s.tags' % remote],
276 cwd=cwd)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000277 branch = GIT.MatchSvnGlob(url, base_url, tag_spec, True)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000278 except subprocess2.CalledProcessError:
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000279 branch = None
280 if branch:
281 return branch
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000282
283 @staticmethod
284 def FetchUpstreamTuple(cwd):
285 """Returns a tuple containg remote and remote ref,
286 e.g. 'origin', 'refs/heads/master'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000287 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000288 """
289 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000290 branch = GIT.GetBranch(cwd)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000291 try:
292 upstream_branch = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000293 ['config', '--local', 'branch.%s.merge' % branch], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000294 except subprocess2.CalledProcessError:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000295 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000296 if upstream_branch:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000297 try:
298 remote = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000299 ['config', '--local', 'branch.%s.remote' % branch], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000300 except subprocess2.CalledProcessError:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000301 pass
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000302 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000303 try:
304 upstream_branch = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000305 ['config', '--local', 'rietveld.upstream-branch'], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000306 except subprocess2.CalledProcessError:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000307 upstream_branch = None
308 if upstream_branch:
309 try:
310 remote = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000311 ['config', '--local', 'rietveld.upstream-remote'], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000312 except subprocess2.CalledProcessError:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000313 pass
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000314 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000315 # Fall back on trying a git-svn upstream branch.
316 if GIT.IsGitSvn(cwd):
317 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000318 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000319 # Else, try to guess the origin remote.
320 remote_branches = GIT.Capture(['branch', '-r'], cwd=cwd).split()
321 if 'origin/master' in remote_branches:
322 # Fall back on origin/master if it exits.
323 remote = 'origin'
324 upstream_branch = 'refs/heads/master'
325 elif 'origin/trunk' in remote_branches:
326 # Fall back on origin/trunk if it exists. Generally a shared
327 # git-svn clone
328 remote = 'origin'
329 upstream_branch = 'refs/heads/trunk'
330 else:
331 # Give up.
332 remote = None
333 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000334 return remote, upstream_branch
335
336 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000337 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000338 """Gets the current branch's upstream branch."""
339 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000340 if remote != '.' and upstream_branch:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000341 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
342 return upstream_branch
343
344 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000345 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
346 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000347 """Diffs against the upstream branch or optionally another branch.
348
349 full_move means that move or copy operations should completely recreate the
350 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000351 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000352 branch = GIT.GetUpstreamBranch(cwd)
scottbyer@chromium.org33167332012-02-23 21:15:30 +0000353 command = ['diff', '-p', '--no-color', '--no-prefix', '--no-ext-diff',
evan@chromium.org400f3e72010-05-19 14:23:36 +0000354 branch + "..." + branch_head]
mcgrathr@chromium.org9249f642013-06-03 21:36:18 +0000355 if full_move:
356 command.append('--no-renames')
357 else:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000358 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000359 # TODO(maruel): --binary support.
360 if files:
361 command.append('--')
362 command.extend(files)
ilevy@chromium.org4380c802013-07-12 23:38:41 +0000363 diff = GIT.Capture(command, cwd=cwd, strip_out=False).splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000364 for i in range(len(diff)):
365 # In the case of added files, replace /dev/null with the path to the
366 # file being added.
367 if diff[i].startswith('--- /dev/null'):
368 diff[i] = '--- %s' % diff[i+1][4:]
369 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000370
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000371 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000372 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
373 """Returns the list of modified files between two branches."""
374 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000375 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000376 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000377 return GIT.Capture(command, cwd=cwd).splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000378
379 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000380 def GetPatchName(cwd):
381 """Constructs a name for this patch."""
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000382 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd)
maruel@chromium.org862ff8e2010-08-06 15:29:16 +0000383 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000384
385 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000386 def GetCheckoutRoot(cwd):
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000387 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000388 """
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000389 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000390 return os.path.abspath(os.path.join(cwd, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000391
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000392 @staticmethod
393 def GetGitSvnHeadRev(cwd):
394 """Gets the most recently pulled git-svn revision."""
395 try:
396 output = GIT.Capture(['svn', 'info'], cwd=cwd)
397 match = re.search(r'^Revision: ([0-9]+)$', output, re.MULTILINE)
398 return int(match.group(1)) if match else None
399 except (subprocess2.CalledProcessError, ValueError):
400 return None
401
402 @staticmethod
wittman@chromium.org492a3682012-08-10 00:28:28 +0000403 def ParseGitSvnSha1(output):
404 """Parses git-svn output for the first sha1."""
405 match = re.search(r'[0-9a-fA-F]{40}', output)
406 return match.group(0) if match else None
407
408 @staticmethod
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000409 def GetSha1ForSvnRev(cwd, rev):
410 """Returns a corresponding git sha1 for a SVN revision."""
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000411 if not GIT.IsGitSvn(cwd=cwd):
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000412 return None
413 try:
szager@chromium.orgc51def32012-10-15 18:50:37 +0000414 output = GIT.Capture(['svn', 'find-rev', 'r' + str(rev)], cwd=cwd)
415 return GIT.ParseGitSvnSha1(output)
416 except subprocess2.CalledProcessError:
417 return None
418
419 @staticmethod
420 def GetBlessedSha1ForSvnRev(cwd, rev):
421 """Returns a git commit hash from the master branch history that has
422 accurate .DEPS.git and git submodules. To understand why this is more
423 complicated than a simple call to `git svn find-rev`, refer to:
424
425 http://www.chromium.org/developers/how-tos/git-repo
426 """
427 git_svn_rev = GIT.GetSha1ForSvnRev(cwd, rev)
428 if not git_svn_rev:
429 return None
430 try:
szager@google.com312a6a42012-10-11 21:19:42 +0000431 output = GIT.Capture(
432 ['rev-list', '--ancestry-path', '--reverse',
433 '--grep', 'SVN changes up to revision [0-9]*',
434 '%s..refs/remotes/origin/master' % git_svn_rev], cwd=cwd)
435 if not output:
436 return None
437 sha1 = output.splitlines()[0]
438 if not sha1:
439 return None
440 output = GIT.Capture(['rev-list', '-n', '1', '%s^1' % sha1], cwd=cwd)
441 if git_svn_rev != output.rstrip():
442 raise gclient_utils.Error(sha1)
443 return sha1
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000444 except subprocess2.CalledProcessError:
445 return None
446
447 @staticmethod
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000448 def IsValidRevision(cwd, rev, sha_only=False):
449 """Verifies the revision is a proper git revision.
450
451 sha_only: Fail unless rev is a sha hash.
452 """
maruel@chromium.org81473862012-06-27 17:30:56 +0000453 # 'git rev-parse foo' where foo is *any* 40 character hex string will return
454 # the string and return code 0. So strip one character to force 'git
455 # rev-parse' to do a hash table look-up and returns 128 if the hash is not
456 # present.
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000457 lookup_rev = rev
maruel@chromium.org81473862012-06-27 17:30:56 +0000458 if re.match(r'^[0-9a-fA-F]{40}$', rev):
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000459 lookup_rev = rev[:-1]
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000460 try:
ilevy@chromium.org224ba242013-07-08 22:02:31 +0000461 sha = GIT.Capture(['rev-parse', lookup_rev], cwd=cwd).lower()
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000462 if lookup_rev != rev:
463 # Make sure we get the original 40 chars back.
ilevy@chromium.org224ba242013-07-08 22:02:31 +0000464 return rev.lower() == sha
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000465 if sha_only:
ilevy@chromium.org224ba242013-07-08 22:02:31 +0000466 return sha.startswith(rev.lower())
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000467 return True
468 except subprocess2.CalledProcessError:
469 return False
470
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000471 @classmethod
472 def AssertVersion(cls, min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000473 """Asserts git's version is at least min_version."""
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000474 if cls.current_version is None:
bashi@chromium.orgfcffd482012-02-24 01:47:00 +0000475 current_version = cls.Capture(['--version'], '.')
476 matched = re.search(r'version ([0-9\.]+)', current_version)
477 cls.current_version = matched.group(1)
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000478 current_version_list = map(only_int, cls.current_version.split('.'))
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000479 for min_ver in map(int, min_version.split('.')):
480 ver = current_version_list.pop(0)
481 if ver < min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000482 return (False, cls.current_version)
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000483 elif ver > min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000484 return (True, cls.current_version)
485 return (True, cls.current_version)
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000486
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000487
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000488class SVN(object):
tony@chromium.org57564662010-04-14 02:35:12 +0000489 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000490
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000491 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000492 def Capture(args, cwd, **kwargs):
maruel@chromium.org54019f32010-09-09 13:50:11 +0000493 """Always redirect stderr.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000494
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000495 Throws an exception if non-0 is returned.
496 """
maruel@chromium.org904af082011-09-08 22:06:09 +0000497 return subprocess2.check_output(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000498 ['svn'] + args, stderr=subprocess2.PIPE, cwd=cwd, **kwargs)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000499
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000500 @staticmethod
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000501 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000502 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000503
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000504 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000505
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000506 svn's stdout is parsed to collect a list of files checked out or updated.
507 These files are appended to file_list. svn's stdout is also printed to
508 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000509
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000510 Args:
maruel@chromium.org03807072010-08-16 17:18:44 +0000511 verbose: If True, uses verbose output
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000512 args: A sequence of command line parameters to be passed to svn.
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000513 cwd: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000514
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000515 Raises:
516 Error: An error occurred while running the svn command.
517 """
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000518 stdout = stdout or sys.stdout
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000519 if file_list is None:
520 # Even if our caller doesn't care about file_list, we use it internally.
521 file_list = []
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000522
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000523 # svn update and svn checkout use the same pattern: the first three columns
524 # are for file status, property status, and lock status. This is followed
525 # by two spaces, and then the path to the file.
526 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000527
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000528 # The first three columns of svn status are the same as for svn update and
529 # svn checkout. The next three columns indicate addition-with-history,
530 # switch, and remote lock status. This is followed by one space, and then
531 # the path to the file.
532 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000533
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000534 # args[0] must be a supported command. This will blow up if it's something
535 # else, which is good. Note that the patterns are only effective when
536 # these commands are used in their ordinary forms, the patterns are invalid
537 # for "svn status --show-updates", for example.
538 pattern = {
539 'checkout': update_pattern,
540 'status': status_pattern,
541 'update': update_pattern,
542 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000543 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000544 # Place an upper limit.
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000545 backoff_time = 5
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000546 retries = 0
maruel@chromium.org03507062010-10-26 00:58:27 +0000547 while True:
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000548 retries += 1
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000549 previous_list_len = len(file_list)
550 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000551
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000552 def CaptureMatchingLines(line):
553 match = compiled_pattern.search(line)
554 if match:
555 file_list.append(match.group(1))
556 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000557 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000558
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000559 try:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000560 gclient_utils.CheckCallAndFilterAndHeader(
561 ['svn'] + args,
562 cwd=cwd,
563 always=verbose,
564 filter_fn=CaptureMatchingLines,
565 stdout=stdout)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000566 except subprocess2.CalledProcessError:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000567 def IsKnownFailure():
568 for x in failure:
569 if (x.startswith('svn: OPTIONS of') or
570 x.startswith('svn: PROPFIND of') or
571 x.startswith('svn: REPORT of') or
maruel@chromium.orgf61fc932010-08-19 13:05:24 +0000572 x.startswith('svn: Unknown hostname') or
maruel@chromium.org7d8b97d2011-10-11 23:32:30 +0000573 x.startswith('svn: Server sent unexpected return value') or
574 x.startswith('svn: Can\'t connect to host')):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000575 return True
576 return False
577
maruel@chromium.org953586a2010-06-15 14:22:24 +0000578 # Subversion client is really misbehaving with Google Code.
579 if args[0] == 'checkout':
580 # Ensure at least one file was checked out, otherwise *delete* the
581 # directory.
582 if len(file_list) == previous_list_len:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000583 if not IsKnownFailure():
maruel@chromium.org953586a2010-06-15 14:22:24 +0000584 # No known svn error was found, bail out.
585 raise
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000586 # No file were checked out, so make sure the directory is
587 # deleted in case it's messed up and try again.
588 # Warning: It's bad, it assumes args[2] is the directory
589 # argument.
590 if os.path.isdir(args[2]):
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000591 gclient_utils.rmtree(args[2])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000592 else:
593 # Progress was made, convert to update since an aborted checkout
594 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000595 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000596 else:
597 # It was an update or export.
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000598 # We enforce that some progress has been made or a known failure.
599 if len(file_list) == previous_list_len and not IsKnownFailure():
600 # No known svn error was found and no progress, bail out.
601 raise
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000602 if retries == 10:
maruel@chromium.org03507062010-10-26 00:58:27 +0000603 raise
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000604 print "Sleeping %.1f seconds and retrying...." % backoff_time
605 time.sleep(backoff_time)
606 backoff_time *= 1.3
maruel@chromium.org953586a2010-06-15 14:22:24 +0000607 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000608 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000609
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000610 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000611 def CaptureRemoteInfo(url):
612 """Returns a dictionary from the svn info output for the given url.
613
614 Throws an exception if svn info fails.
615 """
616 assert isinstance(url, str)
617 return SVN._CaptureInfo([url], None)
618
619 @staticmethod
620 def CaptureLocalInfo(files, cwd):
621 """Returns a dictionary from the svn info output for the given files.
622
623 Throws an exception if svn info fails.
624 """
625 assert isinstance(files, (list, tuple))
626 return SVN._CaptureInfo(files, cwd)
627
628 @staticmethod
629 def _CaptureInfo(files, cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000630 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000631
maruel@chromium.org54019f32010-09-09 13:50:11 +0000632 Throws an exception if svn info fails."""
maruel@chromium.orgd25fb8f2011-04-07 13:40:15 +0000633 result = {}
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000634 info = ElementTree.XML(SVN.Capture(['info', '--xml'] + files, cwd))
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000635 if info is None:
636 return result
637 entry = info.find('entry')
maruel@chromium.org6f323bb2011-04-26 15:42:53 +0000638 if entry is None:
639 return result
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000640
641 # Use .text when the item is not optional.
642 result['Path'] = entry.attrib['path']
maruel@chromium.org7d654672012-01-05 19:07:23 +0000643 rev = entry.attrib['revision']
644 try:
645 result['Revision'] = int(rev)
646 except ValueError:
647 result['Revision'] = None
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000648 result['Node Kind'] = entry.attrib['kind']
649 # Differs across versions.
650 if result['Node Kind'] == 'dir':
651 result['Node Kind'] = 'directory'
652 result['URL'] = entry.find('url').text
653 repository = entry.find('repository')
654 result['Repository Root'] = repository.find('root').text
655 result['UUID'] = repository.find('uuid')
656 wc_info = entry.find('wc-info')
657 if wc_info is not None:
658 result['Schedule'] = wc_info.find('schedule').text
659 result['Copied From URL'] = wc_info.find('copy-from-url')
660 result['Copied From Rev'] = wc_info.find('copy-from-rev')
661 else:
662 result['Schedule'] = None
663 result['Copied From URL'] = None
664 result['Copied From Rev'] = None
665 for key in result.keys():
666 if isinstance(result[key], unicode):
667 # Unicode results interferes with the higher layers matching up things
668 # in the deps dictionary.
669 result[key] = result[key].encode()
670 # Automatic conversion of optional parameters.
671 result[key] = getattr(result[key], 'text', result[key])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000672 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000673
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000674 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000675 def CaptureRevision(cwd):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000676 """Get the base revision of a SVN repository.
677
678 Returns:
679 Int base revision
680 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000681 return SVN.CaptureLocalInfo([], cwd).get('Revision')
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000682
683 @staticmethod
maruel@chromium.orgea15cb72012-05-04 14:16:31 +0000684 def CaptureStatus(files, cwd, no_ignore=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000685 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000686
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000687 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000688
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000689 Returns an array of (status, file) tuples."""
690 command = ["status", "--xml"]
maruel@chromium.orgea15cb72012-05-04 14:16:31 +0000691 if no_ignore:
692 command.append('--no-ignore')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000693 if not files:
694 pass
695 elif isinstance(files, basestring):
696 command.append(files)
697 else:
698 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000699
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000700 status_letter = {
701 None: ' ',
702 '': ' ',
703 'added': 'A',
704 'conflicted': 'C',
705 'deleted': 'D',
706 'external': 'X',
707 'ignored': 'I',
708 'incomplete': '!',
709 'merged': 'G',
710 'missing': '!',
711 'modified': 'M',
712 'none': ' ',
713 'normal': ' ',
714 'obstructed': '~',
715 'replaced': 'R',
716 'unversioned': '?',
717 }
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000718 dom = ElementTree.XML(SVN.Capture(command, cwd))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000719 results = []
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000720 if dom is None:
721 return results
722 # /status/target/entry/(wc-status|commit|author|date)
723 for target in dom.findall('target'):
724 for entry in target.findall('entry'):
725 file_path = entry.attrib['path']
726 wc_status = entry.find('wc-status')
727 # Emulate svn 1.5 status ouput...
728 statuses = [' '] * 7
729 # Col 0
730 xml_item_status = wc_status.attrib['item']
731 if xml_item_status in status_letter:
732 statuses[0] = status_letter[xml_item_status]
733 else:
734 raise gclient_utils.Error(
735 'Unknown item status "%s"; please implement me!' %
736 xml_item_status)
737 # Col 1
738 xml_props_status = wc_status.attrib['props']
739 if xml_props_status == 'modified':
740 statuses[1] = 'M'
741 elif xml_props_status == 'conflicted':
742 statuses[1] = 'C'
743 elif (not xml_props_status or xml_props_status == 'none' or
744 xml_props_status == 'normal'):
745 pass
746 else:
747 raise gclient_utils.Error(
748 'Unknown props status "%s"; please implement me!' %
749 xml_props_status)
750 # Col 2
751 if wc_status.attrib.get('wc-locked') == 'true':
752 statuses[2] = 'L'
753 # Col 3
754 if wc_status.attrib.get('copied') == 'true':
755 statuses[3] = '+'
756 # Col 4
757 if wc_status.attrib.get('switched') == 'true':
758 statuses[4] = 'S'
759 # TODO(maruel): Col 5 and 6
760 item = (''.join(statuses), file_path)
761 results.append(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000762 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000763
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000764 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000765 def IsMoved(filename, cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000766 """Determine if a file has been added through svn mv"""
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000767 assert isinstance(filename, basestring)
768 return SVN.IsMovedInfo(SVN.CaptureLocalInfo([filename], cwd))
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000769
770 @staticmethod
771 def IsMovedInfo(info):
772 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000773 return (info.get('Copied From URL') and
774 info.get('Copied From Rev') and
775 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000776
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000777 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000778 def GetFileProperty(filename, property_name, cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000779 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000780
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000781 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000782 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000783 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000784
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000785 Returns:
786 The value of the property, which will be the empty string if the property
787 is not set on the file. If the file is not under version control, the
788 empty string is also returned.
789 """
maruel@chromium.org54019f32010-09-09 13:50:11 +0000790 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000791 return SVN.Capture(['propget', property_name, filename], cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000792 except subprocess2.CalledProcessError:
maruel@chromium.org54019f32010-09-09 13:50:11 +0000793 return ''
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000794
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000795 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000796 def GenerateDiff(filenames, cwd, full_move, revision):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000797 """Returns a string containing the diff for the given file list.
798
799 The files in the list should either be absolute paths or relative to the
800 given root. If no root directory is provided, the repository root will be
801 used.
802 The diff will always use relative paths.
803 """
maruel@chromium.org00fdcb32011-02-24 01:41:02 +0000804 assert isinstance(filenames, (list, tuple))
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000805 # If the user specified a custom diff command in their svn config file,
806 # then it'll be used when we do svn diff, which we don't want to happen
807 # since we want the unified diff.
808 if SVN.AssertVersion("1.7")[0]:
809 # On svn >= 1.7, the "--internal-diff" flag will solve this.
810 return SVN._GenerateDiffInternal(filenames, cwd, full_move, revision,
811 ["diff", "--internal-diff"],
812 ["diff", "--internal-diff"])
813 else:
814 # On svn < 1.7, the "--internal-diff" flag doesn't exist. Using
815 # --diff-cmd=diff doesn't always work, since e.g. Windows cmd users may
816 # not have a "diff" executable in their path at all. So we use an empty
817 # temporary directory as the config directory, which bypasses any user
818 # settings for the diff-cmd. However, we don't pass this for the
819 # remote_safe_diff_command parameter, since when a new config-dir is
820 # specified for an svn diff against a remote URL, it triggers
821 # authentication prompts. In this case there isn't really a good
822 # alternative to svn 1.7's --internal-diff flag.
823 bogus_dir = tempfile.mkdtemp()
824 try:
825 return SVN._GenerateDiffInternal(filenames, cwd, full_move, revision,
826 ["diff", "--config-dir", bogus_dir],
827 ["diff"])
828 finally:
829 gclient_utils.rmtree(bogus_dir)
830
831 @staticmethod
832 def _GenerateDiffInternal(filenames, cwd, full_move, revision, diff_command,
833 remote_safe_diff_command):
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000834 root = os.path.normcase(os.path.join(cwd, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000835 def RelativePath(path, root):
836 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000837 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000838 return path[len(root):]
839 return path
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000840 # Cleanup filenames
841 filenames = [RelativePath(f, root) for f in filenames]
842 # Get information about the modified items (files and directories)
843 data = dict((f, SVN.CaptureLocalInfo([f], root)) for f in filenames)
844 diffs = []
845 if full_move:
846 # Eliminate modified files inside moved/copied directory.
847 for (filename, info) in data.iteritems():
848 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
849 # Remove files inside the directory.
850 filenames = [f for f in filenames
851 if not f.startswith(filename + os.path.sep)]
852 for filename in data.keys():
853 if not filename in filenames:
854 # Remove filtered out items.
855 del data[filename]
856 else:
857 metaheaders = []
858 for (filename, info) in data.iteritems():
859 if SVN.IsMovedInfo(info):
860 # for now, the most common case is a head copy,
861 # so let's just encode that as a straight up cp.
862 srcurl = info.get('Copied From URL')
863 file_root = info.get('Repository Root')
864 rev = int(info.get('Copied From Rev'))
865 assert srcurl.startswith(file_root)
866 src = srcurl[len(file_root)+1:]
867 try:
868 srcinfo = SVN.CaptureRemoteInfo(srcurl)
869 except subprocess2.CalledProcessError, e:
870 if not 'Not a valid URL' in e.stderr:
871 raise
872 # Assume the file was deleted. No idea how to figure out at which
873 # revision the file was deleted.
874 srcinfo = {'Revision': rev}
875 if (srcinfo.get('Revision') != rev and
876 SVN.Capture(remote_safe_diff_command + ['-r', '%d:head' % rev,
877 srcurl], cwd)):
878 metaheaders.append("#$ svn cp -r %d %s %s "
879 "### WARNING: note non-trunk copy\n" %
880 (rev, src, filename))
881 else:
882 metaheaders.append("#$ cp %s %s\n" % (src,
883 filename))
884 if metaheaders:
885 diffs.append("### BEGIN SVN COPY METADATA\n")
886 diffs.extend(metaheaders)
887 diffs.append("### END SVN COPY METADATA\n")
888 # Now ready to do the actual diff.
889 for filename in sorted(data):
890 diffs.append(SVN._DiffItemInternal(
891 filename, cwd, data[filename], diff_command, full_move, revision))
892 # Use StringIO since it can be messy when diffing a directory move with
893 # full_move=True.
894 buf = cStringIO.StringIO()
895 for d in filter(None, diffs):
896 buf.write(d)
897 result = buf.getvalue()
898 buf.close()
899 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000900
901 @staticmethod
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000902 def _DiffItemInternal(filename, cwd, info, diff_command, full_move, revision):
pkasting@chromium.org917ea7f2013-04-19 20:10:03 +0000903 """Grabs the diff data."""
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000904 command = diff_command + [filename]
pkasting@chromium.org917ea7f2013-04-19 20:10:03 +0000905 if revision:
906 command.extend(['--revision', revision])
907 data = None
908 if SVN.IsMovedInfo(info):
909 if full_move:
910 if info.get("Node Kind") == "directory":
911 # Things become tricky here. It's a directory copy/move. We need to
912 # diff all the files inside it.
913 # This will put a lot of pressure on the heap. This is why StringIO
914 # is used and converted back into a string at the end. The reason to
915 # return a string instead of a StringIO is that StringIO.write()
916 # doesn't accept a StringIO object. *sigh*.
917 for (dirpath, dirnames, filenames) in os.walk(filename):
918 # Cleanup all files starting with a '.'.
919 for d in dirnames:
920 if d.startswith('.'):
921 dirnames.remove(d)
922 for f in filenames:
923 if f.startswith('.'):
924 filenames.remove(f)
925 for f in filenames:
926 if data is None:
927 data = cStringIO.StringIO()
928 data.write(GenFakeDiff(os.path.join(dirpath, f)))
929 if data:
930 tmp = data.getvalue()
931 data.close()
932 data = tmp
933 else:
934 data = GenFakeDiff(filename)
935 else:
936 if info.get("Node Kind") != "directory":
937 # svn diff on a mv/cp'd file outputs nothing if there was no change.
938 data = SVN.Capture(command, cwd)
939 if not data:
940 # We put in an empty Index entry so upload.py knows about them.
941 data = "Index: %s\n" % filename.replace(os.sep, '/')
942 # Otherwise silently ignore directories.
943 else:
944 if info.get("Node Kind") != "directory":
945 # Normal simple case.
946 try:
947 data = SVN.Capture(command, cwd)
948 except subprocess2.CalledProcessError:
949 if revision:
950 data = GenFakeDiff(filename)
951 else:
952 raise
953 # Otherwise silently ignore directories.
954 return data
955
956 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000957 def GetEmail(cwd):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000958 """Retrieves the svn account which we assume is an email address."""
maruel@chromium.org54019f32010-09-09 13:50:11 +0000959 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000960 infos = SVN.CaptureLocalInfo([], cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000961 except subprocess2.CalledProcessError:
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000962 return None
963
964 # Should check for uuid but it is incorrectly saved for https creds.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000965 root = infos['Repository Root']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000966 realm = root.rsplit('/', 1)[0]
maruel@chromium.org54019f32010-09-09 13:50:11 +0000967 uuid = infos['UUID']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000968 if root.startswith('https') or not uuid:
969 regexp = re.compile(r'<%s:\d+>.*' % realm)
970 else:
971 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
972 if regexp is None:
973 return None
974 if sys.platform.startswith('win'):
975 if not 'APPDATA' in os.environ:
976 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000977 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
978 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000979 else:
980 if not 'HOME' in os.environ:
981 return None
982 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
983 'svn.simple')
984 for credfile in os.listdir(auth_dir):
985 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
986 if regexp.match(cred_info.get('svn:realmstring')):
987 return cred_info.get('username')
988
989 @staticmethod
990 def ReadSimpleAuth(filename):
991 f = open(filename, 'r')
992 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000993 def ReadOneItem(item_type):
994 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000995 if not m:
996 return None
997 data = f.read(int(m.group(1)))
998 if f.read(1) != '\n':
999 return None
1000 return data
1001
1002 while True:
1003 key = ReadOneItem('K')
1004 if not key:
1005 break
1006 value = ReadOneItem('V')
1007 if not value:
1008 break
1009 values[key] = value
1010 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001011
1012 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001013 def GetCheckoutRoot(cwd):
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001014 """Returns the top level directory of the current repository.
1015
1016 The directory is returned as an absolute path.
1017 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001018 cwd = os.path.abspath(cwd)
maruel@chromium.org54019f32010-09-09 13:50:11 +00001019 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001020 info = SVN.CaptureLocalInfo([], cwd)
maruel@chromium.org885d6e82011-02-24 20:21:46 +00001021 cur_dir_repo_root = info['Repository Root']
1022 url = info['URL']
maruel@chromium.orgda64d632011-09-08 17:41:15 +00001023 except subprocess2.CalledProcessError:
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001024 return None
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001025 while True:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001026 parent = os.path.dirname(cwd)
maruel@chromium.org54019f32010-09-09 13:50:11 +00001027 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001028 info = SVN.CaptureLocalInfo([], parent)
maruel@chromium.org885d6e82011-02-24 20:21:46 +00001029 if (info['Repository Root'] != cur_dir_repo_root or
1030 info['URL'] != os.path.dirname(url)):
maruel@chromium.org54019f32010-09-09 13:50:11 +00001031 break
maruel@chromium.org885d6e82011-02-24 20:21:46 +00001032 url = info['URL']
maruel@chromium.orgda64d632011-09-08 17:41:15 +00001033 except subprocess2.CalledProcessError:
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001034 break
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001035 cwd = parent
1036 return GetCasedPath(cwd)
tony@chromium.org57564662010-04-14 02:35:12 +00001037
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +00001038 @staticmethod
1039 def IsValidRevision(url):
1040 """Verifies the revision looks like an SVN revision."""
1041 try:
1042 SVN.Capture(['info', url], cwd=None)
1043 return True
1044 except subprocess2.CalledProcessError:
1045 return False
1046
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001047 @classmethod
1048 def AssertVersion(cls, min_version):
tony@chromium.org57564662010-04-14 02:35:12 +00001049 """Asserts svn's version is at least min_version."""
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001050 if cls.current_version is None:
shouqun.liu@intel.com13b522c2012-07-20 17:16:51 +00001051 cls.current_version = cls.Capture(['--version', '--quiet'], None)
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001052 current_version_list = map(only_int, cls.current_version.split('.'))
tony@chromium.org57564662010-04-14 02:35:12 +00001053 for min_ver in map(int, min_version.split('.')):
1054 ver = current_version_list.pop(0)
1055 if ver < min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001056 return (False, cls.current_version)
tony@chromium.org57564662010-04-14 02:35:12 +00001057 elif ver > min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001058 return (True, cls.current_version)
1059 return (True, cls.current_version)
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001060
1061 @staticmethod
maruel@chromium.orgea15cb72012-05-04 14:16:31 +00001062 def Revert(cwd, callback=None, ignore_externals=False, no_ignore=False):
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001063 """Reverts all svn modifications in cwd, including properties.
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001064
1065 Deletes any modified files or directory.
1066
1067 A "svn update --revision BASE" call is required after to revive deleted
1068 files.
1069 """
maruel@chromium.orgea15cb72012-05-04 14:16:31 +00001070 for file_status in SVN.CaptureStatus(None, cwd, no_ignore=no_ignore):
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001071 file_path = os.path.join(cwd, file_status[1])
maruel@chromium.org8c415122011-03-15 17:14:27 +00001072 if (ignore_externals and
1073 file_status[0][0] == 'X' and
1074 file_status[0][1:].isspace()):
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001075 # Ignore externals.
1076 logging.info('Ignoring external %s' % file_status[1])
1077 continue
1078
maruel@chromium.org62087572012-04-24 23:16:28 +00001079 # This is the case where '! L .' is returned by 'svn status'. Just
1080 # strip off the '/.'.
1081 if file_path.endswith(os.path.sep + '.'):
1082 file_path = file_path[:-2]
1083
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001084 if callback:
1085 callback(file_status)
1086
maruel@chromium.org8c415122011-03-15 17:14:27 +00001087 if os.path.exists(file_path):
1088 # svn revert is really stupid. It fails on inconsistent line-endings,
1089 # on switched directories, etc. So take no chance and delete everything!
1090 # In theory, it wouldn't be necessary for property-only change but then
1091 # it'd have to look for switched directories, etc so it's not worth
1092 # optimizing this use case.
1093 if os.path.isfile(file_path) or os.path.islink(file_path):
1094 logging.info('os.remove(%s)' % file_path)
1095 os.remove(file_path)
1096 elif os.path.isdir(file_path):
digit@chromium.orgdc112ac2013-04-24 13:00:19 +00001097 logging.info('rmtree(%s)' % file_path)
1098 gclient_utils.rmtree(file_path)
maruel@chromium.org8c415122011-03-15 17:14:27 +00001099 else:
1100 logging.critical(
1101 ('No idea what is %s.\nYou just found a bug in gclient'
1102 ', please ping maruel@chromium.org ASAP!') % file_path)
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001103
maruel@chromium.org8c415122011-03-15 17:14:27 +00001104 if (file_status[0][0] in ('D', 'A', '!') or
1105 not file_status[0][1:].isspace()):
maruel@chromium.orgaf453492011-03-03 21:04:09 +00001106 # Added, deleted file requires manual intervention and require calling
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001107 # revert, like for properties.
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001108 if not os.path.isdir(cwd):
maruel@chromium.org8b322b32011-11-01 19:05:50 +00001109 # '.' was deleted. It's not worth continuing.
1110 return
maruel@chromium.orgaf453492011-03-03 21:04:09 +00001111 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001112 SVN.Capture(['revert', file_status[1]], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +00001113 except subprocess2.CalledProcessError:
maruel@chromium.orgaf453492011-03-03 21:04:09 +00001114 if not os.path.exists(file_path):
1115 continue
1116 raise