blob: 4703e61b3051fd1a90faaf3aa100b59cb448428d [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
nodir@chromium.orgead4c7e2014-04-03 01:01:06 +0000144 def IsWorkTreeDirty(cwd):
145 return GIT.Capture(['status', '-s'], cwd=cwd) != ''
146
147 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000148 def GetEmail(cwd):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000149 """Retrieves the user email address if known."""
150 # We could want to look at the svn cred when it has a svn remote but it
151 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000152 try:
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000153 return GIT.Capture(['config', 'user.email'], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000154 except subprocess2.CalledProcessError:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000155 return ''
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000156
157 @staticmethod
158 def ShortBranchName(branch):
159 """Converts a name like 'refs/heads/foo' to just 'foo'."""
160 return branch.replace('refs/heads/', '')
161
162 @staticmethod
163 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000164 """Returns the full branch reference, e.g. 'refs/heads/master'."""
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000165 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000166
167 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000168 def GetBranch(cwd):
169 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000170 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000171
172 @staticmethod
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000173 def IsGitSvn(cwd):
borenet@google.comb09097a2014-04-09 19:09:08 +0000174 """Returns True if this repo looks like it's using git-svn."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000175 # If you have any "svn-remote.*" config keys, we think you're using svn.
176 try:
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000177 GIT.Capture(['config', '--local', '--get-regexp', r'^svn-remote\.'],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000178 cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000179 return True
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000180 except subprocess2.CalledProcessError:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000181 return False
182
183 @staticmethod
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000184 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
185 """Return the corresponding git ref if |base_url| together with |glob_spec|
186 matches the full |url|.
187
188 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
189 """
190 fetch_suburl, as_ref = glob_spec.split(':')
191 if allow_wildcards:
192 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
193 if glob_match:
194 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
195 # "branches/{472,597,648}/src:refs/remotes/svn/*".
196 branch_re = re.escape(base_url)
197 if glob_match.group(1):
198 branch_re += '/' + re.escape(glob_match.group(1))
199 wildcard = glob_match.group(2)
200 if wildcard == '*':
201 branch_re += '([^/]*)'
202 else:
203 # Escape and replace surrounding braces with parentheses and commas
204 # with pipe symbols.
205 wildcard = re.escape(wildcard)
206 wildcard = re.sub('^\\\\{', '(', wildcard)
207 wildcard = re.sub('\\\\,', '|', wildcard)
208 wildcard = re.sub('\\\\}$', ')', wildcard)
209 branch_re += wildcard
210 if glob_match.group(3):
211 branch_re += re.escape(glob_match.group(3))
212 match = re.match(branch_re, url)
213 if match:
214 return re.sub('\*$', match.group(1), as_ref)
215
216 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
217 if fetch_suburl:
218 full_url = base_url + '/' + fetch_suburl
219 else:
220 full_url = base_url
221 if full_url == url:
222 return as_ref
223 return None
224
225 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000226 def GetSVNBranch(cwd):
227 """Returns the svn branch name if found."""
228 # Try to figure out which remote branch we're based on.
229 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000230 # 1) iterate through our branch history and find the svn URL.
231 # 2) find the svn-remote that fetches from the URL.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000232
233 # regexp matching the git-svn line that contains the URL.
234 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
235
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000236 # We don't want to go through all of history, so read a line from the
237 # pipe at a time.
238 # The -100 is an arbitrary limit so we don't search forever.
239 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.orgf94e3f12011-12-13 21:03:46 +0000240 proc = subprocess2.Popen(cmd, cwd=cwd, stdout=subprocess2.PIPE)
maruel@chromium.orge8c28622011-04-05 14:41:44 +0000241 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000242 for line in proc.stdout:
243 match = git_svn_re.match(line)
244 if match:
245 url = match.group(1)
246 proc.stdout.close() # Cut pipe.
247 break
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000248
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000249 if url:
250 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000251 remotes = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000252 ['config', '--local', '--get-regexp', r'^svn-remote\..*\.url'],
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000253 cwd=cwd).splitlines()
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000254 for remote in remotes:
255 match = svn_remote_re.match(remote)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000256 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000257 remote = match.group(1)
258 base_url = match.group(2)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000259 try:
260 fetch_spec = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000261 ['config', '--local', 'svn-remote.%s.fetch' % remote],
262 cwd=cwd)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000263 branch = GIT.MatchSvnGlob(url, base_url, fetch_spec, False)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000264 except subprocess2.CalledProcessError:
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000265 branch = None
266 if branch:
267 return branch
268 try:
269 branch_spec = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000270 ['config', '--local', 'svn-remote.%s.branches' % remote],
271 cwd=cwd)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000272 branch = GIT.MatchSvnGlob(url, base_url, branch_spec, True)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000273 except subprocess2.CalledProcessError:
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000274 branch = None
275 if branch:
276 return branch
277 try:
278 tag_spec = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000279 ['config', '--local', 'svn-remote.%s.tags' % remote],
280 cwd=cwd)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000281 branch = GIT.MatchSvnGlob(url, base_url, tag_spec, True)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000282 except subprocess2.CalledProcessError:
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000283 branch = None
284 if branch:
285 return branch
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000286
287 @staticmethod
288 def FetchUpstreamTuple(cwd):
289 """Returns a tuple containg remote and remote ref,
290 e.g. 'origin', 'refs/heads/master'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000291 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000292 """
293 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000294 branch = GIT.GetBranch(cwd)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000295 try:
296 upstream_branch = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000297 ['config', '--local', 'branch.%s.merge' % branch], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000298 except subprocess2.CalledProcessError:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000299 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000300 if upstream_branch:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000301 try:
302 remote = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000303 ['config', '--local', 'branch.%s.remote' % branch], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000304 except subprocess2.CalledProcessError:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000305 pass
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000306 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000307 try:
308 upstream_branch = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000309 ['config', '--local', 'rietveld.upstream-branch'], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000310 except subprocess2.CalledProcessError:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000311 upstream_branch = None
312 if upstream_branch:
313 try:
314 remote = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000315 ['config', '--local', 'rietveld.upstream-remote'], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000316 except subprocess2.CalledProcessError:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000317 pass
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000318 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000319 # Fall back on trying a git-svn upstream branch.
320 if GIT.IsGitSvn(cwd):
321 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000322 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000323 # Else, try to guess the origin remote.
324 remote_branches = GIT.Capture(['branch', '-r'], cwd=cwd).split()
325 if 'origin/master' in remote_branches:
326 # Fall back on origin/master if it exits.
327 remote = 'origin'
328 upstream_branch = 'refs/heads/master'
329 elif 'origin/trunk' in remote_branches:
330 # Fall back on origin/trunk if it exists. Generally a shared
331 # git-svn clone
332 remote = 'origin'
333 upstream_branch = 'refs/heads/trunk'
334 else:
335 # Give up.
336 remote = None
337 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000338 return remote, upstream_branch
339
340 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000341 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000342 """Gets the current branch's upstream branch."""
343 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000344 if remote != '.' and upstream_branch:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000345 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
346 return upstream_branch
347
348 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000349 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
350 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000351 """Diffs against the upstream branch or optionally another branch.
352
353 full_move means that move or copy operations should completely recreate the
354 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000355 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000356 branch = GIT.GetUpstreamBranch(cwd)
scottbyer@chromium.org33167332012-02-23 21:15:30 +0000357 command = ['diff', '-p', '--no-color', '--no-prefix', '--no-ext-diff',
evan@chromium.org400f3e72010-05-19 14:23:36 +0000358 branch + "..." + branch_head]
mcgrathr@chromium.org9249f642013-06-03 21:36:18 +0000359 if full_move:
360 command.append('--no-renames')
361 else:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000362 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000363 # TODO(maruel): --binary support.
364 if files:
365 command.append('--')
366 command.extend(files)
ilevy@chromium.org4380c802013-07-12 23:38:41 +0000367 diff = GIT.Capture(command, cwd=cwd, strip_out=False).splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000368 for i in range(len(diff)):
369 # In the case of added files, replace /dev/null with the path to the
370 # file being added.
371 if diff[i].startswith('--- /dev/null'):
372 diff[i] = '--- %s' % diff[i+1][4:]
373 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000374
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000375 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000376 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
377 """Returns the list of modified files between two branches."""
378 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000379 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000380 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000381 return GIT.Capture(command, cwd=cwd).splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000382
383 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000384 def GetPatchName(cwd):
385 """Constructs a name for this patch."""
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000386 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd)
maruel@chromium.org862ff8e2010-08-06 15:29:16 +0000387 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000388
389 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000390 def GetCheckoutRoot(cwd):
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000391 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000392 """
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000393 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000394 return os.path.abspath(os.path.join(cwd, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000395
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000396 @staticmethod
nodir@chromium.orgead4c7e2014-04-03 01:01:06 +0000397 def GetGitDir(cwd):
398 return os.path.abspath(GIT.Capture(['rev-parse', '--git-dir'], cwd=cwd))
399
400 @staticmethod
401 def IsInsideWorkTree(cwd):
402 try:
403 return GIT.Capture(['rev-parse', '--is-inside-work-tree'], cwd=cwd)
404 except (OSError, subprocess2.CalledProcessError):
405 return False
406
407 @staticmethod
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000408 def GetGitSvnHeadRev(cwd):
409 """Gets the most recently pulled git-svn revision."""
410 try:
411 output = GIT.Capture(['svn', 'info'], cwd=cwd)
412 match = re.search(r'^Revision: ([0-9]+)$', output, re.MULTILINE)
413 return int(match.group(1)) if match else None
414 except (subprocess2.CalledProcessError, ValueError):
415 return None
416
417 @staticmethod
wittman@chromium.org492a3682012-08-10 00:28:28 +0000418 def ParseGitSvnSha1(output):
419 """Parses git-svn output for the first sha1."""
420 match = re.search(r'[0-9a-fA-F]{40}', output)
421 return match.group(0) if match else None
422
423 @staticmethod
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000424 def GetSha1ForSvnRev(cwd, rev):
425 """Returns a corresponding git sha1 for a SVN revision."""
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000426 if not GIT.IsGitSvn(cwd=cwd):
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000427 return None
428 try:
szager@chromium.orgc51def32012-10-15 18:50:37 +0000429 output = GIT.Capture(['svn', 'find-rev', 'r' + str(rev)], cwd=cwd)
430 return GIT.ParseGitSvnSha1(output)
431 except subprocess2.CalledProcessError:
432 return None
433
434 @staticmethod
435 def GetBlessedSha1ForSvnRev(cwd, rev):
436 """Returns a git commit hash from the master branch history that has
437 accurate .DEPS.git and git submodules. To understand why this is more
438 complicated than a simple call to `git svn find-rev`, refer to:
439
440 http://www.chromium.org/developers/how-tos/git-repo
441 """
442 git_svn_rev = GIT.GetSha1ForSvnRev(cwd, rev)
443 if not git_svn_rev:
444 return None
445 try:
szager@google.com312a6a42012-10-11 21:19:42 +0000446 output = GIT.Capture(
447 ['rev-list', '--ancestry-path', '--reverse',
448 '--grep', 'SVN changes up to revision [0-9]*',
449 '%s..refs/remotes/origin/master' % git_svn_rev], cwd=cwd)
450 if not output:
451 return None
452 sha1 = output.splitlines()[0]
453 if not sha1:
454 return None
455 output = GIT.Capture(['rev-list', '-n', '1', '%s^1' % sha1], cwd=cwd)
456 if git_svn_rev != output.rstrip():
457 raise gclient_utils.Error(sha1)
458 return sha1
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000459 except subprocess2.CalledProcessError:
460 return None
461
462 @staticmethod
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000463 def IsValidRevision(cwd, rev, sha_only=False):
464 """Verifies the revision is a proper git revision.
465
466 sha_only: Fail unless rev is a sha hash.
467 """
maruel@chromium.org81473862012-06-27 17:30:56 +0000468 # 'git rev-parse foo' where foo is *any* 40 character hex string will return
469 # the string and return code 0. So strip one character to force 'git
470 # rev-parse' to do a hash table look-up and returns 128 if the hash is not
471 # present.
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000472 lookup_rev = rev
maruel@chromium.org81473862012-06-27 17:30:56 +0000473 if re.match(r'^[0-9a-fA-F]{40}$', rev):
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000474 lookup_rev = rev[:-1]
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000475 try:
ilevy@chromium.org224ba242013-07-08 22:02:31 +0000476 sha = GIT.Capture(['rev-parse', lookup_rev], cwd=cwd).lower()
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000477 if lookup_rev != rev:
478 # Make sure we get the original 40 chars back.
ilevy@chromium.org224ba242013-07-08 22:02:31 +0000479 return rev.lower() == sha
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000480 if sha_only:
ilevy@chromium.org224ba242013-07-08 22:02:31 +0000481 return sha.startswith(rev.lower())
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000482 return True
483 except subprocess2.CalledProcessError:
484 return False
485
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000486 @classmethod
487 def AssertVersion(cls, min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000488 """Asserts git's version is at least min_version."""
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000489 if cls.current_version is None:
bashi@chromium.orgfcffd482012-02-24 01:47:00 +0000490 current_version = cls.Capture(['--version'], '.')
491 matched = re.search(r'version ([0-9\.]+)', current_version)
492 cls.current_version = matched.group(1)
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000493 current_version_list = map(only_int, cls.current_version.split('.'))
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000494 for min_ver in map(int, min_version.split('.')):
495 ver = current_version_list.pop(0)
496 if ver < min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000497 return (False, cls.current_version)
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000498 elif ver > min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000499 return (True, cls.current_version)
500 return (True, cls.current_version)
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000501
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000502
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000503class SVN(object):
tony@chromium.org57564662010-04-14 02:35:12 +0000504 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000505
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000506 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000507 def Capture(args, cwd, **kwargs):
maruel@chromium.org54019f32010-09-09 13:50:11 +0000508 """Always redirect stderr.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000509
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000510 Throws an exception if non-0 is returned.
511 """
maruel@chromium.org904af082011-09-08 22:06:09 +0000512 return subprocess2.check_output(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000513 ['svn'] + args, stderr=subprocess2.PIPE, cwd=cwd, **kwargs)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000514
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000515 @staticmethod
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000516 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000517 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000518
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000519 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000520
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000521 svn's stdout is parsed to collect a list of files checked out or updated.
522 These files are appended to file_list. svn's stdout is also printed to
523 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000524
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000525 Args:
maruel@chromium.org03807072010-08-16 17:18:44 +0000526 verbose: If True, uses verbose output
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000527 args: A sequence of command line parameters to be passed to svn.
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000528 cwd: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000529
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000530 Raises:
531 Error: An error occurred while running the svn command.
532 """
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000533 stdout = stdout or sys.stdout
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000534 if file_list is None:
535 # Even if our caller doesn't care about file_list, we use it internally.
536 file_list = []
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000537
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000538 # svn update and svn checkout use the same pattern: the first three columns
539 # are for file status, property status, and lock status. This is followed
540 # by two spaces, and then the path to the file.
541 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000542
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000543 # The first three columns of svn status are the same as for svn update and
544 # svn checkout. The next three columns indicate addition-with-history,
545 # switch, and remote lock status. This is followed by one space, and then
546 # the path to the file.
547 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000548
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000549 # args[0] must be a supported command. This will blow up if it's something
550 # else, which is good. Note that the patterns are only effective when
551 # these commands are used in their ordinary forms, the patterns are invalid
552 # for "svn status --show-updates", for example.
553 pattern = {
554 'checkout': update_pattern,
555 'status': status_pattern,
556 'update': update_pattern,
557 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000558 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000559 # Place an upper limit.
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000560 backoff_time = 5
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000561 retries = 0
maruel@chromium.org03507062010-10-26 00:58:27 +0000562 while True:
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000563 retries += 1
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000564 previous_list_len = len(file_list)
565 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000566
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000567 def CaptureMatchingLines(line):
568 match = compiled_pattern.search(line)
569 if match:
570 file_list.append(match.group(1))
571 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000572 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000573
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000574 try:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000575 gclient_utils.CheckCallAndFilterAndHeader(
576 ['svn'] + args,
577 cwd=cwd,
578 always=verbose,
579 filter_fn=CaptureMatchingLines,
580 stdout=stdout)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000581 except subprocess2.CalledProcessError:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000582 def IsKnownFailure():
583 for x in failure:
584 if (x.startswith('svn: OPTIONS of') or
585 x.startswith('svn: PROPFIND of') or
586 x.startswith('svn: REPORT of') or
maruel@chromium.orgf61fc932010-08-19 13:05:24 +0000587 x.startswith('svn: Unknown hostname') or
maruel@chromium.org7d8b97d2011-10-11 23:32:30 +0000588 x.startswith('svn: Server sent unexpected return value') or
589 x.startswith('svn: Can\'t connect to host')):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000590 return True
591 return False
592
maruel@chromium.org953586a2010-06-15 14:22:24 +0000593 # Subversion client is really misbehaving with Google Code.
594 if args[0] == 'checkout':
595 # Ensure at least one file was checked out, otherwise *delete* the
596 # directory.
597 if len(file_list) == previous_list_len:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000598 if not IsKnownFailure():
maruel@chromium.org953586a2010-06-15 14:22:24 +0000599 # No known svn error was found, bail out.
600 raise
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000601 # No file were checked out, so make sure the directory is
602 # deleted in case it's messed up and try again.
603 # Warning: It's bad, it assumes args[2] is the directory
604 # argument.
605 if os.path.isdir(args[2]):
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000606 gclient_utils.rmtree(args[2])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000607 else:
608 # Progress was made, convert to update since an aborted checkout
609 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000610 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000611 else:
612 # It was an update or export.
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000613 # We enforce that some progress has been made or a known failure.
614 if len(file_list) == previous_list_len and not IsKnownFailure():
615 # No known svn error was found and no progress, bail out.
616 raise
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000617 if retries == 10:
maruel@chromium.org03507062010-10-26 00:58:27 +0000618 raise
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000619 print "Sleeping %.1f seconds and retrying...." % backoff_time
620 time.sleep(backoff_time)
621 backoff_time *= 1.3
maruel@chromium.org953586a2010-06-15 14:22:24 +0000622 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000623 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000624
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000625 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000626 def CaptureRemoteInfo(url):
627 """Returns a dictionary from the svn info output for the given url.
628
629 Throws an exception if svn info fails.
630 """
631 assert isinstance(url, str)
632 return SVN._CaptureInfo([url], None)
633
634 @staticmethod
635 def CaptureLocalInfo(files, cwd):
636 """Returns a dictionary from the svn info output for the given files.
637
638 Throws an exception if svn info fails.
639 """
640 assert isinstance(files, (list, tuple))
641 return SVN._CaptureInfo(files, cwd)
642
643 @staticmethod
644 def _CaptureInfo(files, cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000645 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000646
maruel@chromium.org54019f32010-09-09 13:50:11 +0000647 Throws an exception if svn info fails."""
maruel@chromium.orgd25fb8f2011-04-07 13:40:15 +0000648 result = {}
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000649 info = ElementTree.XML(SVN.Capture(['info', '--xml'] + files, cwd))
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000650 if info is None:
651 return result
652 entry = info.find('entry')
maruel@chromium.org6f323bb2011-04-26 15:42:53 +0000653 if entry is None:
654 return result
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000655
656 # Use .text when the item is not optional.
657 result['Path'] = entry.attrib['path']
maruel@chromium.org7d654672012-01-05 19:07:23 +0000658 rev = entry.attrib['revision']
659 try:
660 result['Revision'] = int(rev)
661 except ValueError:
662 result['Revision'] = None
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000663 result['Node Kind'] = entry.attrib['kind']
664 # Differs across versions.
665 if result['Node Kind'] == 'dir':
666 result['Node Kind'] = 'directory'
667 result['URL'] = entry.find('url').text
668 repository = entry.find('repository')
669 result['Repository Root'] = repository.find('root').text
670 result['UUID'] = repository.find('uuid')
671 wc_info = entry.find('wc-info')
672 if wc_info is not None:
673 result['Schedule'] = wc_info.find('schedule').text
674 result['Copied From URL'] = wc_info.find('copy-from-url')
675 result['Copied From Rev'] = wc_info.find('copy-from-rev')
676 else:
677 result['Schedule'] = None
678 result['Copied From URL'] = None
679 result['Copied From Rev'] = None
680 for key in result.keys():
681 if isinstance(result[key], unicode):
682 # Unicode results interferes with the higher layers matching up things
683 # in the deps dictionary.
684 result[key] = result[key].encode()
685 # Automatic conversion of optional parameters.
686 result[key] = getattr(result[key], 'text', result[key])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000687 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000688
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000689 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000690 def CaptureRevision(cwd):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000691 """Get the base revision of a SVN repository.
692
693 Returns:
694 Int base revision
695 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000696 return SVN.CaptureLocalInfo([], cwd).get('Revision')
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000697
698 @staticmethod
maruel@chromium.orgea15cb72012-05-04 14:16:31 +0000699 def CaptureStatus(files, cwd, no_ignore=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000700 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000701
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000702 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000703
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000704 Returns an array of (status, file) tuples."""
705 command = ["status", "--xml"]
maruel@chromium.orgea15cb72012-05-04 14:16:31 +0000706 if no_ignore:
707 command.append('--no-ignore')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000708 if not files:
709 pass
710 elif isinstance(files, basestring):
711 command.append(files)
712 else:
713 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000714
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000715 status_letter = {
716 None: ' ',
717 '': ' ',
718 'added': 'A',
719 'conflicted': 'C',
720 'deleted': 'D',
721 'external': 'X',
722 'ignored': 'I',
723 'incomplete': '!',
724 'merged': 'G',
725 'missing': '!',
726 'modified': 'M',
727 'none': ' ',
728 'normal': ' ',
729 'obstructed': '~',
730 'replaced': 'R',
731 'unversioned': '?',
732 }
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000733 dom = ElementTree.XML(SVN.Capture(command, cwd))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000734 results = []
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000735 if dom is None:
736 return results
737 # /status/target/entry/(wc-status|commit|author|date)
738 for target in dom.findall('target'):
739 for entry in target.findall('entry'):
740 file_path = entry.attrib['path']
741 wc_status = entry.find('wc-status')
742 # Emulate svn 1.5 status ouput...
743 statuses = [' '] * 7
744 # Col 0
745 xml_item_status = wc_status.attrib['item']
746 if xml_item_status in status_letter:
747 statuses[0] = status_letter[xml_item_status]
748 else:
749 raise gclient_utils.Error(
750 'Unknown item status "%s"; please implement me!' %
751 xml_item_status)
752 # Col 1
753 xml_props_status = wc_status.attrib['props']
754 if xml_props_status == 'modified':
755 statuses[1] = 'M'
756 elif xml_props_status == 'conflicted':
757 statuses[1] = 'C'
758 elif (not xml_props_status or xml_props_status == 'none' or
759 xml_props_status == 'normal'):
760 pass
761 else:
762 raise gclient_utils.Error(
763 'Unknown props status "%s"; please implement me!' %
764 xml_props_status)
765 # Col 2
766 if wc_status.attrib.get('wc-locked') == 'true':
767 statuses[2] = 'L'
768 # Col 3
769 if wc_status.attrib.get('copied') == 'true':
770 statuses[3] = '+'
771 # Col 4
772 if wc_status.attrib.get('switched') == 'true':
773 statuses[4] = 'S'
774 # TODO(maruel): Col 5 and 6
775 item = (''.join(statuses), file_path)
776 results.append(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000777 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000778
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000779 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000780 def IsMoved(filename, cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000781 """Determine if a file has been added through svn mv"""
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000782 assert isinstance(filename, basestring)
783 return SVN.IsMovedInfo(SVN.CaptureLocalInfo([filename], cwd))
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000784
785 @staticmethod
786 def IsMovedInfo(info):
787 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000788 return (info.get('Copied From URL') and
789 info.get('Copied From Rev') and
790 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000791
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000792 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000793 def GetFileProperty(filename, property_name, cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000794 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000795
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000796 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000797 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000798 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000799
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000800 Returns:
801 The value of the property, which will be the empty string if the property
802 is not set on the file. If the file is not under version control, the
803 empty string is also returned.
804 """
maruel@chromium.org54019f32010-09-09 13:50:11 +0000805 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000806 return SVN.Capture(['propget', property_name, filename], cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000807 except subprocess2.CalledProcessError:
maruel@chromium.org54019f32010-09-09 13:50:11 +0000808 return ''
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000809
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000810 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000811 def GenerateDiff(filenames, cwd, full_move, revision):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000812 """Returns a string containing the diff for the given file list.
813
814 The files in the list should either be absolute paths or relative to the
815 given root. If no root directory is provided, the repository root will be
816 used.
817 The diff will always use relative paths.
818 """
maruel@chromium.org00fdcb32011-02-24 01:41:02 +0000819 assert isinstance(filenames, (list, tuple))
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000820 # If the user specified a custom diff command in their svn config file,
821 # then it'll be used when we do svn diff, which we don't want to happen
822 # since we want the unified diff.
823 if SVN.AssertVersion("1.7")[0]:
824 # On svn >= 1.7, the "--internal-diff" flag will solve this.
825 return SVN._GenerateDiffInternal(filenames, cwd, full_move, revision,
826 ["diff", "--internal-diff"],
827 ["diff", "--internal-diff"])
828 else:
829 # On svn < 1.7, the "--internal-diff" flag doesn't exist. Using
830 # --diff-cmd=diff doesn't always work, since e.g. Windows cmd users may
831 # not have a "diff" executable in their path at all. So we use an empty
832 # temporary directory as the config directory, which bypasses any user
833 # settings for the diff-cmd. However, we don't pass this for the
834 # remote_safe_diff_command parameter, since when a new config-dir is
835 # specified for an svn diff against a remote URL, it triggers
836 # authentication prompts. In this case there isn't really a good
837 # alternative to svn 1.7's --internal-diff flag.
838 bogus_dir = tempfile.mkdtemp()
839 try:
840 return SVN._GenerateDiffInternal(filenames, cwd, full_move, revision,
841 ["diff", "--config-dir", bogus_dir],
842 ["diff"])
843 finally:
844 gclient_utils.rmtree(bogus_dir)
845
846 @staticmethod
847 def _GenerateDiffInternal(filenames, cwd, full_move, revision, diff_command,
848 remote_safe_diff_command):
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000849 root = os.path.normcase(os.path.join(cwd, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000850 def RelativePath(path, root):
851 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000852 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000853 return path[len(root):]
854 return path
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000855 # Cleanup filenames
856 filenames = [RelativePath(f, root) for f in filenames]
857 # Get information about the modified items (files and directories)
858 data = dict((f, SVN.CaptureLocalInfo([f], root)) for f in filenames)
859 diffs = []
860 if full_move:
861 # Eliminate modified files inside moved/copied directory.
862 for (filename, info) in data.iteritems():
863 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
864 # Remove files inside the directory.
865 filenames = [f for f in filenames
866 if not f.startswith(filename + os.path.sep)]
867 for filename in data.keys():
868 if not filename in filenames:
869 # Remove filtered out items.
870 del data[filename]
871 else:
872 metaheaders = []
873 for (filename, info) in data.iteritems():
874 if SVN.IsMovedInfo(info):
875 # for now, the most common case is a head copy,
876 # so let's just encode that as a straight up cp.
877 srcurl = info.get('Copied From URL')
878 file_root = info.get('Repository Root')
879 rev = int(info.get('Copied From Rev'))
880 assert srcurl.startswith(file_root)
881 src = srcurl[len(file_root)+1:]
882 try:
883 srcinfo = SVN.CaptureRemoteInfo(srcurl)
884 except subprocess2.CalledProcessError, e:
885 if not 'Not a valid URL' in e.stderr:
886 raise
887 # Assume the file was deleted. No idea how to figure out at which
888 # revision the file was deleted.
889 srcinfo = {'Revision': rev}
890 if (srcinfo.get('Revision') != rev and
891 SVN.Capture(remote_safe_diff_command + ['-r', '%d:head' % rev,
892 srcurl], cwd)):
893 metaheaders.append("#$ svn cp -r %d %s %s "
894 "### WARNING: note non-trunk copy\n" %
895 (rev, src, filename))
896 else:
897 metaheaders.append("#$ cp %s %s\n" % (src,
898 filename))
899 if metaheaders:
900 diffs.append("### BEGIN SVN COPY METADATA\n")
901 diffs.extend(metaheaders)
902 diffs.append("### END SVN COPY METADATA\n")
903 # Now ready to do the actual diff.
904 for filename in sorted(data):
905 diffs.append(SVN._DiffItemInternal(
906 filename, cwd, data[filename], diff_command, full_move, revision))
907 # Use StringIO since it can be messy when diffing a directory move with
908 # full_move=True.
909 buf = cStringIO.StringIO()
910 for d in filter(None, diffs):
911 buf.write(d)
912 result = buf.getvalue()
913 buf.close()
914 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000915
916 @staticmethod
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000917 def _DiffItemInternal(filename, cwd, info, diff_command, full_move, revision):
pkasting@chromium.org917ea7f2013-04-19 20:10:03 +0000918 """Grabs the diff data."""
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000919 command = diff_command + [filename]
pkasting@chromium.org917ea7f2013-04-19 20:10:03 +0000920 if revision:
921 command.extend(['--revision', revision])
922 data = None
923 if SVN.IsMovedInfo(info):
924 if full_move:
925 if info.get("Node Kind") == "directory":
926 # Things become tricky here. It's a directory copy/move. We need to
927 # diff all the files inside it.
928 # This will put a lot of pressure on the heap. This is why StringIO
929 # is used and converted back into a string at the end. The reason to
930 # return a string instead of a StringIO is that StringIO.write()
931 # doesn't accept a StringIO object. *sigh*.
932 for (dirpath, dirnames, filenames) in os.walk(filename):
933 # Cleanup all files starting with a '.'.
934 for d in dirnames:
935 if d.startswith('.'):
936 dirnames.remove(d)
937 for f in filenames:
938 if f.startswith('.'):
939 filenames.remove(f)
940 for f in filenames:
941 if data is None:
942 data = cStringIO.StringIO()
943 data.write(GenFakeDiff(os.path.join(dirpath, f)))
944 if data:
945 tmp = data.getvalue()
946 data.close()
947 data = tmp
948 else:
949 data = GenFakeDiff(filename)
950 else:
951 if info.get("Node Kind") != "directory":
952 # svn diff on a mv/cp'd file outputs nothing if there was no change.
953 data = SVN.Capture(command, cwd)
954 if not data:
955 # We put in an empty Index entry so upload.py knows about them.
956 data = "Index: %s\n" % filename.replace(os.sep, '/')
957 # Otherwise silently ignore directories.
958 else:
959 if info.get("Node Kind") != "directory":
960 # Normal simple case.
961 try:
962 data = SVN.Capture(command, cwd)
963 except subprocess2.CalledProcessError:
964 if revision:
965 data = GenFakeDiff(filename)
966 else:
967 raise
968 # Otherwise silently ignore directories.
969 return data
970
971 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000972 def GetEmail(cwd):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000973 """Retrieves the svn account which we assume is an email address."""
maruel@chromium.org54019f32010-09-09 13:50:11 +0000974 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000975 infos = SVN.CaptureLocalInfo([], cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000976 except subprocess2.CalledProcessError:
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000977 return None
978
979 # Should check for uuid but it is incorrectly saved for https creds.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000980 root = infos['Repository Root']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000981 realm = root.rsplit('/', 1)[0]
maruel@chromium.org54019f32010-09-09 13:50:11 +0000982 uuid = infos['UUID']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000983 if root.startswith('https') or not uuid:
984 regexp = re.compile(r'<%s:\d+>.*' % realm)
985 else:
986 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
987 if regexp is None:
988 return None
989 if sys.platform.startswith('win'):
990 if not 'APPDATA' in os.environ:
991 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000992 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
993 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000994 else:
995 if not 'HOME' in os.environ:
996 return None
997 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
998 'svn.simple')
999 for credfile in os.listdir(auth_dir):
1000 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
1001 if regexp.match(cred_info.get('svn:realmstring')):
1002 return cred_info.get('username')
1003
1004 @staticmethod
1005 def ReadSimpleAuth(filename):
1006 f = open(filename, 'r')
1007 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001008 def ReadOneItem(item_type):
1009 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +00001010 if not m:
1011 return None
1012 data = f.read(int(m.group(1)))
1013 if f.read(1) != '\n':
1014 return None
1015 return data
1016
1017 while True:
1018 key = ReadOneItem('K')
1019 if not key:
1020 break
1021 value = ReadOneItem('V')
1022 if not value:
1023 break
1024 values[key] = value
1025 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001026
1027 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001028 def GetCheckoutRoot(cwd):
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001029 """Returns the top level directory of the current repository.
1030
1031 The directory is returned as an absolute path.
1032 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001033 cwd = os.path.abspath(cwd)
maruel@chromium.org54019f32010-09-09 13:50:11 +00001034 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001035 info = SVN.CaptureLocalInfo([], cwd)
maruel@chromium.org885d6e82011-02-24 20:21:46 +00001036 cur_dir_repo_root = info['Repository Root']
1037 url = info['URL']
maruel@chromium.orgda64d632011-09-08 17:41:15 +00001038 except subprocess2.CalledProcessError:
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001039 return None
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001040 while True:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001041 parent = os.path.dirname(cwd)
maruel@chromium.org54019f32010-09-09 13:50:11 +00001042 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001043 info = SVN.CaptureLocalInfo([], parent)
maruel@chromium.org885d6e82011-02-24 20:21:46 +00001044 if (info['Repository Root'] != cur_dir_repo_root or
1045 info['URL'] != os.path.dirname(url)):
maruel@chromium.org54019f32010-09-09 13:50:11 +00001046 break
maruel@chromium.org885d6e82011-02-24 20:21:46 +00001047 url = info['URL']
maruel@chromium.orgda64d632011-09-08 17:41:15 +00001048 except subprocess2.CalledProcessError:
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001049 break
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001050 cwd = parent
1051 return GetCasedPath(cwd)
tony@chromium.org57564662010-04-14 02:35:12 +00001052
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +00001053 @staticmethod
1054 def IsValidRevision(url):
1055 """Verifies the revision looks like an SVN revision."""
1056 try:
1057 SVN.Capture(['info', url], cwd=None)
1058 return True
1059 except subprocess2.CalledProcessError:
1060 return False
1061
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001062 @classmethod
1063 def AssertVersion(cls, min_version):
tony@chromium.org57564662010-04-14 02:35:12 +00001064 """Asserts svn's version is at least min_version."""
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001065 if cls.current_version is None:
shouqun.liu@intel.com13b522c2012-07-20 17:16:51 +00001066 cls.current_version = cls.Capture(['--version', '--quiet'], None)
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001067 current_version_list = map(only_int, cls.current_version.split('.'))
tony@chromium.org57564662010-04-14 02:35:12 +00001068 for min_ver in map(int, min_version.split('.')):
1069 ver = current_version_list.pop(0)
1070 if ver < min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001071 return (False, cls.current_version)
tony@chromium.org57564662010-04-14 02:35:12 +00001072 elif ver > min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001073 return (True, cls.current_version)
1074 return (True, cls.current_version)
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001075
1076 @staticmethod
maruel@chromium.orgea15cb72012-05-04 14:16:31 +00001077 def Revert(cwd, callback=None, ignore_externals=False, no_ignore=False):
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001078 """Reverts all svn modifications in cwd, including properties.
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001079
1080 Deletes any modified files or directory.
1081
1082 A "svn update --revision BASE" call is required after to revive deleted
1083 files.
1084 """
maruel@chromium.orgea15cb72012-05-04 14:16:31 +00001085 for file_status in SVN.CaptureStatus(None, cwd, no_ignore=no_ignore):
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001086 file_path = os.path.join(cwd, file_status[1])
maruel@chromium.org8c415122011-03-15 17:14:27 +00001087 if (ignore_externals and
1088 file_status[0][0] == 'X' and
1089 file_status[0][1:].isspace()):
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001090 # Ignore externals.
1091 logging.info('Ignoring external %s' % file_status[1])
1092 continue
1093
maruel@chromium.org62087572012-04-24 23:16:28 +00001094 # This is the case where '! L .' is returned by 'svn status'. Just
1095 # strip off the '/.'.
1096 if file_path.endswith(os.path.sep + '.'):
1097 file_path = file_path[:-2]
1098
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001099 if callback:
1100 callback(file_status)
1101
maruel@chromium.org8c415122011-03-15 17:14:27 +00001102 if os.path.exists(file_path):
1103 # svn revert is really stupid. It fails on inconsistent line-endings,
1104 # on switched directories, etc. So take no chance and delete everything!
1105 # In theory, it wouldn't be necessary for property-only change but then
1106 # it'd have to look for switched directories, etc so it's not worth
1107 # optimizing this use case.
1108 if os.path.isfile(file_path) or os.path.islink(file_path):
1109 logging.info('os.remove(%s)' % file_path)
1110 os.remove(file_path)
1111 elif os.path.isdir(file_path):
digit@chromium.orgdc112ac2013-04-24 13:00:19 +00001112 logging.info('rmtree(%s)' % file_path)
1113 gclient_utils.rmtree(file_path)
maruel@chromium.org8c415122011-03-15 17:14:27 +00001114 else:
1115 logging.critical(
1116 ('No idea what is %s.\nYou just found a bug in gclient'
1117 ', please ping maruel@chromium.org ASAP!') % file_path)
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001118
maruel@chromium.org8c415122011-03-15 17:14:27 +00001119 if (file_status[0][0] in ('D', 'A', '!') or
1120 not file_status[0][1:].isspace()):
maruel@chromium.orgaf453492011-03-03 21:04:09 +00001121 # Added, deleted file requires manual intervention and require calling
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001122 # revert, like for properties.
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001123 if not os.path.isdir(cwd):
maruel@chromium.org8b322b32011-11-01 19:05:50 +00001124 # '.' was deleted. It's not worth continuing.
1125 return
maruel@chromium.orgaf453492011-03-03 21:04:09 +00001126 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001127 SVN.Capture(['revert', file_status[1]], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +00001128 except subprocess2.CalledProcessError:
maruel@chromium.orgaf453492011-03-03 21:04:09 +00001129 if not os.path.exists(file_path):
1130 continue
1131 raise