blob: 6f508c83ab2b7a2e3ec9e266028164c14e0c2735 [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):
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'
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000104 return subprocess2.check_output(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000105 ['git'] + args,
106 cwd=cwd, stderr=subprocess2.PIPE, env=env, **kwargs).strip()
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000107
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000108 @staticmethod
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000109 def CaptureStatus(files, cwd, upstream_branch):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000110 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000111
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000112 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000113
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000114 Returns an array of (status, file) tuples."""
msb@chromium.org786fb682010-06-02 15:16:23 +0000115 if upstream_branch is None:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000116 upstream_branch = GIT.GetUpstreamBranch(cwd)
msb@chromium.org786fb682010-06-02 15:16:23 +0000117 if upstream_branch is None:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000118 raise gclient_utils.Error('Cannot determine upstream branch')
mcgrathr@chromium.org9249f642013-06-03 21:36:18 +0000119 command = ['diff', '--name-status', '--no-renames',
120 '-r', '%s...' % upstream_branch]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000121 if not files:
122 pass
123 elif isinstance(files, basestring):
124 command.append(files)
125 else:
126 command.extend(files)
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000127 status = GIT.Capture(command, cwd)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000128 results = []
129 if status:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000130 for statusline in status.splitlines():
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +0000131 # 3-way merges can cause the status can be 'MMM' instead of 'M'. This
132 # can happen when the user has 2 local branches and he diffs between
133 # these 2 branches instead diffing to upstream.
134 m = re.match('^(\w)+\t(.+)$', statusline)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000135 if not m:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000136 raise gclient_utils.Error(
137 'status currently unsupported: %s' % statusline)
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +0000138 # Only grab the first letter.
139 results.append(('%s ' % m.group(1)[0], m.group(2)))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000140 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000141
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000142 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000143 def GetEmail(cwd):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000144 """Retrieves the user email address if known."""
145 # We could want to look at the svn cred when it has a svn remote but it
146 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000147 try:
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000148 return GIT.Capture(['config', 'user.email'], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000149 except subprocess2.CalledProcessError:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000150 return ''
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000151
152 @staticmethod
153 def ShortBranchName(branch):
154 """Converts a name like 'refs/heads/foo' to just 'foo'."""
155 return branch.replace('refs/heads/', '')
156
157 @staticmethod
158 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000159 """Returns the full branch reference, e.g. 'refs/heads/master'."""
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000160 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000161
162 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000163 def GetBranch(cwd):
164 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000165 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000166
167 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000168 def IsGitSvn(cwd):
169 """Returns true if this repo looks like it's using git-svn."""
170 # If you have any "svn-remote.*" config keys, we think you're using svn.
171 try:
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000172 GIT.Capture(['config', '--local', '--get-regexp', r'^svn-remote\.'],
173 cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000174 return True
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000175 except subprocess2.CalledProcessError:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000176 return False
177
178 @staticmethod
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000179 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
180 """Return the corresponding git ref if |base_url| together with |glob_spec|
181 matches the full |url|.
182
183 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
184 """
185 fetch_suburl, as_ref = glob_spec.split(':')
186 if allow_wildcards:
187 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
188 if glob_match:
189 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
190 # "branches/{472,597,648}/src:refs/remotes/svn/*".
191 branch_re = re.escape(base_url)
192 if glob_match.group(1):
193 branch_re += '/' + re.escape(glob_match.group(1))
194 wildcard = glob_match.group(2)
195 if wildcard == '*':
196 branch_re += '([^/]*)'
197 else:
198 # Escape and replace surrounding braces with parentheses and commas
199 # with pipe symbols.
200 wildcard = re.escape(wildcard)
201 wildcard = re.sub('^\\\\{', '(', wildcard)
202 wildcard = re.sub('\\\\,', '|', wildcard)
203 wildcard = re.sub('\\\\}$', ')', wildcard)
204 branch_re += wildcard
205 if glob_match.group(3):
206 branch_re += re.escape(glob_match.group(3))
207 match = re.match(branch_re, url)
208 if match:
209 return re.sub('\*$', match.group(1), as_ref)
210
211 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
212 if fetch_suburl:
213 full_url = base_url + '/' + fetch_suburl
214 else:
215 full_url = base_url
216 if full_url == url:
217 return as_ref
218 return None
219
220 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000221 def GetSVNBranch(cwd):
222 """Returns the svn branch name if found."""
223 # Try to figure out which remote branch we're based on.
224 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000225 # 1) iterate through our branch history and find the svn URL.
226 # 2) find the svn-remote that fetches from the URL.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000227
228 # regexp matching the git-svn line that contains the URL.
229 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
230
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000231 # We don't want to go through all of history, so read a line from the
232 # pipe at a time.
233 # The -100 is an arbitrary limit so we don't search forever.
234 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.orgf94e3f12011-12-13 21:03:46 +0000235 proc = subprocess2.Popen(cmd, cwd=cwd, stdout=subprocess2.PIPE)
maruel@chromium.orge8c28622011-04-05 14:41:44 +0000236 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000237 for line in proc.stdout:
238 match = git_svn_re.match(line)
239 if match:
240 url = match.group(1)
241 proc.stdout.close() # Cut pipe.
242 break
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000243
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000244 if url:
245 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000246 remotes = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000247 ['config', '--local', '--get-regexp', r'^svn-remote\..*\.url'],
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000248 cwd=cwd).splitlines()
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000249 for remote in remotes:
250 match = svn_remote_re.match(remote)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000251 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000252 remote = match.group(1)
253 base_url = match.group(2)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000254 try:
255 fetch_spec = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000256 ['config', '--local', 'svn-remote.%s.fetch' % remote],
257 cwd=cwd)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000258 branch = GIT.MatchSvnGlob(url, base_url, fetch_spec, False)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000259 except subprocess2.CalledProcessError:
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000260 branch = None
261 if branch:
262 return branch
263 try:
264 branch_spec = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000265 ['config', '--local', 'svn-remote.%s.branches' % remote],
266 cwd=cwd)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000267 branch = GIT.MatchSvnGlob(url, base_url, branch_spec, True)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000268 except subprocess2.CalledProcessError:
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000269 branch = None
270 if branch:
271 return branch
272 try:
273 tag_spec = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000274 ['config', '--local', 'svn-remote.%s.tags' % remote],
275 cwd=cwd)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000276 branch = GIT.MatchSvnGlob(url, base_url, tag_spec, True)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000277 except subprocess2.CalledProcessError:
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000278 branch = None
279 if branch:
280 return branch
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000281
282 @staticmethod
283 def FetchUpstreamTuple(cwd):
284 """Returns a tuple containg remote and remote ref,
285 e.g. 'origin', 'refs/heads/master'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000286 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000287 """
288 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000289 branch = GIT.GetBranch(cwd)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000290 try:
291 upstream_branch = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000292 ['config', '--local', 'branch.%s.merge' % branch], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000293 except subprocess2.CalledProcessError:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000294 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000295 if upstream_branch:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000296 try:
297 remote = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000298 ['config', '--local', 'branch.%s.remote' % branch], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000299 except subprocess2.CalledProcessError:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000300 pass
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000301 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000302 try:
303 upstream_branch = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000304 ['config', '--local', 'rietveld.upstream-branch'], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000305 except subprocess2.CalledProcessError:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000306 upstream_branch = None
307 if upstream_branch:
308 try:
309 remote = GIT.Capture(
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000310 ['config', '--local', 'rietveld.upstream-remote'], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000311 except subprocess2.CalledProcessError:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000312 pass
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000313 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000314 # Fall back on trying a git-svn upstream branch.
315 if GIT.IsGitSvn(cwd):
316 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000317 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000318 # Else, try to guess the origin remote.
319 remote_branches = GIT.Capture(['branch', '-r'], cwd=cwd).split()
320 if 'origin/master' in remote_branches:
321 # Fall back on origin/master if it exits.
322 remote = 'origin'
323 upstream_branch = 'refs/heads/master'
324 elif 'origin/trunk' in remote_branches:
325 # Fall back on origin/trunk if it exists. Generally a shared
326 # git-svn clone
327 remote = 'origin'
328 upstream_branch = 'refs/heads/trunk'
329 else:
330 # Give up.
331 remote = None
332 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000333 return remote, upstream_branch
334
335 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000336 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000337 """Gets the current branch's upstream branch."""
338 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000339 if remote != '.' and upstream_branch:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000340 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
341 return upstream_branch
342
343 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000344 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
345 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000346 """Diffs against the upstream branch or optionally another branch.
347
348 full_move means that move or copy operations should completely recreate the
349 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000350 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000351 branch = GIT.GetUpstreamBranch(cwd)
scottbyer@chromium.org33167332012-02-23 21:15:30 +0000352 command = ['diff', '-p', '--no-color', '--no-prefix', '--no-ext-diff',
evan@chromium.org400f3e72010-05-19 14:23:36 +0000353 branch + "..." + branch_head]
mcgrathr@chromium.org9249f642013-06-03 21:36:18 +0000354 if full_move:
355 command.append('--no-renames')
356 else:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000357 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000358 # TODO(maruel): --binary support.
359 if files:
360 command.append('--')
361 command.extend(files)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000362 diff = GIT.Capture(command, cwd=cwd).splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000363 for i in range(len(diff)):
364 # In the case of added files, replace /dev/null with the path to the
365 # file being added.
366 if diff[i].startswith('--- /dev/null'):
367 diff[i] = '--- %s' % diff[i+1][4:]
368 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000369
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000370 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000371 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
372 """Returns the list of modified files between two branches."""
373 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000374 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000375 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000376 return GIT.Capture(command, cwd=cwd).splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000377
378 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000379 def GetPatchName(cwd):
380 """Constructs a name for this patch."""
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000381 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd)
maruel@chromium.org862ff8e2010-08-06 15:29:16 +0000382 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000383
384 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000385 def GetCheckoutRoot(cwd):
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000386 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000387 """
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000388 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000389 return os.path.abspath(os.path.join(cwd, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000390
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000391 @staticmethod
392 def GetGitSvnHeadRev(cwd):
393 """Gets the most recently pulled git-svn revision."""
394 try:
395 output = GIT.Capture(['svn', 'info'], cwd=cwd)
396 match = re.search(r'^Revision: ([0-9]+)$', output, re.MULTILINE)
397 return int(match.group(1)) if match else None
398 except (subprocess2.CalledProcessError, ValueError):
399 return None
400
401 @staticmethod
wittman@chromium.org492a3682012-08-10 00:28:28 +0000402 def ParseGitSvnSha1(output):
403 """Parses git-svn output for the first sha1."""
404 match = re.search(r'[0-9a-fA-F]{40}', output)
405 return match.group(0) if match else None
406
407 @staticmethod
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000408 def GetSha1ForSvnRev(cwd, rev):
409 """Returns a corresponding git sha1 for a SVN revision."""
410 if not GIT.IsGitSvn(cwd=cwd):
411 return None
412 try:
szager@chromium.orgc51def32012-10-15 18:50:37 +0000413 output = GIT.Capture(['svn', 'find-rev', 'r' + str(rev)], cwd=cwd)
414 return GIT.ParseGitSvnSha1(output)
415 except subprocess2.CalledProcessError:
416 return None
417
418 @staticmethod
419 def GetBlessedSha1ForSvnRev(cwd, rev):
420 """Returns a git commit hash from the master branch history that has
421 accurate .DEPS.git and git submodules. To understand why this is more
422 complicated than a simple call to `git svn find-rev`, refer to:
423
424 http://www.chromium.org/developers/how-tos/git-repo
425 """
426 git_svn_rev = GIT.GetSha1ForSvnRev(cwd, rev)
427 if not git_svn_rev:
428 return None
429 try:
szager@google.com312a6a42012-10-11 21:19:42 +0000430 output = GIT.Capture(
431 ['rev-list', '--ancestry-path', '--reverse',
432 '--grep', 'SVN changes up to revision [0-9]*',
433 '%s..refs/remotes/origin/master' % git_svn_rev], cwd=cwd)
434 if not output:
435 return None
436 sha1 = output.splitlines()[0]
437 if not sha1:
438 return None
439 output = GIT.Capture(['rev-list', '-n', '1', '%s^1' % sha1], cwd=cwd)
440 if git_svn_rev != output.rstrip():
441 raise gclient_utils.Error(sha1)
442 return sha1
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000443 except subprocess2.CalledProcessError:
444 return None
445
446 @staticmethod
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000447 def IsValidRevision(cwd, rev, sha_only=False):
448 """Verifies the revision is a proper git revision.
449
450 sha_only: Fail unless rev is a sha hash.
451 """
maruel@chromium.org81473862012-06-27 17:30:56 +0000452 # 'git rev-parse foo' where foo is *any* 40 character hex string will return
453 # the string and return code 0. So strip one character to force 'git
454 # rev-parse' to do a hash table look-up and returns 128 if the hash is not
455 # present.
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000456 lookup_rev = rev
maruel@chromium.org81473862012-06-27 17:30:56 +0000457 if re.match(r'^[0-9a-fA-F]{40}$', rev):
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000458 lookup_rev = rev[:-1]
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000459 try:
ilevy@chromium.org224ba242013-07-08 22:02:31 +0000460 sha = GIT.Capture(['rev-parse', lookup_rev], cwd=cwd).lower()
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000461 if lookup_rev != rev:
462 # Make sure we get the original 40 chars back.
ilevy@chromium.org224ba242013-07-08 22:02:31 +0000463 return rev.lower() == sha
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000464 if sha_only:
ilevy@chromium.org224ba242013-07-08 22:02:31 +0000465 return sha.startswith(rev.lower())
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000466 return True
467 except subprocess2.CalledProcessError:
468 return False
469
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000470 @classmethod
471 def AssertVersion(cls, min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000472 """Asserts git's version is at least min_version."""
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000473 if cls.current_version is None:
bashi@chromium.orgfcffd482012-02-24 01:47:00 +0000474 current_version = cls.Capture(['--version'], '.')
475 matched = re.search(r'version ([0-9\.]+)', current_version)
476 cls.current_version = matched.group(1)
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000477 current_version_list = map(only_int, cls.current_version.split('.'))
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000478 for min_ver in map(int, min_version.split('.')):
479 ver = current_version_list.pop(0)
480 if ver < min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000481 return (False, cls.current_version)
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000482 elif ver > min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000483 return (True, cls.current_version)
484 return (True, cls.current_version)
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000485
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000486
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000487class SVN(object):
tony@chromium.org57564662010-04-14 02:35:12 +0000488 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000489
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000490 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000491 def Capture(args, cwd, **kwargs):
maruel@chromium.org54019f32010-09-09 13:50:11 +0000492 """Always redirect stderr.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000493
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000494 Throws an exception if non-0 is returned.
495 """
maruel@chromium.org904af082011-09-08 22:06:09 +0000496 return subprocess2.check_output(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000497 ['svn'] + args, stderr=subprocess2.PIPE, cwd=cwd, **kwargs)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000498
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000499 @staticmethod
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000500 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000501 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000502
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000503 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000504
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000505 svn's stdout is parsed to collect a list of files checked out or updated.
506 These files are appended to file_list. svn's stdout is also printed to
507 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000508
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000509 Args:
maruel@chromium.org03807072010-08-16 17:18:44 +0000510 verbose: If True, uses verbose output
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000511 args: A sequence of command line parameters to be passed to svn.
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000512 cwd: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000513
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000514 Raises:
515 Error: An error occurred while running the svn command.
516 """
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000517 stdout = stdout or sys.stdout
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000518 if file_list is None:
519 # Even if our caller doesn't care about file_list, we use it internally.
520 file_list = []
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000521
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000522 # svn update and svn checkout use the same pattern: the first three columns
523 # are for file status, property status, and lock status. This is followed
524 # by two spaces, and then the path to the file.
525 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000526
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000527 # The first three columns of svn status are the same as for svn update and
528 # svn checkout. The next three columns indicate addition-with-history,
529 # switch, and remote lock status. This is followed by one space, and then
530 # the path to the file.
531 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000532
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000533 # args[0] must be a supported command. This will blow up if it's something
534 # else, which is good. Note that the patterns are only effective when
535 # these commands are used in their ordinary forms, the patterns are invalid
536 # for "svn status --show-updates", for example.
537 pattern = {
538 'checkout': update_pattern,
539 'status': status_pattern,
540 'update': update_pattern,
541 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000542 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000543 # Place an upper limit.
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000544 backoff_time = 5
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000545 retries = 0
maruel@chromium.org03507062010-10-26 00:58:27 +0000546 while True:
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000547 retries += 1
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000548 previous_list_len = len(file_list)
549 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000550
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000551 def CaptureMatchingLines(line):
552 match = compiled_pattern.search(line)
553 if match:
554 file_list.append(match.group(1))
555 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000556 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000557
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000558 try:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000559 gclient_utils.CheckCallAndFilterAndHeader(
560 ['svn'] + args,
561 cwd=cwd,
562 always=verbose,
563 filter_fn=CaptureMatchingLines,
564 stdout=stdout)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000565 except subprocess2.CalledProcessError:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000566 def IsKnownFailure():
567 for x in failure:
568 if (x.startswith('svn: OPTIONS of') or
569 x.startswith('svn: PROPFIND of') or
570 x.startswith('svn: REPORT of') or
maruel@chromium.orgf61fc932010-08-19 13:05:24 +0000571 x.startswith('svn: Unknown hostname') or
maruel@chromium.org7d8b97d2011-10-11 23:32:30 +0000572 x.startswith('svn: Server sent unexpected return value') or
573 x.startswith('svn: Can\'t connect to host')):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000574 return True
575 return False
576
maruel@chromium.org953586a2010-06-15 14:22:24 +0000577 # Subversion client is really misbehaving with Google Code.
578 if args[0] == 'checkout':
579 # Ensure at least one file was checked out, otherwise *delete* the
580 # directory.
581 if len(file_list) == previous_list_len:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000582 if not IsKnownFailure():
maruel@chromium.org953586a2010-06-15 14:22:24 +0000583 # No known svn error was found, bail out.
584 raise
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000585 # No file were checked out, so make sure the directory is
586 # deleted in case it's messed up and try again.
587 # Warning: It's bad, it assumes args[2] is the directory
588 # argument.
589 if os.path.isdir(args[2]):
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000590 gclient_utils.rmtree(args[2])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000591 else:
592 # Progress was made, convert to update since an aborted checkout
593 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000594 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000595 else:
596 # It was an update or export.
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000597 # We enforce that some progress has been made or a known failure.
598 if len(file_list) == previous_list_len and not IsKnownFailure():
599 # No known svn error was found and no progress, bail out.
600 raise
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000601 if retries == 10:
maruel@chromium.org03507062010-10-26 00:58:27 +0000602 raise
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000603 print "Sleeping %.1f seconds and retrying...." % backoff_time
604 time.sleep(backoff_time)
605 backoff_time *= 1.3
maruel@chromium.org953586a2010-06-15 14:22:24 +0000606 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000607 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000608
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000609 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000610 def CaptureRemoteInfo(url):
611 """Returns a dictionary from the svn info output for the given url.
612
613 Throws an exception if svn info fails.
614 """
615 assert isinstance(url, str)
616 return SVN._CaptureInfo([url], None)
617
618 @staticmethod
619 def CaptureLocalInfo(files, cwd):
620 """Returns a dictionary from the svn info output for the given files.
621
622 Throws an exception if svn info fails.
623 """
624 assert isinstance(files, (list, tuple))
625 return SVN._CaptureInfo(files, cwd)
626
627 @staticmethod
628 def _CaptureInfo(files, cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000629 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000630
maruel@chromium.org54019f32010-09-09 13:50:11 +0000631 Throws an exception if svn info fails."""
maruel@chromium.orgd25fb8f2011-04-07 13:40:15 +0000632 result = {}
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000633 info = ElementTree.XML(SVN.Capture(['info', '--xml'] + files, cwd))
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000634 if info is None:
635 return result
636 entry = info.find('entry')
maruel@chromium.org6f323bb2011-04-26 15:42:53 +0000637 if entry is None:
638 return result
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000639
640 # Use .text when the item is not optional.
641 result['Path'] = entry.attrib['path']
maruel@chromium.org7d654672012-01-05 19:07:23 +0000642 rev = entry.attrib['revision']
643 try:
644 result['Revision'] = int(rev)
645 except ValueError:
646 result['Revision'] = None
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000647 result['Node Kind'] = entry.attrib['kind']
648 # Differs across versions.
649 if result['Node Kind'] == 'dir':
650 result['Node Kind'] = 'directory'
651 result['URL'] = entry.find('url').text
652 repository = entry.find('repository')
653 result['Repository Root'] = repository.find('root').text
654 result['UUID'] = repository.find('uuid')
655 wc_info = entry.find('wc-info')
656 if wc_info is not None:
657 result['Schedule'] = wc_info.find('schedule').text
658 result['Copied From URL'] = wc_info.find('copy-from-url')
659 result['Copied From Rev'] = wc_info.find('copy-from-rev')
660 else:
661 result['Schedule'] = None
662 result['Copied From URL'] = None
663 result['Copied From Rev'] = None
664 for key in result.keys():
665 if isinstance(result[key], unicode):
666 # Unicode results interferes with the higher layers matching up things
667 # in the deps dictionary.
668 result[key] = result[key].encode()
669 # Automatic conversion of optional parameters.
670 result[key] = getattr(result[key], 'text', result[key])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000671 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000672
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000673 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000674 def CaptureRevision(cwd):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000675 """Get the base revision of a SVN repository.
676
677 Returns:
678 Int base revision
679 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000680 return SVN.CaptureLocalInfo([], cwd).get('Revision')
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000681
682 @staticmethod
maruel@chromium.orgea15cb72012-05-04 14:16:31 +0000683 def CaptureStatus(files, cwd, no_ignore=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000684 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000685
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000686 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000687
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000688 Returns an array of (status, file) tuples."""
689 command = ["status", "--xml"]
maruel@chromium.orgea15cb72012-05-04 14:16:31 +0000690 if no_ignore:
691 command.append('--no-ignore')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000692 if not files:
693 pass
694 elif isinstance(files, basestring):
695 command.append(files)
696 else:
697 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000698
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000699 status_letter = {
700 None: ' ',
701 '': ' ',
702 'added': 'A',
703 'conflicted': 'C',
704 'deleted': 'D',
705 'external': 'X',
706 'ignored': 'I',
707 'incomplete': '!',
708 'merged': 'G',
709 'missing': '!',
710 'modified': 'M',
711 'none': ' ',
712 'normal': ' ',
713 'obstructed': '~',
714 'replaced': 'R',
715 'unversioned': '?',
716 }
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000717 dom = ElementTree.XML(SVN.Capture(command, cwd))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000718 results = []
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000719 if dom is None:
720 return results
721 # /status/target/entry/(wc-status|commit|author|date)
722 for target in dom.findall('target'):
723 for entry in target.findall('entry'):
724 file_path = entry.attrib['path']
725 wc_status = entry.find('wc-status')
726 # Emulate svn 1.5 status ouput...
727 statuses = [' '] * 7
728 # Col 0
729 xml_item_status = wc_status.attrib['item']
730 if xml_item_status in status_letter:
731 statuses[0] = status_letter[xml_item_status]
732 else:
733 raise gclient_utils.Error(
734 'Unknown item status "%s"; please implement me!' %
735 xml_item_status)
736 # Col 1
737 xml_props_status = wc_status.attrib['props']
738 if xml_props_status == 'modified':
739 statuses[1] = 'M'
740 elif xml_props_status == 'conflicted':
741 statuses[1] = 'C'
742 elif (not xml_props_status or xml_props_status == 'none' or
743 xml_props_status == 'normal'):
744 pass
745 else:
746 raise gclient_utils.Error(
747 'Unknown props status "%s"; please implement me!' %
748 xml_props_status)
749 # Col 2
750 if wc_status.attrib.get('wc-locked') == 'true':
751 statuses[2] = 'L'
752 # Col 3
753 if wc_status.attrib.get('copied') == 'true':
754 statuses[3] = '+'
755 # Col 4
756 if wc_status.attrib.get('switched') == 'true':
757 statuses[4] = 'S'
758 # TODO(maruel): Col 5 and 6
759 item = (''.join(statuses), file_path)
760 results.append(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000761 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000762
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000763 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000764 def IsMoved(filename, cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000765 """Determine if a file has been added through svn mv"""
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000766 assert isinstance(filename, basestring)
767 return SVN.IsMovedInfo(SVN.CaptureLocalInfo([filename], cwd))
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000768
769 @staticmethod
770 def IsMovedInfo(info):
771 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000772 return (info.get('Copied From URL') and
773 info.get('Copied From Rev') and
774 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000775
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000776 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000777 def GetFileProperty(filename, property_name, cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000778 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000779
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000780 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000781 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000782 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000783
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000784 Returns:
785 The value of the property, which will be the empty string if the property
786 is not set on the file. If the file is not under version control, the
787 empty string is also returned.
788 """
maruel@chromium.org54019f32010-09-09 13:50:11 +0000789 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000790 return SVN.Capture(['propget', property_name, filename], cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000791 except subprocess2.CalledProcessError:
maruel@chromium.org54019f32010-09-09 13:50:11 +0000792 return ''
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000793
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000794 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000795 def GenerateDiff(filenames, cwd, full_move, revision):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000796 """Returns a string containing the diff for the given file list.
797
798 The files in the list should either be absolute paths or relative to the
799 given root. If no root directory is provided, the repository root will be
800 used.
801 The diff will always use relative paths.
802 """
maruel@chromium.org00fdcb32011-02-24 01:41:02 +0000803 assert isinstance(filenames, (list, tuple))
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000804 # If the user specified a custom diff command in their svn config file,
805 # then it'll be used when we do svn diff, which we don't want to happen
806 # since we want the unified diff.
807 if SVN.AssertVersion("1.7")[0]:
808 # On svn >= 1.7, the "--internal-diff" flag will solve this.
809 return SVN._GenerateDiffInternal(filenames, cwd, full_move, revision,
810 ["diff", "--internal-diff"],
811 ["diff", "--internal-diff"])
812 else:
813 # On svn < 1.7, the "--internal-diff" flag doesn't exist. Using
814 # --diff-cmd=diff doesn't always work, since e.g. Windows cmd users may
815 # not have a "diff" executable in their path at all. So we use an empty
816 # temporary directory as the config directory, which bypasses any user
817 # settings for the diff-cmd. However, we don't pass this for the
818 # remote_safe_diff_command parameter, since when a new config-dir is
819 # specified for an svn diff against a remote URL, it triggers
820 # authentication prompts. In this case there isn't really a good
821 # alternative to svn 1.7's --internal-diff flag.
822 bogus_dir = tempfile.mkdtemp()
823 try:
824 return SVN._GenerateDiffInternal(filenames, cwd, full_move, revision,
825 ["diff", "--config-dir", bogus_dir],
826 ["diff"])
827 finally:
828 gclient_utils.rmtree(bogus_dir)
829
830 @staticmethod
831 def _GenerateDiffInternal(filenames, cwd, full_move, revision, diff_command,
832 remote_safe_diff_command):
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000833 root = os.path.normcase(os.path.join(cwd, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000834 def RelativePath(path, root):
835 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000836 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000837 return path[len(root):]
838 return path
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000839 # Cleanup filenames
840 filenames = [RelativePath(f, root) for f in filenames]
841 # Get information about the modified items (files and directories)
842 data = dict((f, SVN.CaptureLocalInfo([f], root)) for f in filenames)
843 diffs = []
844 if full_move:
845 # Eliminate modified files inside moved/copied directory.
846 for (filename, info) in data.iteritems():
847 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
848 # Remove files inside the directory.
849 filenames = [f for f in filenames
850 if not f.startswith(filename + os.path.sep)]
851 for filename in data.keys():
852 if not filename in filenames:
853 # Remove filtered out items.
854 del data[filename]
855 else:
856 metaheaders = []
857 for (filename, info) in data.iteritems():
858 if SVN.IsMovedInfo(info):
859 # for now, the most common case is a head copy,
860 # so let's just encode that as a straight up cp.
861 srcurl = info.get('Copied From URL')
862 file_root = info.get('Repository Root')
863 rev = int(info.get('Copied From Rev'))
864 assert srcurl.startswith(file_root)
865 src = srcurl[len(file_root)+1:]
866 try:
867 srcinfo = SVN.CaptureRemoteInfo(srcurl)
868 except subprocess2.CalledProcessError, e:
869 if not 'Not a valid URL' in e.stderr:
870 raise
871 # Assume the file was deleted. No idea how to figure out at which
872 # revision the file was deleted.
873 srcinfo = {'Revision': rev}
874 if (srcinfo.get('Revision') != rev and
875 SVN.Capture(remote_safe_diff_command + ['-r', '%d:head' % rev,
876 srcurl], cwd)):
877 metaheaders.append("#$ svn cp -r %d %s %s "
878 "### WARNING: note non-trunk copy\n" %
879 (rev, src, filename))
880 else:
881 metaheaders.append("#$ cp %s %s\n" % (src,
882 filename))
883 if metaheaders:
884 diffs.append("### BEGIN SVN COPY METADATA\n")
885 diffs.extend(metaheaders)
886 diffs.append("### END SVN COPY METADATA\n")
887 # Now ready to do the actual diff.
888 for filename in sorted(data):
889 diffs.append(SVN._DiffItemInternal(
890 filename, cwd, data[filename], diff_command, full_move, revision))
891 # Use StringIO since it can be messy when diffing a directory move with
892 # full_move=True.
893 buf = cStringIO.StringIO()
894 for d in filter(None, diffs):
895 buf.write(d)
896 result = buf.getvalue()
897 buf.close()
898 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000899
900 @staticmethod
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000901 def _DiffItemInternal(filename, cwd, info, diff_command, full_move, revision):
pkasting@chromium.org917ea7f2013-04-19 20:10:03 +0000902 """Grabs the diff data."""
pkasting@chromium.org0db557c2013-04-24 23:44:23 +0000903 command = diff_command + [filename]
pkasting@chromium.org917ea7f2013-04-19 20:10:03 +0000904 if revision:
905 command.extend(['--revision', revision])
906 data = None
907 if SVN.IsMovedInfo(info):
908 if full_move:
909 if info.get("Node Kind") == "directory":
910 # Things become tricky here. It's a directory copy/move. We need to
911 # diff all the files inside it.
912 # This will put a lot of pressure on the heap. This is why StringIO
913 # is used and converted back into a string at the end. The reason to
914 # return a string instead of a StringIO is that StringIO.write()
915 # doesn't accept a StringIO object. *sigh*.
916 for (dirpath, dirnames, filenames) in os.walk(filename):
917 # Cleanup all files starting with a '.'.
918 for d in dirnames:
919 if d.startswith('.'):
920 dirnames.remove(d)
921 for f in filenames:
922 if f.startswith('.'):
923 filenames.remove(f)
924 for f in filenames:
925 if data is None:
926 data = cStringIO.StringIO()
927 data.write(GenFakeDiff(os.path.join(dirpath, f)))
928 if data:
929 tmp = data.getvalue()
930 data.close()
931 data = tmp
932 else:
933 data = GenFakeDiff(filename)
934 else:
935 if info.get("Node Kind") != "directory":
936 # svn diff on a mv/cp'd file outputs nothing if there was no change.
937 data = SVN.Capture(command, cwd)
938 if not data:
939 # We put in an empty Index entry so upload.py knows about them.
940 data = "Index: %s\n" % filename.replace(os.sep, '/')
941 # Otherwise silently ignore directories.
942 else:
943 if info.get("Node Kind") != "directory":
944 # Normal simple case.
945 try:
946 data = SVN.Capture(command, cwd)
947 except subprocess2.CalledProcessError:
948 if revision:
949 data = GenFakeDiff(filename)
950 else:
951 raise
952 # Otherwise silently ignore directories.
953 return data
954
955 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000956 def GetEmail(cwd):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000957 """Retrieves the svn account which we assume is an email address."""
maruel@chromium.org54019f32010-09-09 13:50:11 +0000958 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000959 infos = SVN.CaptureLocalInfo([], cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000960 except subprocess2.CalledProcessError:
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000961 return None
962
963 # Should check for uuid but it is incorrectly saved for https creds.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000964 root = infos['Repository Root']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000965 realm = root.rsplit('/', 1)[0]
maruel@chromium.org54019f32010-09-09 13:50:11 +0000966 uuid = infos['UUID']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000967 if root.startswith('https') or not uuid:
968 regexp = re.compile(r'<%s:\d+>.*' % realm)
969 else:
970 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
971 if regexp is None:
972 return None
973 if sys.platform.startswith('win'):
974 if not 'APPDATA' in os.environ:
975 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000976 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
977 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000978 else:
979 if not 'HOME' in os.environ:
980 return None
981 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
982 'svn.simple')
983 for credfile in os.listdir(auth_dir):
984 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
985 if regexp.match(cred_info.get('svn:realmstring')):
986 return cred_info.get('username')
987
988 @staticmethod
989 def ReadSimpleAuth(filename):
990 f = open(filename, 'r')
991 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000992 def ReadOneItem(item_type):
993 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000994 if not m:
995 return None
996 data = f.read(int(m.group(1)))
997 if f.read(1) != '\n':
998 return None
999 return data
1000
1001 while True:
1002 key = ReadOneItem('K')
1003 if not key:
1004 break
1005 value = ReadOneItem('V')
1006 if not value:
1007 break
1008 values[key] = value
1009 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001010
1011 @staticmethod
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001012 def GetCheckoutRoot(cwd):
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001013 """Returns the top level directory of the current repository.
1014
1015 The directory is returned as an absolute path.
1016 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001017 cwd = os.path.abspath(cwd)
maruel@chromium.org54019f32010-09-09 13:50:11 +00001018 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001019 info = SVN.CaptureLocalInfo([], cwd)
maruel@chromium.org885d6e82011-02-24 20:21:46 +00001020 cur_dir_repo_root = info['Repository Root']
1021 url = info['URL']
maruel@chromium.orgda64d632011-09-08 17:41:15 +00001022 except subprocess2.CalledProcessError:
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001023 return None
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001024 while True:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001025 parent = os.path.dirname(cwd)
maruel@chromium.org54019f32010-09-09 13:50:11 +00001026 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001027 info = SVN.CaptureLocalInfo([], parent)
maruel@chromium.org885d6e82011-02-24 20:21:46 +00001028 if (info['Repository Root'] != cur_dir_repo_root or
1029 info['URL'] != os.path.dirname(url)):
maruel@chromium.org54019f32010-09-09 13:50:11 +00001030 break
maruel@chromium.org885d6e82011-02-24 20:21:46 +00001031 url = info['URL']
maruel@chromium.orgda64d632011-09-08 17:41:15 +00001032 except subprocess2.CalledProcessError:
maruel@chromium.org94b1ee92009-12-19 20:27:20 +00001033 break
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001034 cwd = parent
1035 return GetCasedPath(cwd)
tony@chromium.org57564662010-04-14 02:35:12 +00001036
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +00001037 @staticmethod
1038 def IsValidRevision(url):
1039 """Verifies the revision looks like an SVN revision."""
1040 try:
1041 SVN.Capture(['info', url], cwd=None)
1042 return True
1043 except subprocess2.CalledProcessError:
1044 return False
1045
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001046 @classmethod
1047 def AssertVersion(cls, min_version):
tony@chromium.org57564662010-04-14 02:35:12 +00001048 """Asserts svn's version is at least min_version."""
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001049 if cls.current_version is None:
shouqun.liu@intel.com13b522c2012-07-20 17:16:51 +00001050 cls.current_version = cls.Capture(['--version', '--quiet'], None)
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001051 current_version_list = map(only_int, cls.current_version.split('.'))
tony@chromium.org57564662010-04-14 02:35:12 +00001052 for min_ver in map(int, min_version.split('.')):
1053 ver = current_version_list.pop(0)
1054 if ver < min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001055 return (False, cls.current_version)
tony@chromium.org57564662010-04-14 02:35:12 +00001056 elif ver > min_ver:
maruel@chromium.org36ac2392011-10-12 16:36:11 +00001057 return (True, cls.current_version)
1058 return (True, cls.current_version)
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001059
1060 @staticmethod
maruel@chromium.orgea15cb72012-05-04 14:16:31 +00001061 def Revert(cwd, callback=None, ignore_externals=False, no_ignore=False):
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001062 """Reverts all svn modifications in cwd, including properties.
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001063
1064 Deletes any modified files or directory.
1065
1066 A "svn update --revision BASE" call is required after to revive deleted
1067 files.
1068 """
maruel@chromium.orgea15cb72012-05-04 14:16:31 +00001069 for file_status in SVN.CaptureStatus(None, cwd, no_ignore=no_ignore):
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001070 file_path = os.path.join(cwd, file_status[1])
maruel@chromium.org8c415122011-03-15 17:14:27 +00001071 if (ignore_externals and
1072 file_status[0][0] == 'X' and
1073 file_status[0][1:].isspace()):
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001074 # Ignore externals.
1075 logging.info('Ignoring external %s' % file_status[1])
1076 continue
1077
maruel@chromium.org62087572012-04-24 23:16:28 +00001078 # This is the case where '! L .' is returned by 'svn status'. Just
1079 # strip off the '/.'.
1080 if file_path.endswith(os.path.sep + '.'):
1081 file_path = file_path[:-2]
1082
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001083 if callback:
1084 callback(file_status)
1085
maruel@chromium.org8c415122011-03-15 17:14:27 +00001086 if os.path.exists(file_path):
1087 # svn revert is really stupid. It fails on inconsistent line-endings,
1088 # on switched directories, etc. So take no chance and delete everything!
1089 # In theory, it wouldn't be necessary for property-only change but then
1090 # it'd have to look for switched directories, etc so it's not worth
1091 # optimizing this use case.
1092 if os.path.isfile(file_path) or os.path.islink(file_path):
1093 logging.info('os.remove(%s)' % file_path)
1094 os.remove(file_path)
1095 elif os.path.isdir(file_path):
digit@chromium.orgdc112ac2013-04-24 13:00:19 +00001096 logging.info('rmtree(%s)' % file_path)
1097 gclient_utils.rmtree(file_path)
maruel@chromium.org8c415122011-03-15 17:14:27 +00001098 else:
1099 logging.critical(
1100 ('No idea what is %s.\nYou just found a bug in gclient'
1101 ', please ping maruel@chromium.org ASAP!') % file_path)
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001102
maruel@chromium.org8c415122011-03-15 17:14:27 +00001103 if (file_status[0][0] in ('D', 'A', '!') or
1104 not file_status[0][1:].isspace()):
maruel@chromium.orgaf453492011-03-03 21:04:09 +00001105 # Added, deleted file requires manual intervention and require calling
maruel@chromium.org07ab60e2011-02-08 21:54:00 +00001106 # revert, like for properties.
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001107 if not os.path.isdir(cwd):
maruel@chromium.org8b322b32011-11-01 19:05:50 +00001108 # '.' was deleted. It's not worth continuing.
1109 return
maruel@chromium.orgaf453492011-03-03 21:04:09 +00001110 try:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +00001111 SVN.Capture(['revert', file_status[1]], cwd=cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +00001112 except subprocess2.CalledProcessError:
maruel@chromium.orgaf453492011-03-03 21:04:09 +00001113 if not os.path.exists(file_path):
1114 continue
1115 raise