blob: c47a00c2466847ee0a4483db83aca31ec5a5e5e5 [file] [log] [blame]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00001# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
2# 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.orgfd9cbbb2010-01-08 23:04:03 +00007import glob
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00008import os
9import re
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000010import shutil
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000011import subprocess
12import sys
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000013import tempfile
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000014import xml.dom.minidom
15
16import gclient_utils
17
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000018def ValidateEmail(email):
19 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
20 is not None)
21
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000022
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000023def GetCasedPath(path):
24 """Elcheapos way to get the real path case on Windows."""
25 if sys.platform.startswith('win') and os.path.exists(path):
26 # Reconstruct the path.
27 path = os.path.abspath(path)
28 paths = path.split('\\')
29 for i in range(len(paths)):
30 if i == 0:
31 # Skip drive letter.
32 continue
33 subpath = '\\'.join(paths[:i+1])
34 prev = len('\\'.join(paths[:i]))
35 # glob.glob will return the cased path for the last item only. This is why
36 # we are calling it in a loop. Extract the data we want and put it back
37 # into the list.
38 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)]
39 path = '\\'.join(paths)
40 return path
41
42
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000043class GIT(object):
44 COMMAND = "git"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000045
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000046 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000047 def Capture(args, in_directory=None, print_error=True, error_ok=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000048 """Runs git, capturing output sent to stdout as a string.
49
50 Args:
51 args: A sequence of command line parameters to be passed to git.
52 in_directory: The directory where git is to be run.
53
54 Returns:
55 The output sent to stdout as a string.
56 """
57 c = [GIT.COMMAND]
58 c.extend(args)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000059 try:
60 return gclient_utils.CheckCall(c, in_directory, print_error)
61 except gclient_utils.CheckCallError:
62 if error_ok:
63 return ''
64 raise
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000065
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000066 @staticmethod
67 def CaptureStatus(files, upstream_branch='origin'):
68 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000069
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000070 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000071
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000072 Returns an array of (status, file) tuples."""
73 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch]
74 if not files:
75 pass
76 elif isinstance(files, basestring):
77 command.append(files)
78 else:
79 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000080
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000081 status = GIT.Capture(command).rstrip()
82 results = []
83 if status:
84 for statusline in status.split('\n'):
85 m = re.match('^(\w)\t(.+)$', statusline)
86 if not m:
87 raise Exception("status currently unsupported: %s" % statusline)
88 results.append(('%s ' % m.group(1), m.group(2)))
89 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000090
maruel@chromium.orgc78f2462009-11-21 01:20:57 +000091 @staticmethod
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000092 def RunAndFilterOutput(args,
93 in_directory,
94 print_messages,
95 print_stdout,
96 filter):
97 """Runs a command, optionally outputting to stdout.
98
99 stdout is passed line-by-line to the given filter function. If
100 print_stdout is true, it is also printed to sys.stdout as in Run.
101
102 Args:
103 args: A sequence of command line parameters to be passed.
msb@chromium.orgd6504212010-01-13 17:34:31 +0000104 in_directory: The directory where git is to be run.
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000105 print_messages: Whether to print status messages to stdout about
106 which commands are being run.
107 print_stdout: Whether to forward program's output to stdout.
108 filter: A function taking one argument (a string) which will be
109 passed each line (with the ending newline character removed) of
110 program's output for filtering.
111
112 Raises:
113 gclient_utils.Error: An error occurred while running the command.
114 """
115 command = [GIT.COMMAND]
116 command.extend(args)
117 gclient_utils.SubprocessCallAndFilter(command,
118 in_directory,
119 print_messages,
120 print_stdout,
121 filter=filter)
122
123 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000124 def GetEmail(repo_root):
125 """Retrieves the user email address if known."""
126 # We could want to look at the svn cred when it has a svn remote but it
127 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000128 return GIT.Capture(['config', 'user.email'],
129 repo_root, error_ok=True).strip()
130
131 @staticmethod
132 def ShortBranchName(branch):
133 """Converts a name like 'refs/heads/foo' to just 'foo'."""
134 return branch.replace('refs/heads/', '')
135
136 @staticmethod
137 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000138 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000139 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd).strip()
140
141 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000142 def GetBranch(cwd):
143 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000144 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000145
146 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000147 def IsGitSvn(cwd):
148 """Returns true if this repo looks like it's using git-svn."""
149 # If you have any "svn-remote.*" config keys, we think you're using svn.
150 try:
151 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd)
152 return True
153 except gclient_utils.CheckCallError:
154 return False
155
156 @staticmethod
157 def GetSVNBranch(cwd):
158 """Returns the svn branch name if found."""
159 # Try to figure out which remote branch we're based on.
160 # Strategy:
161 # 1) find all git-svn branches and note their svn URLs.
162 # 2) iterate through our branch history and match up the URLs.
163
164 # regexp matching the git-svn line that contains the URL.
165 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
166
167 # Get the refname and svn url for all refs/remotes/*.
168 remotes = GIT.Capture(
169 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
170 cwd).splitlines()
171 svn_refs = {}
172 for ref in remotes:
173 match = git_svn_re.search(
174 GIT.Capture(['cat-file', '-p', ref], cwd))
175 if match:
176 svn_refs[match.group(1)] = ref
177
178 svn_branch = ''
179 if len(svn_refs) == 1:
180 # Only one svn branch exists -- seems like a good candidate.
181 svn_branch = svn_refs.values()[0]
182 elif len(svn_refs) > 1:
183 # We have more than one remote branch available. We don't
184 # want to go through all of history, so read a line from the
185 # pipe at a time.
186 # The -100 is an arbitrary limit so we don't search forever.
187 cmd = ['git', 'log', '-100', '--pretty=medium']
188 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
189 for line in proc.stdout:
190 match = git_svn_re.match(line)
191 if match:
192 url = match.group(1)
193 if url in svn_refs:
194 svn_branch = svn_refs[url]
195 proc.stdout.close() # Cut pipe.
196 break
197 return svn_branch
198
199 @staticmethod
200 def FetchUpstreamTuple(cwd):
201 """Returns a tuple containg remote and remote ref,
202 e.g. 'origin', 'refs/heads/master'
203 """
204 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000205 branch = GIT.GetBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000206 upstream_branch = None
207 upstream_branch = GIT.Capture(
208 ['config', 'branch.%s.merge' % branch], error_ok=True).strip()
209 if upstream_branch:
210 remote = GIT.Capture(
211 ['config', 'branch.%s.remote' % branch],
212 error_ok=True).strip()
213 else:
214 # Fall back on trying a git-svn upstream branch.
215 if GIT.IsGitSvn(cwd):
216 upstream_branch = GIT.GetSVNBranch(cwd)
217 # Fall back on origin/master if it exits.
218 if not upstream_branch:
219 GIT.Capture(['branch', '-r']).split().count('origin/master')
220 remote = 'origin'
221 upstream_branch = 'refs/heads/master'
222 return remote, upstream_branch
223
224 @staticmethod
225 def GetUpstream(cwd):
226 """Gets the current branch's upstream branch."""
227 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
228 if remote is not '.':
229 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
230 return upstream_branch
231
232 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000233 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
234 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000235 """Diffs against the upstream branch or optionally another branch.
236
237 full_move means that move or copy operations should completely recreate the
238 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000239 if not branch:
240 branch = GIT.GetUpstream(cwd)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000241 command = ['diff-tree', '-p', '--no-prefix', branch, branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000242 if not full_move:
243 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000244 # TODO(maruel): --binary support.
245 if files:
246 command.append('--')
247 command.extend(files)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000248 diff = GIT.Capture(command, cwd).splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000249 for i in range(len(diff)):
250 # In the case of added files, replace /dev/null with the path to the
251 # file being added.
252 if diff[i].startswith('--- /dev/null'):
253 diff[i] = '--- %s' % diff[i+1][4:]
254 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000255
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000256 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000257 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
258 """Returns the list of modified files between two branches."""
259 if not branch:
260 branch = GIT.GetUpstream(cwd)
261 command = ['diff', '--name-only', branch, branch_head]
262 return GIT.Capture(command, cwd).splitlines(False)
263
264 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000265 def GetPatchName(cwd):
266 """Constructs a name for this patch."""
267 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd).strip()
268 return "%s-%s" % (GIT.GetBranch(cwd), short_sha)
269
270 @staticmethod
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000271 def GetCheckoutRoot(path):
272 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000273 """
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000274 root = GIT.Capture(['rev-parse', '--show-cdup'], path).strip()
275 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000276
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000277
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000278class SVN(object):
279 COMMAND = "svn"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000280
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000281 @staticmethod
282 def Run(args, in_directory):
283 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000284
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000285 Args:
286 args: A sequence of command line parameters to be passed to svn.
287 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000288
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000289 Raises:
290 Error: An error occurred while running the svn command.
291 """
292 c = [SVN.COMMAND]
293 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000294 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000295 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000296
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000297 @staticmethod
298 def Capture(args, in_directory=None, print_error=True):
299 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000300
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000301 Args:
302 args: A sequence of command line parameters to be passed to svn.
303 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000304
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000305 Returns:
306 The output sent to stdout as a string.
307 """
308 c = [SVN.COMMAND]
309 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000310
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000311 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
312 # the svn.exe executable, but shell=True makes subprocess on Linux fail
313 # when it's called with a list because it only tries to execute the
314 # first string ("svn").
315 stderr = None
316 if not print_error:
317 stderr = subprocess.PIPE
318 return subprocess.Popen(c,
319 cwd=in_directory,
320 shell=(sys.platform == 'win32'),
321 stdout=subprocess.PIPE,
322 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000323
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000324 @staticmethod
325 def RunAndGetFileList(options, args, in_directory, file_list):
326 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000327
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000328 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000329
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000330 svn's stdout is parsed to collect a list of files checked out or updated.
331 These files are appended to file_list. svn's stdout is also printed to
332 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000333
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000334 Args:
335 options: command line options to gclient
336 args: A sequence of command line parameters to be passed to svn.
337 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000338
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000339 Raises:
340 Error: An error occurred while running the svn command.
341 """
342 command = [SVN.COMMAND]
343 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000344
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000345 # svn update and svn checkout use the same pattern: the first three columns
346 # are for file status, property status, and lock status. This is followed
347 # by two spaces, and then the path to the file.
348 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000349
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000350 # The first three columns of svn status are the same as for svn update and
351 # svn checkout. The next three columns indicate addition-with-history,
352 # switch, and remote lock status. This is followed by one space, and then
353 # the path to the file.
354 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000355
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000356 # args[0] must be a supported command. This will blow up if it's something
357 # else, which is good. Note that the patterns are only effective when
358 # these commands are used in their ordinary forms, the patterns are invalid
359 # for "svn status --show-updates", for example.
360 pattern = {
361 'checkout': update_pattern,
362 'status': status_pattern,
363 'update': update_pattern,
364 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000365 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000366 # Place an upper limit.
367 for i in range(1, 10):
368 previous_list_len = len(file_list)
369 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000370
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000371 def CaptureMatchingLines(line):
372 match = compiled_pattern.search(line)
373 if match:
374 file_list.append(match.group(1))
375 if line.startswith('svn: '):
376 # We can't raise an exception. We can't alias a variable. Use a cheap
377 # way.
378 failure.append(True)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000379
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000380 try:
381 SVN.RunAndFilterOutput(args,
382 in_directory,
383 options.verbose,
384 True,
385 CaptureMatchingLines)
386 except gclient_utils.Error:
387 # We enforce that some progress has been made.
388 if len(failure) and len(file_list) > previous_list_len:
389 if args[0] == 'checkout':
390 args = args[:]
391 # An aborted checkout is now an update.
392 args[0] = 'update'
393 continue
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000394 # No progress was made, bail out.
395 raise
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000396 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000397
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000398 @staticmethod
399 def RunAndFilterOutput(args,
400 in_directory,
401 print_messages,
402 print_stdout,
403 filter):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000404 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000405
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000406 stdout is passed line-by-line to the given filter function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000407 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000408
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000409 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000410 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000411 in_directory: The directory where svn is to be run.
412 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000413 which commands are being run.
414 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000415 filter: A function taking one argument (a string) which will be
416 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000417 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000418
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000419 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000420 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000421 """
422 command = [SVN.COMMAND]
423 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000424 gclient_utils.SubprocessCallAndFilter(command,
425 in_directory,
426 print_messages,
427 print_stdout,
428 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000429
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000430 @staticmethod
431 def CaptureInfo(relpath, in_directory=None, print_error=True):
432 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000433
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000434 Args:
435 relpath: The directory where the working copy resides relative to
436 the directory given by in_directory.
437 in_directory: The directory where svn is to be run.
438 """
439 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
440 dom = gclient_utils.ParseXML(output)
441 result = {}
442 if dom:
443 GetNamedNodeText = gclient_utils.GetNamedNodeText
444 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
445 def C(item, f):
446 if item is not None: return f(item)
447 # /info/entry/
448 # url
449 # reposityory/(root|uuid)
450 # wc-info/(schedule|depth)
451 # commit/(author|date)
452 # str() the results because they may be returned as Unicode, which
453 # interferes with the higher layers matching up things in the deps
454 # dictionary.
455 # TODO(maruel): Fix at higher level instead (!)
456 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
457 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
458 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
459 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
460 'revision'),
461 int)
462 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
463 str)
464 # Differs across versions.
465 if result['Node Kind'] == 'dir':
466 result['Node Kind'] = 'directory'
467 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
468 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
469 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
470 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
471 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000472
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000473 @staticmethod
474 def CaptureHeadRevision(url):
475 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000476
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000477 Returns:
478 Int head revision
479 """
480 info = SVN.Capture(["info", "--xml", url], os.getcwd())
481 dom = xml.dom.minidom.parseString(info)
482 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000483
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000484 @staticmethod
485 def CaptureStatus(files):
486 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000487
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000488 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000489
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000490 Returns an array of (status, file) tuples."""
491 command = ["status", "--xml"]
492 if not files:
493 pass
494 elif isinstance(files, basestring):
495 command.append(files)
496 else:
497 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000498
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000499 status_letter = {
500 None: ' ',
501 '': ' ',
502 'added': 'A',
503 'conflicted': 'C',
504 'deleted': 'D',
505 'external': 'X',
506 'ignored': 'I',
507 'incomplete': '!',
508 'merged': 'G',
509 'missing': '!',
510 'modified': 'M',
511 'none': ' ',
512 'normal': ' ',
513 'obstructed': '~',
514 'replaced': 'R',
515 'unversioned': '?',
516 }
517 dom = gclient_utils.ParseXML(SVN.Capture(command))
518 results = []
519 if dom:
520 # /status/target/entry/(wc-status|commit|author|date)
521 for target in dom.getElementsByTagName('target'):
522 #base_path = target.getAttribute('path')
523 for entry in target.getElementsByTagName('entry'):
524 file_path = entry.getAttribute('path')
525 wc_status = entry.getElementsByTagName('wc-status')
526 assert len(wc_status) == 1
527 # Emulate svn 1.5 status ouput...
528 statuses = [' '] * 7
529 # Col 0
530 xml_item_status = wc_status[0].getAttribute('item')
531 if xml_item_status in status_letter:
532 statuses[0] = status_letter[xml_item_status]
533 else:
534 raise Exception('Unknown item status "%s"; please implement me!' %
535 xml_item_status)
536 # Col 1
537 xml_props_status = wc_status[0].getAttribute('props')
538 if xml_props_status == 'modified':
539 statuses[1] = 'M'
540 elif xml_props_status == 'conflicted':
541 statuses[1] = 'C'
542 elif (not xml_props_status or xml_props_status == 'none' or
543 xml_props_status == 'normal'):
544 pass
545 else:
546 raise Exception('Unknown props status "%s"; please implement me!' %
547 xml_props_status)
548 # Col 2
549 if wc_status[0].getAttribute('wc-locked') == 'true':
550 statuses[2] = 'L'
551 # Col 3
552 if wc_status[0].getAttribute('copied') == 'true':
553 statuses[3] = '+'
554 # Col 4
555 if wc_status[0].getAttribute('switched') == 'true':
556 statuses[4] = 'S'
557 # TODO(maruel): Col 5 and 6
558 item = (''.join(statuses), file_path)
559 results.append(item)
560 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000561
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000562 @staticmethod
563 def IsMoved(filename):
564 """Determine if a file has been added through svn mv"""
565 info = SVN.CaptureInfo(filename)
566 return (info.get('Copied From URL') and
567 info.get('Copied From Rev') and
568 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000569
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000570 @staticmethod
571 def GetFileProperty(file, property_name):
572 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000573
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000574 Args:
575 file: The file to check
576 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000577
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000578 Returns:
579 The value of the property, which will be the empty string if the property
580 is not set on the file. If the file is not under version control, the
581 empty string is also returned.
582 """
583 output = SVN.Capture(["propget", property_name, file])
584 if (output.startswith("svn: ") and
585 output.endswith("is not under version control")):
586 return ""
587 else:
588 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000589
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000590 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000591 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000592 """Diffs a single file.
593
594 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000595 expected relative path.
596 full_move means that move or copy operations should completely recreate the
597 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000598 # Use svn info output instead of os.path.isdir because the latter fails
599 # when the file is deleted.
600 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
601 return None
602 # If the user specified a custom diff command in their svn config file,
603 # then it'll be used when we do svn diff, which we don't want to happen
604 # since we want the unified diff. Using --diff-cmd=diff doesn't always
605 # work, since they can have another diff executable in their path that
606 # gives different line endings. So we use a bogus temp directory as the
607 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000608 bogus_dir = tempfile.mkdtemp()
609 try:
610 # Grabs the diff data.
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000611 command = ["diff", "--config-dir", bogus_dir, filename]
maruel@chromium.orgce3c8622010-01-07 02:18:16 +0000612 if revision:
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000613 command.extend(['--revision', revision])
maruel@chromium.org0836c562010-01-22 01:10:06 +0000614 if SVN.IsMoved(filename):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000615 if full_move:
616 file_content = gclient_utils.FileRead(filename, 'rb')
617 # Prepend '+' to every lines.
618 file_content = ['+' + i for i in file_content.splitlines(True)]
619 nb_lines = len(file_content)
620 # We need to use / since patch on unix will fail otherwise.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000621 data = "Index: %s\n" % filename
622 data += '=' * 67 + '\n'
623 # Note: Should we use /dev/null instead?
624 data += "--- %s\n" % filename
625 data += "+++ %s\n" % filename
626 data += "@@ -0,0 +1,%d @@\n" % nb_lines
627 data += ''.join(file_content)
628 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000629 # svn diff on a mv/cp'd file outputs nothing if there was no change.
630 data = SVN.Capture(command, None)
631 if not data:
632 # We put in an empty Index entry so upload.py knows about them.
633 data = "Index: %s\n" % filename
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000634 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000635 data = SVN.Capture(command, None)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000636 finally:
637 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000638 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000639
640 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000641 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000642 """Returns a string containing the diff for the given file list.
643
644 The files in the list should either be absolute paths or relative to the
645 given root. If no root directory is provided, the repository root will be
646 used.
647 The diff will always use relative paths.
648 """
649 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000650 root = root or SVN.GetCheckoutRoot(previous_cwd)
651 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000652 def RelativePath(path, root):
653 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000654 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000655 return path[len(root):]
656 return path
657 try:
658 os.chdir(root)
659 diff = "".join(filter(None,
maruel@chromium.org0c401692010-01-07 02:44:20 +0000660 [SVN.DiffItem(RelativePath(f, root),
661 full_move=full_move,
662 revision=revision)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000663 for f in filenames]))
664 finally:
665 os.chdir(previous_cwd)
666 return diff
667
668
669 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000670 def GetEmail(repo_root):
671 """Retrieves the svn account which we assume is an email address."""
672 infos = SVN.CaptureInfo(repo_root)
673 uuid = infos.get('UUID')
674 root = infos.get('Repository Root')
675 if not root:
676 return None
677
678 # Should check for uuid but it is incorrectly saved for https creds.
679 realm = root.rsplit('/', 1)[0]
680 if root.startswith('https') or not uuid:
681 regexp = re.compile(r'<%s:\d+>.*' % realm)
682 else:
683 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
684 if regexp is None:
685 return None
686 if sys.platform.startswith('win'):
687 if not 'APPDATA' in os.environ:
688 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000689 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
690 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000691 else:
692 if not 'HOME' in os.environ:
693 return None
694 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
695 'svn.simple')
696 for credfile in os.listdir(auth_dir):
697 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
698 if regexp.match(cred_info.get('svn:realmstring')):
699 return cred_info.get('username')
700
701 @staticmethod
702 def ReadSimpleAuth(filename):
703 f = open(filename, 'r')
704 values = {}
705 def ReadOneItem(type):
706 m = re.match(r'%s (\d+)' % type, f.readline())
707 if not m:
708 return None
709 data = f.read(int(m.group(1)))
710 if f.read(1) != '\n':
711 return None
712 return data
713
714 while True:
715 key = ReadOneItem('K')
716 if not key:
717 break
718 value = ReadOneItem('V')
719 if not value:
720 break
721 values[key] = value
722 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000723
724 @staticmethod
725 def GetCheckoutRoot(directory):
726 """Returns the top level directory of the current repository.
727
728 The directory is returned as an absolute path.
729 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000730 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000731 infos = SVN.CaptureInfo(directory, print_error=False)
732 cur_dir_repo_root = infos.get("Repository Root")
733 if not cur_dir_repo_root:
734 return None
735
736 while True:
737 parent = os.path.dirname(directory)
738 if (SVN.CaptureInfo(parent, print_error=False).get(
739 "Repository Root") != cur_dir_repo_root):
740 break
741 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000742 return GetCasedPath(directory)