blob: 4e9c11c7cf12c67c8ecfd6bcf0d45d75a2f85207 [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
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000100 def Capture(args, cwd, **kwargs):
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000101 return subprocess2.check_output(
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000102 ['git', '--no-pager'] + args,
103 cwd=cwd, stderr=subprocess2.PIPE, **kwargs)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000104
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000105 @staticmethod
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000106 def CaptureStatus(files, cwd, upstream_branch):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000107 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000108
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000109 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000110
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000111 Returns an array of (status, file) tuples."""
msb@chromium.org786fb682010-06-02 15:16:23 +0000112 if upstream_branch is None:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000113 upstream_branch = GIT.GetUpstreamBranch(cwd)
msb@chromium.org786fb682010-06-02 15:16:23 +0000114 if upstream_branch is None:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000115 raise gclient_utils.Error('Cannot determine upstream branch')
116 command = ['diff', '--name-status', '-r', '%s...' % upstream_branch]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000117 if not files:
118 pass
119 elif isinstance(files, basestring):
120 command.append(files)
121 else:
122 command.extend(files)
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000123 status = GIT.Capture(command, cwd).rstrip()
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000124 results = []
125 if status:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000126 for statusline in status.splitlines():
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +0000127 # 3-way merges can cause the status can be 'MMM' instead of 'M'. This
128 # can happen when the user has 2 local branches and he diffs between
129 # these 2 branches instead diffing to upstream.
130 m = re.match('^(\w)+\t(.+)$', statusline)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000131 if not m:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000132 raise gclient_utils.Error(
133 'status currently unsupported: %s' % statusline)
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +0000134 # Only grab the first letter.
135 results.append(('%s ' % m.group(1)[0], m.group(2)))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000136 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000137
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000138 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000139 def GetEmail(cwd):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000140 """Retrieves the user email address if known."""
141 # We could want to look at the svn cred when it has a svn remote but it
142 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000143 try:
144 return GIT.Capture(['config', 'user.email'], cwd=cwd).strip()
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000145 except subprocess2.CalledProcessError:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000146 return ''
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000147
148 @staticmethod
149 def ShortBranchName(branch):
150 """Converts a name like 'refs/heads/foo' to just 'foo'."""
151 return branch.replace('refs/heads/', '')
152
153 @staticmethod
154 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000155 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000156 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd).strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000157
158 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000159 def GetBranch(cwd):
160 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000161 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000162
163 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000164 def IsGitSvn(cwd):
165 """Returns true if this repo looks like it's using git-svn."""
166 # If you have any "svn-remote.*" config keys, we think you're using svn.
167 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000168 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000169 return True
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000170 except subprocess2.CalledProcessError:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000171 return False
172
173 @staticmethod
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000174 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
175 """Return the corresponding git ref if |base_url| together with |glob_spec|
176 matches the full |url|.
177
178 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
179 """
180 fetch_suburl, as_ref = glob_spec.split(':')
181 if allow_wildcards:
182 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
183 if glob_match:
184 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
185 # "branches/{472,597,648}/src:refs/remotes/svn/*".
186 branch_re = re.escape(base_url)
187 if glob_match.group(1):
188 branch_re += '/' + re.escape(glob_match.group(1))
189 wildcard = glob_match.group(2)
190 if wildcard == '*':
191 branch_re += '([^/]*)'
192 else:
193 # Escape and replace surrounding braces with parentheses and commas
194 # with pipe symbols.
195 wildcard = re.escape(wildcard)
196 wildcard = re.sub('^\\\\{', '(', wildcard)
197 wildcard = re.sub('\\\\,', '|', wildcard)
198 wildcard = re.sub('\\\\}$', ')', wildcard)
199 branch_re += wildcard
200 if glob_match.group(3):
201 branch_re += re.escape(glob_match.group(3))
202 match = re.match(branch_re, url)
203 if match:
204 return re.sub('\*$', match.group(1), as_ref)
205
206 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
207 if fetch_suburl:
208 full_url = base_url + '/' + fetch_suburl
209 else:
210 full_url = base_url
211 if full_url == url:
212 return as_ref
213 return None
214
215 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000216 def GetSVNBranch(cwd):
217 """Returns the svn branch name if found."""
218 # Try to figure out which remote branch we're based on.
219 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000220 # 1) iterate through our branch history and find the svn URL.
221 # 2) find the svn-remote that fetches from the URL.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000222
223 # regexp matching the git-svn line that contains the URL.
224 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
225
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000226 # We don't want to go through all of history, so read a line from the
227 # pipe at a time.
228 # The -100 is an arbitrary limit so we don't search forever.
229 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.orgf94e3f12011-12-13 21:03:46 +0000230 proc = subprocess2.Popen(cmd, cwd=cwd, stdout=subprocess2.PIPE)
maruel@chromium.orge8c28622011-04-05 14:41:44 +0000231 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000232 for line in proc.stdout:
233 match = git_svn_re.match(line)
234 if match:
235 url = match.group(1)
236 proc.stdout.close() # Cut pipe.
237 break
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000238
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000239 if url:
240 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000241 remotes = GIT.Capture(
242 ['config', '--get-regexp', r'^svn-remote\..*\.url'],
243 cwd=cwd).splitlines()
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000244 for remote in remotes:
245 match = svn_remote_re.match(remote)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000246 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000247 remote = match.group(1)
248 base_url = match.group(2)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000249 try:
250 fetch_spec = GIT.Capture(
251 ['config', 'svn-remote.%s.fetch' % remote],
252 cwd=cwd).strip()
253 branch = GIT.MatchSvnGlob(url, base_url, fetch_spec, False)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000254 except subprocess2.CalledProcessError:
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000255 branch = None
256 if branch:
257 return branch
258 try:
259 branch_spec = GIT.Capture(
260 ['config', 'svn-remote.%s.branches' % remote],
261 cwd=cwd).strip()
262 branch = GIT.MatchSvnGlob(url, base_url, branch_spec, True)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000263 except subprocess2.CalledProcessError:
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000264 branch = None
265 if branch:
266 return branch
267 try:
268 tag_spec = GIT.Capture(
269 ['config', 'svn-remote.%s.tags' % remote],
270 cwd=cwd).strip()
271 branch = GIT.MatchSvnGlob(url, base_url, tag_spec, True)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000272 except subprocess2.CalledProcessError:
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000273 branch = None
274 if branch:
275 return branch
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000276
277 @staticmethod
278 def FetchUpstreamTuple(cwd):
279 """Returns a tuple containg remote and remote ref,
280 e.g. 'origin', 'refs/heads/master'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000281 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000282 """
283 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000284 branch = GIT.GetBranch(cwd)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000285 try:
286 upstream_branch = GIT.Capture(
287 ['config', 'branch.%s.merge' % branch], cwd=cwd).strip()
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000288 except subprocess2.CalledProcessError:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000289 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000290 if upstream_branch:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000291 try:
292 remote = GIT.Capture(
293 ['config', 'branch.%s.remote' % branch], cwd=cwd).strip()
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000294 except subprocess2.CalledProcessError:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000295 pass
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000296 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000297 try:
298 upstream_branch = GIT.Capture(
299 ['config', 'rietveld.upstream-branch'], cwd=cwd).strip()
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000300 except subprocess2.CalledProcessError:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000301 upstream_branch = None
302 if upstream_branch:
303 try:
304 remote = GIT.Capture(
305 ['config', 'rietveld.upstream-remote'], cwd=cwd).strip()
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000306 except subprocess2.CalledProcessError:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000307 pass
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000308 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000309 # Fall back on trying a git-svn upstream branch.
310 if GIT.IsGitSvn(cwd):
311 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000312 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000313 # Else, try to guess the origin remote.
314 remote_branches = GIT.Capture(['branch', '-r'], cwd=cwd).split()
315 if 'origin/master' in remote_branches:
316 # Fall back on origin/master if it exits.
317 remote = 'origin'
318 upstream_branch = 'refs/heads/master'
319 elif 'origin/trunk' in remote_branches:
320 # Fall back on origin/trunk if it exists. Generally a shared
321 # git-svn clone
322 remote = 'origin'
323 upstream_branch = 'refs/heads/trunk'
324 else:
325 # Give up.
326 remote = None
327 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000328 return remote, upstream_branch
329
330 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000331 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000332 """Gets the current branch's upstream branch."""
333 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000334 if remote != '.' and upstream_branch:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000335 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
336 return upstream_branch
337
338 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000339 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
340 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000341 """Diffs against the upstream branch or optionally another branch.
342
343 full_move means that move or copy operations should completely recreate the
344 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000345 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000346 branch = GIT.GetUpstreamBranch(cwd)
scottbyer@chromium.org33167332012-02-23 21:15:30 +0000347 command = ['diff', '-p', '--no-color', '--no-prefix', '--no-ext-diff',
evan@chromium.org400f3e72010-05-19 14:23:36 +0000348 branch + "..." + branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000349 if not full_move:
350 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000351 # TODO(maruel): --binary support.
352 if files:
353 command.append('--')
354 command.extend(files)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000355 diff = GIT.Capture(command, cwd=cwd).splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000356 for i in range(len(diff)):
357 # In the case of added files, replace /dev/null with the path to the
358 # file being added.
359 if diff[i].startswith('--- /dev/null'):
360 diff[i] = '--- %s' % diff[i+1][4:]
361 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000362
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000363 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000364 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
365 """Returns the list of modified files between two branches."""
366 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000367 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000368 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000369 return GIT.Capture(command, cwd=cwd).splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000370
371 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000372 def GetPatchName(cwd):
373 """Constructs a name for this patch."""
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000374 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd).strip()
maruel@chromium.org862ff8e2010-08-06 15:29:16 +0000375 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000376
377 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000378 def GetCheckoutRoot(cwd):
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000379 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000380 """
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000381 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd).strip()
382 return os.path.abspath(os.path.join(cwd, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000383
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000384 @staticmethod
385 def GetGitSvnHeadRev(cwd):
386 """Gets the most recently pulled git-svn revision."""
387 try:
388 output = GIT.Capture(['svn', 'info'], cwd=cwd)
389 match = re.search(r'^Revision: ([0-9]+)$', output, re.MULTILINE)
390 return int(match.group(1)) if match else None
391 except (subprocess2.CalledProcessError, ValueError):
392 return None
393
394 @staticmethod
wittman@chromium.org492a3682012-08-10 00:28:28 +0000395 def ParseGitSvnSha1(output):
396 """Parses git-svn output for the first sha1."""
397 match = re.search(r'[0-9a-fA-F]{40}', output)
398 return match.group(0) if match else None
399
400 @staticmethod
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000401 def GetSha1ForSvnRev(cwd, rev):
402 """Returns a corresponding git sha1 for a SVN revision."""
403 if not GIT.IsGitSvn(cwd=cwd):
404 return None
405 try:
szager@chromium.orgc51def32012-10-15 18:50:37 +0000406 output = GIT.Capture(['svn', 'find-rev', 'r' + str(rev)], cwd=cwd)
407 return GIT.ParseGitSvnSha1(output)
408 except subprocess2.CalledProcessError:
409 return None
410
411 @staticmethod
412 def GetBlessedSha1ForSvnRev(cwd, rev):
413 """Returns a git commit hash from the master branch history that has
414 accurate .DEPS.git and git submodules. To understand why this is more
415 complicated than a simple call to `git svn find-rev`, refer to:
416
417 http://www.chromium.org/developers/how-tos/git-repo
418 """
419 git_svn_rev = GIT.GetSha1ForSvnRev(cwd, rev)
420 if not git_svn_rev:
421 return None
422 try:
szager@google.com312a6a42012-10-11 21:19:42 +0000423 output = GIT.Capture(
424 ['rev-list', '--ancestry-path', '--reverse',
425 '--grep', 'SVN changes up to revision [0-9]*',
426 '%s..refs/remotes/origin/master' % git_svn_rev], cwd=cwd)
427 if not output:
428 return None
429 sha1 = output.splitlines()[0]
430 if not sha1:
431 return None
432 output = GIT.Capture(['rev-list', '-n', '1', '%s^1' % sha1], cwd=cwd)
433 if git_svn_rev != output.rstrip():
434 raise gclient_utils.Error(sha1)
435 return sha1
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000436 except subprocess2.CalledProcessError:
437 return None
438
439 @staticmethod
440 def IsValidRevision(cwd, rev):
441 """Verifies the revision is a proper git revision."""
maruel@chromium.org81473862012-06-27 17:30:56 +0000442 # 'git rev-parse foo' where foo is *any* 40 character hex string will return
443 # the string and return code 0. So strip one character to force 'git
444 # rev-parse' to do a hash table look-up and returns 128 if the hash is not
445 # present.
446 if re.match(r'^[0-9a-fA-F]{40}$', rev):
447 rev = rev[:-1]
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000448 try:
449 GIT.Capture(['rev-parse', rev], cwd=cwd)
450 return True
451 except subprocess2.CalledProcessError:
452 return False
453
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000454 @classmethod
455 def AssertVersion(cls, min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000456 """Asserts git's version is at least min_version."""
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000457 if cls.current_version is None:
bashi@chromium.orgfcffd482012-02-24 01:47:00 +0000458 current_version = cls.Capture(['--version'], '.')
459 matched = re.search(r'version ([0-9\.]+)', current_version)
460 cls.current_version = matched.group(1)
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000461 current_version_list = map(only_int, cls.current_version.split('.'))
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000462 for min_ver in map(int, min_version.split('.')):
463 ver = current_version_list.pop(0)
464 if ver < min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000465 return (False, cls.current_version)
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000466 elif ver > min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000467 return (True, cls.current_version)
468 return (True, cls.current_version)
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000469
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000470
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000471class SVN(object):
tony@chromium.org57564662010-04-14 02:35:12 +0000472 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000473
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000474 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000475 def Capture(args, cwd, **kwargs):
maruel@chromium.org54019f32010-09-09 13:50:11 +0000476 """Always redirect stderr.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000477
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000478 Throws an exception if non-0 is returned.
479 """
maruel@chromium.org904af082011-09-08 22:06:09 +0000480 return subprocess2.check_output(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000481 ['svn'] + args, stderr=subprocess2.PIPE, cwd=cwd, **kwargs)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000482
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000483 @staticmethod
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000484 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000485 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000486
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000487 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000488
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000489 svn's stdout is parsed to collect a list of files checked out or updated.
490 These files are appended to file_list. svn's stdout is also printed to
491 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000492
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000493 Args:
maruel@chromium.org03807072010-08-16 17:18:44 +0000494 verbose: If True, uses verbose output
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000495 args: A sequence of command line parameters to be passed to svn.
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000496 cwd: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000497
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000498 Raises:
499 Error: An error occurred while running the svn command.
500 """
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000501 stdout = stdout or sys.stdout
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000502
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000503 # svn update and svn checkout use the same pattern: the first three columns
504 # are for file status, property status, and lock status. This is followed
505 # by two spaces, and then the path to the file.
506 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000507
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000508 # The first three columns of svn status are the same as for svn update and
509 # svn checkout. The next three columns indicate addition-with-history,
510 # switch, and remote lock status. This is followed by one space, and then
511 # the path to the file.
512 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000513
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000514 # args[0] must be a supported command. This will blow up if it's something
515 # else, which is good. Note that the patterns are only effective when
516 # these commands are used in their ordinary forms, the patterns are invalid
517 # for "svn status --show-updates", for example.
518 pattern = {
519 'checkout': update_pattern,
520 'status': status_pattern,
521 'update': update_pattern,
522 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000523 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000524 # Place an upper limit.
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000525 backoff_time = 5
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000526 retries = 0
maruel@chromium.org03507062010-10-26 00:58:27 +0000527 while True:
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000528 retries += 1
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000529 previous_list_len = len(file_list)
530 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000531
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000532 def CaptureMatchingLines(line):
533 match = compiled_pattern.search(line)
534 if match:
535 file_list.append(match.group(1))
536 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000537 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000538
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000539 try:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000540 gclient_utils.CheckCallAndFilterAndHeader(
541 ['svn'] + args,
542 cwd=cwd,
543 always=verbose,
544 filter_fn=CaptureMatchingLines,
545 stdout=stdout)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000546 except subprocess2.CalledProcessError:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000547 def IsKnownFailure():
548 for x in failure:
549 if (x.startswith('svn: OPTIONS of') or
550 x.startswith('svn: PROPFIND of') or
551 x.startswith('svn: REPORT of') or
maruel@chromium.orgf61fc932010-08-19 13:05:24 +0000552 x.startswith('svn: Unknown hostname') or
maruel@chromium.org7d8b97d2011-10-11 23:32:30 +0000553 x.startswith('svn: Server sent unexpected return value') or
554 x.startswith('svn: Can\'t connect to host')):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000555 return True
556 return False
557
maruel@chromium.org953586a2010-06-15 14:22:24 +0000558 # Subversion client is really misbehaving with Google Code.
559 if args[0] == 'checkout':
560 # Ensure at least one file was checked out, otherwise *delete* the
561 # directory.
562 if len(file_list) == previous_list_len:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000563 if not IsKnownFailure():
maruel@chromium.org953586a2010-06-15 14:22:24 +0000564 # No known svn error was found, bail out.
565 raise
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000566 # No file were checked out, so make sure the directory is
567 # deleted in case it's messed up and try again.
568 # Warning: It's bad, it assumes args[2] is the directory
569 # argument.
570 if os.path.isdir(args[2]):
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000571 gclient_utils.rmtree(args[2])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000572 else:
573 # Progress was made, convert to update since an aborted checkout
574 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000575 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000576 else:
577 # It was an update or export.
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000578 # We enforce that some progress has been made or a known failure.
579 if len(file_list) == previous_list_len and not IsKnownFailure():
580 # No known svn error was found and no progress, bail out.
581 raise
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000582 if retries == 10:
maruel@chromium.org03507062010-10-26 00:58:27 +0000583 raise
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000584 print "Sleeping %.1f seconds and retrying...." % backoff_time
585 time.sleep(backoff_time)
586 backoff_time *= 1.3
maruel@chromium.org953586a2010-06-15 14:22:24 +0000587 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000588 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000589
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000590 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000591 def CaptureRemoteInfo(url):
592 """Returns a dictionary from the svn info output for the given url.
593
594 Throws an exception if svn info fails.
595 """
596 assert isinstance(url, str)
597 return SVN._CaptureInfo([url], None)
598
599 @staticmethod
600 def CaptureLocalInfo(files, cwd):
601 """Returns a dictionary from the svn info output for the given files.
602
603 Throws an exception if svn info fails.
604 """
605 assert isinstance(files, (list, tuple))
606 return SVN._CaptureInfo(files, cwd)
607
608 @staticmethod
609 def _CaptureInfo(files, cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000610 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000611
maruel@chromium.org54019f32010-09-09 13:50:11 +0000612 Throws an exception if svn info fails."""
maruel@chromium.orgd25fb8f2011-04-07 13:40:15 +0000613 result = {}
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000614 info = ElementTree.XML(SVN.Capture(['info', '--xml'] + files, cwd))
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000615 if info is None:
616 return result
617 entry = info.find('entry')
maruel@chromium.org6f323bb2011-04-26 15:42:53 +0000618 if entry is None:
619 return result
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000620
621 # Use .text when the item is not optional.
622 result['Path'] = entry.attrib['path']
maruel@chromium.org7d654672012-01-05 19:07:23 +0000623 rev = entry.attrib['revision']
624 try:
625 result['Revision'] = int(rev)
626 except ValueError:
627 result['Revision'] = None
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000628 result['Node Kind'] = entry.attrib['kind']
629 # Differs across versions.
630 if result['Node Kind'] == 'dir':
631 result['Node Kind'] = 'directory'
632 result['URL'] = entry.find('url').text
633 repository = entry.find('repository')
634 result['Repository Root'] = repository.find('root').text
635 result['UUID'] = repository.find('uuid')
636 wc_info = entry.find('wc-info')
637 if wc_info is not None:
638 result['Schedule'] = wc_info.find('schedule').text
639 result['Copied From URL'] = wc_info.find('copy-from-url')
640 result['Copied From Rev'] = wc_info.find('copy-from-rev')
641 else:
642 result['Schedule'] = None
643 result['Copied From URL'] = None
644 result['Copied From Rev'] = None
645 for key in result.keys():
646 if isinstance(result[key], unicode):
647 # Unicode results interferes with the higher layers matching up things
648 # in the deps dictionary.
649 result[key] = result[key].encode()
650 # Automatic conversion of optional parameters.
651 result[key] = getattr(result[key], 'text', result[key])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000652 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000653
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000654 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000655 def CaptureRevision(cwd):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000656 """Get the base revision of a SVN repository.
657
658 Returns:
659 Int base revision
660 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000661 return SVN.CaptureLocalInfo([], cwd).get('Revision')
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000662
663 @staticmethod
maruel@chromium.orgea15cb72012-05-04 14:16:31 +0000664 def CaptureStatus(files, cwd, no_ignore=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000665 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000666
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000667 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000668
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000669 Returns an array of (status, file) tuples."""
670 command = ["status", "--xml"]
maruel@chromium.orgea15cb72012-05-04 14:16:31 +0000671 if no_ignore:
672 command.append('--no-ignore')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000673 if not files:
674 pass
675 elif isinstance(files, basestring):
676 command.append(files)
677 else:
678 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000679
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000680 status_letter = {
681 None: ' ',
682 '': ' ',
683 'added': 'A',
684 'conflicted': 'C',
685 'deleted': 'D',
686 'external': 'X',
687 'ignored': 'I',
688 'incomplete': '!',
689 'merged': 'G',
690 'missing': '!',
691 'modified': 'M',
692 'none': ' ',
693 'normal': ' ',
694 'obstructed': '~',
695 'replaced': 'R',
696 'unversioned': '?',
697 }
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000698 dom = ElementTree.XML(SVN.Capture(command, cwd))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000699 results = []
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000700 if dom is None:
701 return results
702 # /status/target/entry/(wc-status|commit|author|date)
703 for target in dom.findall('target'):
704 for entry in target.findall('entry'):
705 file_path = entry.attrib['path']
706 wc_status = entry.find('wc-status')
707 # Emulate svn 1.5 status ouput...
708 statuses = [' '] * 7
709 # Col 0
710 xml_item_status = wc_status.attrib['item']
711 if xml_item_status in status_letter:
712 statuses[0] = status_letter[xml_item_status]
713 else:
714 raise gclient_utils.Error(
715 'Unknown item status "%s"; please implement me!' %
716 xml_item_status)
717 # Col 1
718 xml_props_status = wc_status.attrib['props']
719 if xml_props_status == 'modified':
720 statuses[1] = 'M'
721 elif xml_props_status == 'conflicted':
722 statuses[1] = 'C'
723 elif (not xml_props_status or xml_props_status == 'none' or
724 xml_props_status == 'normal'):
725 pass
726 else:
727 raise gclient_utils.Error(
728 'Unknown props status "%s"; please implement me!' %
729 xml_props_status)
730 # Col 2
731 if wc_status.attrib.get('wc-locked') == 'true':
732 statuses[2] = 'L'
733 # Col 3
734 if wc_status.attrib.get('copied') == 'true':
735 statuses[3] = '+'
736 # Col 4
737 if wc_status.attrib.get('switched') == 'true':
738 statuses[4] = 'S'
739 # TODO(maruel): Col 5 and 6
740 item = (''.join(statuses), file_path)
741 results.append(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000742 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000743
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000744 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000745 def IsMoved(filename, cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000746 """Determine if a file has been added through svn mv"""
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000747 assert isinstance(filename, basestring)
748 return SVN.IsMovedInfo(SVN.CaptureLocalInfo([filename], cwd))
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000749
750 @staticmethod
751 def IsMovedInfo(info):
752 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000753 return (info.get('Copied From URL') and
754 info.get('Copied From Rev') and
755 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000756
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000757 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000758 def GetFileProperty(filename, property_name, cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000759 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000760
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000761 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000762 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000763 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000764
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000765 Returns:
766 The value of the property, which will be the empty string if the property
767 is not set on the file. If the file is not under version control, the
768 empty string is also returned.
769 """
maruel@chromium.org54019f32010-09-09 13:50:11 +0000770 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000771 return SVN.Capture(['propget', property_name, filename], cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000772 except subprocess2.CalledProcessError:
maruel@chromium.org54019f32010-09-09 13:50:11 +0000773 return ''
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000774
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000775 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000776 def GenerateDiff(filenames, cwd, full_move, revision):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000777 """Returns a string containing the diff for the given file list.
778
779 The files in the list should either be absolute paths or relative to the
780 given root. If no root directory is provided, the repository root will be
781 used.
782 The diff will always use relative paths.
783 """
maruel@chromium.org00fdcb32011-02-24 01:41:02 +0000784 assert isinstance(filenames, (list, tuple))
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000785 # If the user specified a custom diff command in their svn config file,
786 # then it'll be used when we do svn diff, which we don't want to happen
787 # since we want the unified diff.
788 if SVN.AssertVersion("1.7")[0]:
789 # On svn >= 1.7, the "--internal-diff" flag will solve this.
790 return SVN._GenerateDiffInternal(filenames, cwd, full_move, revision,
791 ["diff", "--internal-diff"],
792 ["diff", "--internal-diff"])
793 else:
794 # On svn < 1.7, the "--internal-diff" flag doesn't exist. Using
795 # --diff-cmd=diff doesn't always work, since e.g. Windows cmd users may
796 # not have a "diff" executable in their path at all. So we use an empty
797 # temporary directory as the config directory, which bypasses any user
798 # settings for the diff-cmd. However, we don't pass this for the
799 # remote_safe_diff_command parameter, since when a new config-dir is
800 # specified for an svn diff against a remote URL, it triggers
801 # authentication prompts. In this case there isn't really a good
802 # alternative to svn 1.7's --internal-diff flag.
803 bogus_dir = tempfile.mkdtemp()
804 try:
805 return SVN._GenerateDiffInternal(filenames, cwd, full_move, revision,
806 ["diff", "--config-dir", bogus_dir],
807 ["diff"])
808 finally:
809 gclient_utils.rmtree(bogus_dir)
810
811 @staticmethod
812 def _GenerateDiffInternal(filenames, cwd, full_move, revision, diff_command,
813 remote_safe_diff_command):
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000814 root = os.path.normcase(os.path.join(cwd, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000815 def RelativePath(path, root):
816 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000817 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000818 return path[len(root):]
819 return path
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000820 # Cleanup filenames
821 filenames = [RelativePath(f, root) for f in filenames]
822 # Get information about the modified items (files and directories)
823 data = dict((f, SVN.CaptureLocalInfo([f], root)) for f in filenames)
824 diffs = []
825 if full_move:
826 # Eliminate modified files inside moved/copied directory.
827 for (filename, info) in data.iteritems():
828 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
829 # Remove files inside the directory.
830 filenames = [f for f in filenames
831 if not f.startswith(filename + os.path.sep)]
832 for filename in data.keys():
833 if not filename in filenames:
834 # Remove filtered out items.
835 del data[filename]
836 else:
837 metaheaders = []
838 for (filename, info) in data.iteritems():
839 if SVN.IsMovedInfo(info):
840 # for now, the most common case is a head copy,
841 # so let's just encode that as a straight up cp.
842 srcurl = info.get('Copied From URL')
843 file_root = info.get('Repository Root')
844 rev = int(info.get('Copied From Rev'))
845 assert srcurl.startswith(file_root)
846 src = srcurl[len(file_root)+1:]
847 try:
848 srcinfo = SVN.CaptureRemoteInfo(srcurl)
849 except subprocess2.CalledProcessError, e:
850 if not 'Not a valid URL' in e.stderr:
851 raise
852 # Assume the file was deleted. No idea how to figure out at which
853 # revision the file was deleted.
854 srcinfo = {'Revision': rev}
855 if (srcinfo.get('Revision') != rev and
856 SVN.Capture(remote_safe_diff_command + ['-r', '%d:head' % rev,
857 srcurl], cwd)):
858 metaheaders.append("#$ svn cp -r %d %s %s "
859 "### WARNING: note non-trunk copy\n" %
860 (rev, src, filename))
861 else:
862 metaheaders.append("#$ cp %s %s\n" % (src,
863 filename))
864 if metaheaders:
865 diffs.append("### BEGIN SVN COPY METADATA\n")
866 diffs.extend(metaheaders)
867 diffs.append("### END SVN COPY METADATA\n")
868 # Now ready to do the actual diff.
869 for filename in sorted(data):
870 diffs.append(SVN._DiffItemInternal(
871 filename, cwd, data[filename], diff_command, full_move, revision))
872 # Use StringIO since it can be messy when diffing a directory move with
873 # full_move=True.
874 buf = cStringIO.StringIO()
875 for d in filter(None, diffs):
876 buf.write(d)
877 result = buf.getvalue()
878 buf.close()
879 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000880
881 @staticmethod
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000882 def _DiffItemInternal(filename, cwd, info, diff_command, full_move, revision):
pkasting@chromium.org917ea7f2013-04-19 20:10:03 +0000883 """Grabs the diff data."""
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000884 command = diff_command + [filename]
pkasting@chromium.org917ea7f2013-04-19 20:10:03 +0000885 if revision:
886 command.extend(['--revision', revision])
887 data = None
888 if SVN.IsMovedInfo(info):
889 if full_move:
890 if info.get("Node Kind") == "directory":
891 # Things become tricky here. It's a directory copy/move. We need to
892 # diff all the files inside it.
893 # This will put a lot of pressure on the heap. This is why StringIO
894 # is used and converted back into a string at the end. The reason to
895 # return a string instead of a StringIO is that StringIO.write()
896 # doesn't accept a StringIO object. *sigh*.
897 for (dirpath, dirnames, filenames) in os.walk(filename):
898 # Cleanup all files starting with a '.'.
899 for d in dirnames:
900 if d.startswith('.'):
901 dirnames.remove(d)
902 for f in filenames:
903 if f.startswith('.'):
904 filenames.remove(f)
905 for f in filenames:
906 if data is None:
907 data = cStringIO.StringIO()
908 data.write(GenFakeDiff(os.path.join(dirpath, f)))
909 if data:
910 tmp = data.getvalue()
911 data.close()
912 data = tmp
913 else:
914 data = GenFakeDiff(filename)
915 else:
916 if info.get("Node Kind") != "directory":
917 # svn diff on a mv/cp'd file outputs nothing if there was no change.
918 data = SVN.Capture(command, cwd)
919 if not data:
920 # We put in an empty Index entry so upload.py knows about them.
921 data = "Index: %s\n" % filename.replace(os.sep, '/')
922 # Otherwise silently ignore directories.
923 else:
924 if info.get("Node Kind") != "directory":
925 # Normal simple case.
926 try:
927 data = SVN.Capture(command, cwd)
928 except subprocess2.CalledProcessError:
929 if revision:
930 data = GenFakeDiff(filename)
931 else:
932 raise
933 # Otherwise silently ignore directories.
934 return data
935
936 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000937 def GetEmail(cwd):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000938 """Retrieves the svn account which we assume is an email address."""
maruel@chromium.org54019f32010-09-09 13:50:11 +0000939 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000940 infos = SVN.CaptureLocalInfo([], cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000941 except subprocess2.CalledProcessError:
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000942 return None
943
944 # Should check for uuid but it is incorrectly saved for https creds.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000945 root = infos['Repository Root']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000946 realm = root.rsplit('/', 1)[0]
maruel@chromium.org54019f32010-09-09 13:50:11 +0000947 uuid = infos['UUID']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000948 if root.startswith('https') or not uuid:
949 regexp = re.compile(r'<%s:\d+>.*' % realm)
950 else:
951 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
952 if regexp is None:
953 return None
954 if sys.platform.startswith('win'):
955 if not 'APPDATA' in os.environ:
956 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000957 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
958 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000959 else:
960 if not 'HOME' in os.environ:
961 return None
962 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
963 'svn.simple')
964 for credfile in os.listdir(auth_dir):
965 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
966 if regexp.match(cred_info.get('svn:realmstring')):
967 return cred_info.get('username')
968
969 @staticmethod
970 def ReadSimpleAuth(filename):
971 f = open(filename, 'r')
972 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000973 def ReadOneItem(item_type):
974 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000975 if not m:
976 return None
977 data = f.read(int(m.group(1)))
978 if f.read(1) != '\n':
979 return None
980 return data
981
982 while True:
983 key = ReadOneItem('K')
984 if not key:
985 break
986 value = ReadOneItem('V')
987 if not value:
988 break
989 values[key] = value
990 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000991
992 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000993 def GetCheckoutRoot(cwd):
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000994 """Returns the top level directory of the current repository.
995
996 The directory is returned as an absolute path.
997 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000998 cwd = os.path.abspath(cwd)
maruel@chromium.org54019f32010-09-09 13:50:11 +0000999 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001000 info = SVN.CaptureLocalInfo([], cwd)
maruel@chromium.org885d6e82011-02-24 20:21:46 +00001001 cur_dir_repo_root = info['Repository Root']
1002 url = info['URL']
maruel@chromium.orgda64d632011-09-08 17:41:15 +00001003 except subprocess2.CalledProcessError:
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001004 return None
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001005 while True:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001006 parent = os.path.dirname(cwd)
maruel@chromium.org54019f32010-09-09 13:50:11 +00001007 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001008 info = SVN.CaptureLocalInfo([], parent)
maruel@chromium.org885d6e82011-02-24 20:21:46 +00001009 if (info['Repository Root'] != cur_dir_repo_root or
1010 info['URL'] != os.path.dirname(url)):
maruel@chromium.org54019f32010-09-09 13:50:11 +00001011 break
maruel@chromium.org885d6e82011-02-24 20:21:46 +00001012 url = info['URL']
maruel@chromium.orgda64d632011-09-08 17:41:15 +00001013 except subprocess2.CalledProcessError:
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001014 break
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001015 cwd = parent
1016 return GetCasedPath(cwd)
tony@chromium.org57564662010-04-14 02:35:12 +00001017
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +00001018 @staticmethod
1019 def IsValidRevision(url):
1020 """Verifies the revision looks like an SVN revision."""
1021 try:
1022 SVN.Capture(['info', url], cwd=None)
1023 return True
1024 except subprocess2.CalledProcessError:
1025 return False
1026
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001027 @classmethod
1028 def AssertVersion(cls, min_version):
tony@chromium.org57564662010-04-14 02:35:12 +00001029 """Asserts svn's version is at least min_version."""
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001030 if cls.current_version is None:
shouqun.liu@intel.com13b522c2012-07-20 17:16:51 +00001031 cls.current_version = cls.Capture(['--version', '--quiet'], None)
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001032 current_version_list = map(only_int, cls.current_version.split('.'))
tony@chromium.org57564662010-04-14 02:35:12 +00001033 for min_ver in map(int, min_version.split('.')):
1034 ver = current_version_list.pop(0)
1035 if ver < min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001036 return (False, cls.current_version)
tony@chromium.org57564662010-04-14 02:35:12 +00001037 elif ver > min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001038 return (True, cls.current_version)
1039 return (True, cls.current_version)
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001040
1041 @staticmethod
maruel@chromium.orgea15cb72012-05-04 14:16:31 +00001042 def Revert(cwd, callback=None, ignore_externals=False, no_ignore=False):
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001043 """Reverts all svn modifications in cwd, including properties.
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001044
1045 Deletes any modified files or directory.
1046
1047 A "svn update --revision BASE" call is required after to revive deleted
1048 files.
1049 """
maruel@chromium.orgea15cb72012-05-04 14:16:31 +00001050 for file_status in SVN.CaptureStatus(None, cwd, no_ignore=no_ignore):
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001051 file_path = os.path.join(cwd, file_status[1])
maruel@chromium.org8c415122011-03-15 17:14:27 +00001052 if (ignore_externals and
1053 file_status[0][0] == 'X' and
1054 file_status[0][1:].isspace()):
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001055 # Ignore externals.
1056 logging.info('Ignoring external %s' % file_status[1])
1057 continue
1058
maruel@chromium.org62087572012-04-24 23:16:28 +00001059 # This is the case where '! L .' is returned by 'svn status'. Just
1060 # strip off the '/.'.
1061 if file_path.endswith(os.path.sep + '.'):
1062 file_path = file_path[:-2]
1063
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001064 if callback:
1065 callback(file_status)
1066
maruel@chromium.org8c415122011-03-15 17:14:27 +00001067 if os.path.exists(file_path):
1068 # svn revert is really stupid. It fails on inconsistent line-endings,
1069 # on switched directories, etc. So take no chance and delete everything!
1070 # In theory, it wouldn't be necessary for property-only change but then
1071 # it'd have to look for switched directories, etc so it's not worth
1072 # optimizing this use case.
1073 if os.path.isfile(file_path) or os.path.islink(file_path):
1074 logging.info('os.remove(%s)' % file_path)
1075 os.remove(file_path)
1076 elif os.path.isdir(file_path):
digit@chromium.orgdc112ac2013-04-24 13:00:19 +00001077 logging.info('rmtree(%s)' % file_path)
1078 gclient_utils.rmtree(file_path)
maruel@chromium.org8c415122011-03-15 17:14:27 +00001079 else:
1080 logging.critical(
1081 ('No idea what is %s.\nYou just found a bug in gclient'
1082 ', please ping maruel@chromium.org ASAP!') % file_path)
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001083
maruel@chromium.org8c415122011-03-15 17:14:27 +00001084 if (file_status[0][0] in ('D', 'A', '!') or
1085 not file_status[0][1:].isspace()):
maruel@chromium.orgaf453492011-03-03 21:04:09 +00001086 # Added, deleted file requires manual intervention and require calling
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001087 # revert, like for properties.
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001088 if not os.path.isdir(cwd):
maruel@chromium.org8b322b32011-11-01 19:05:50 +00001089 # '.' was deleted. It's not worth continuing.
1090 return
maruel@chromium.orgaf453492011-03-03 21:04:09 +00001091 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001092 SVN.Capture(['revert', file_status[1]], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +00001093 except subprocess2.CalledProcessError:
maruel@chromium.orgaf453492011-03-03 21:04:09 +00001094 if not os.path.exists(file_path):
1095 continue
1096 raise