blob: 998f9fb2661cc4b2a932d8aa1c9bfdb51aed1198 [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
7import os
8import re
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +00009import shutil
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000010import subprocess
11import sys
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000012import tempfile
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000013import xml.dom.minidom
14
15import gclient_utils
16
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000017def ValidateEmail(email):
18 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
19 is not None)
20
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000021
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000022class GIT(object):
23 COMMAND = "git"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000024
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000025 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000026 def Capture(args, in_directory=None, print_error=True, error_ok=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000027 """Runs git, capturing output sent to stdout as a string.
28
29 Args:
30 args: A sequence of command line parameters to be passed to git.
31 in_directory: The directory where git is to be run.
32
33 Returns:
34 The output sent to stdout as a string.
35 """
36 c = [GIT.COMMAND]
37 c.extend(args)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000038 try:
39 return gclient_utils.CheckCall(c, in_directory, print_error)
40 except gclient_utils.CheckCallError:
41 if error_ok:
42 return ''
43 raise
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000044
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000045 @staticmethod
46 def CaptureStatus(files, upstream_branch='origin'):
47 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000048
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000049 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000050
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000051 Returns an array of (status, file) tuples."""
52 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch]
53 if not files:
54 pass
55 elif isinstance(files, basestring):
56 command.append(files)
57 else:
58 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000059
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000060 status = GIT.Capture(command).rstrip()
61 results = []
62 if status:
63 for statusline in status.split('\n'):
64 m = re.match('^(\w)\t(.+)$', statusline)
65 if not m:
66 raise Exception("status currently unsupported: %s" % statusline)
67 results.append(('%s ' % m.group(1), m.group(2)))
68 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000069
maruel@chromium.orgc78f2462009-11-21 01:20:57 +000070 @staticmethod
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000071 def RunAndFilterOutput(args,
72 in_directory,
73 print_messages,
74 print_stdout,
75 filter):
76 """Runs a command, optionally outputting to stdout.
77
78 stdout is passed line-by-line to the given filter function. If
79 print_stdout is true, it is also printed to sys.stdout as in Run.
80
81 Args:
82 args: A sequence of command line parameters to be passed.
83 in_directory: The directory where svn is to be run.
84 print_messages: Whether to print status messages to stdout about
85 which commands are being run.
86 print_stdout: Whether to forward program's output to stdout.
87 filter: A function taking one argument (a string) which will be
88 passed each line (with the ending newline character removed) of
89 program's output for filtering.
90
91 Raises:
92 gclient_utils.Error: An error occurred while running the command.
93 """
94 command = [GIT.COMMAND]
95 command.extend(args)
96 gclient_utils.SubprocessCallAndFilter(command,
97 in_directory,
98 print_messages,
99 print_stdout,
100 filter=filter)
101
102 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000103 def GetEmail(repo_root):
104 """Retrieves the user email address if known."""
105 # We could want to look at the svn cred when it has a svn remote but it
106 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000107 return GIT.Capture(['config', 'user.email'],
108 repo_root, error_ok=True).strip()
109
110 @staticmethod
111 def ShortBranchName(branch):
112 """Converts a name like 'refs/heads/foo' to just 'foo'."""
113 return branch.replace('refs/heads/', '')
114
115 @staticmethod
116 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000117 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000118 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd).strip()
119
120 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000121 def GetBranch(cwd):
122 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000123 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000124
125 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000126 def IsGitSvn(cwd):
127 """Returns true if this repo looks like it's using git-svn."""
128 # If you have any "svn-remote.*" config keys, we think you're using svn.
129 try:
130 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd)
131 return True
132 except gclient_utils.CheckCallError:
133 return False
134
135 @staticmethod
136 def GetSVNBranch(cwd):
137 """Returns the svn branch name if found."""
138 # Try to figure out which remote branch we're based on.
139 # Strategy:
140 # 1) find all git-svn branches and note their svn URLs.
141 # 2) iterate through our branch history and match up the URLs.
142
143 # regexp matching the git-svn line that contains the URL.
144 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
145
146 # Get the refname and svn url for all refs/remotes/*.
147 remotes = GIT.Capture(
148 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
149 cwd).splitlines()
150 svn_refs = {}
151 for ref in remotes:
152 match = git_svn_re.search(
153 GIT.Capture(['cat-file', '-p', ref], cwd))
154 if match:
155 svn_refs[match.group(1)] = ref
156
157 svn_branch = ''
158 if len(svn_refs) == 1:
159 # Only one svn branch exists -- seems like a good candidate.
160 svn_branch = svn_refs.values()[0]
161 elif len(svn_refs) > 1:
162 # We have more than one remote branch available. We don't
163 # want to go through all of history, so read a line from the
164 # pipe at a time.
165 # The -100 is an arbitrary limit so we don't search forever.
166 cmd = ['git', 'log', '-100', '--pretty=medium']
167 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
168 for line in proc.stdout:
169 match = git_svn_re.match(line)
170 if match:
171 url = match.group(1)
172 if url in svn_refs:
173 svn_branch = svn_refs[url]
174 proc.stdout.close() # Cut pipe.
175 break
176 return svn_branch
177
178 @staticmethod
179 def FetchUpstreamTuple(cwd):
180 """Returns a tuple containg remote and remote ref,
181 e.g. 'origin', 'refs/heads/master'
182 """
183 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000184 branch = GIT.GetBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000185 upstream_branch = None
186 upstream_branch = GIT.Capture(
187 ['config', 'branch.%s.merge' % branch], error_ok=True).strip()
188 if upstream_branch:
189 remote = GIT.Capture(
190 ['config', 'branch.%s.remote' % branch],
191 error_ok=True).strip()
192 else:
193 # Fall back on trying a git-svn upstream branch.
194 if GIT.IsGitSvn(cwd):
195 upstream_branch = GIT.GetSVNBranch(cwd)
196 # Fall back on origin/master if it exits.
197 if not upstream_branch:
198 GIT.Capture(['branch', '-r']).split().count('origin/master')
199 remote = 'origin'
200 upstream_branch = 'refs/heads/master'
201 return remote, upstream_branch
202
203 @staticmethod
204 def GetUpstream(cwd):
205 """Gets the current branch's upstream branch."""
206 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
207 if remote is not '.':
208 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
209 return upstream_branch
210
211 @staticmethod
maruel@chromium.orga9371762009-12-22 18:27:38 +0000212 def GenerateDiff(cwd, branch=None, full_move=False):
213 """Diffs against the upstream branch or optionally another branch.
214
215 full_move means that move or copy operations should completely recreate the
216 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000217 if not branch:
218 branch = GIT.GetUpstream(cwd)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000219 command = ['diff-tree', '-p', '--no-prefix', branch, 'HEAD']
220 if not full_move:
221 command.append('-C')
222 diff = GIT.Capture(command, cwd).splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000223 for i in range(len(diff)):
224 # In the case of added files, replace /dev/null with the path to the
225 # file being added.
226 if diff[i].startswith('--- /dev/null'):
227 diff[i] = '--- %s' % diff[i+1][4:]
228 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000229
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000230 @staticmethod
231 def GetPatchName(cwd):
232 """Constructs a name for this patch."""
233 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd).strip()
234 return "%s-%s" % (GIT.GetBranch(cwd), short_sha)
235
236 @staticmethod
237 def GetCheckoutRoot(cwd):
238 """Returns the top level directory of the current repository.
239
240 The directory is returned as an absolute path.
241 """
242 return os.path.abspath(GIT.Capture(['rev-parse', '--show-cdup'],
243 cwd).strip())
244
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000245
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000246class SVN(object):
247 COMMAND = "svn"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000248
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000249 @staticmethod
250 def Run(args, in_directory):
251 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000252
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000253 Args:
254 args: A sequence of command line parameters to be passed to svn.
255 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000256
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000257 Raises:
258 Error: An error occurred while running the svn command.
259 """
260 c = [SVN.COMMAND]
261 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000262 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000263 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000264
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000265 @staticmethod
266 def Capture(args, in_directory=None, print_error=True):
267 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000268
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000269 Args:
270 args: A sequence of command line parameters to be passed to svn.
271 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000272
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000273 Returns:
274 The output sent to stdout as a string.
275 """
276 c = [SVN.COMMAND]
277 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000278
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000279 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
280 # the svn.exe executable, but shell=True makes subprocess on Linux fail
281 # when it's called with a list because it only tries to execute the
282 # first string ("svn").
283 stderr = None
284 if not print_error:
285 stderr = subprocess.PIPE
286 return subprocess.Popen(c,
287 cwd=in_directory,
288 shell=(sys.platform == 'win32'),
289 stdout=subprocess.PIPE,
290 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000291
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000292 @staticmethod
293 def RunAndGetFileList(options, args, in_directory, file_list):
294 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000295
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000296 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000297
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000298 svn's stdout is parsed to collect a list of files checked out or updated.
299 These files are appended to file_list. svn's stdout is also printed to
300 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000301
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000302 Args:
303 options: command line options to gclient
304 args: A sequence of command line parameters to be passed to svn.
305 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000306
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000307 Raises:
308 Error: An error occurred while running the svn command.
309 """
310 command = [SVN.COMMAND]
311 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000312
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000313 # svn update and svn checkout use the same pattern: the first three columns
314 # are for file status, property status, and lock status. This is followed
315 # by two spaces, and then the path to the file.
316 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000317
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000318 # The first three columns of svn status are the same as for svn update and
319 # svn checkout. The next three columns indicate addition-with-history,
320 # switch, and remote lock status. This is followed by one space, and then
321 # the path to the file.
322 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000323
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000324 # args[0] must be a supported command. This will blow up if it's something
325 # else, which is good. Note that the patterns are only effective when
326 # these commands are used in their ordinary forms, the patterns are invalid
327 # for "svn status --show-updates", for example.
328 pattern = {
329 'checkout': update_pattern,
330 'status': status_pattern,
331 'update': update_pattern,
332 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000333 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000334 # Place an upper limit.
335 for i in range(1, 10):
336 previous_list_len = len(file_list)
337 failure = []
338 def CaptureMatchingLines(line):
339 match = compiled_pattern.search(line)
340 if match:
341 file_list.append(match.group(1))
342 if line.startswith('svn: '):
343 # We can't raise an exception. We can't alias a variable. Use a cheap
344 # way.
345 failure.append(True)
346 try:
347 SVN.RunAndFilterOutput(args,
348 in_directory,
349 options.verbose,
350 True,
351 CaptureMatchingLines)
352 except gclient_utils.Error:
353 # We enforce that some progress has been made.
354 if len(failure) and len(file_list) > previous_list_len:
355 if args[0] == 'checkout':
356 args = args[:]
357 # An aborted checkout is now an update.
358 args[0] = 'update'
359 continue
360 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000361
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000362 @staticmethod
363 def RunAndFilterOutput(args,
364 in_directory,
365 print_messages,
366 print_stdout,
367 filter):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000368 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000369
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000370 stdout is passed line-by-line to the given filter function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000371 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000372
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000373 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000374 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000375 in_directory: The directory where svn is to be run.
376 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000377 which commands are being run.
378 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000379 filter: A function taking one argument (a string) which will be
380 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000381 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000382
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000383 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000384 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000385 """
386 command = [SVN.COMMAND]
387 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000388 gclient_utils.SubprocessCallAndFilter(command,
389 in_directory,
390 print_messages,
391 print_stdout,
392 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000393
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000394 @staticmethod
395 def CaptureInfo(relpath, in_directory=None, print_error=True):
396 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000397
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000398 Args:
399 relpath: The directory where the working copy resides relative to
400 the directory given by in_directory.
401 in_directory: The directory where svn is to be run.
402 """
403 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
404 dom = gclient_utils.ParseXML(output)
405 result = {}
406 if dom:
407 GetNamedNodeText = gclient_utils.GetNamedNodeText
408 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
409 def C(item, f):
410 if item is not None: return f(item)
411 # /info/entry/
412 # url
413 # reposityory/(root|uuid)
414 # wc-info/(schedule|depth)
415 # commit/(author|date)
416 # str() the results because they may be returned as Unicode, which
417 # interferes with the higher layers matching up things in the deps
418 # dictionary.
419 # TODO(maruel): Fix at higher level instead (!)
420 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
421 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
422 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
423 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
424 'revision'),
425 int)
426 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
427 str)
428 # Differs across versions.
429 if result['Node Kind'] == 'dir':
430 result['Node Kind'] = 'directory'
431 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
432 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
433 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
434 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
435 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000436
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000437 @staticmethod
438 def CaptureHeadRevision(url):
439 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000440
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000441 Returns:
442 Int head revision
443 """
444 info = SVN.Capture(["info", "--xml", url], os.getcwd())
445 dom = xml.dom.minidom.parseString(info)
446 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000447
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000448 @staticmethod
449 def CaptureStatus(files):
450 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000451
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000452 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000453
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000454 Returns an array of (status, file) tuples."""
455 command = ["status", "--xml"]
456 if not files:
457 pass
458 elif isinstance(files, basestring):
459 command.append(files)
460 else:
461 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000462
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000463 status_letter = {
464 None: ' ',
465 '': ' ',
466 'added': 'A',
467 'conflicted': 'C',
468 'deleted': 'D',
469 'external': 'X',
470 'ignored': 'I',
471 'incomplete': '!',
472 'merged': 'G',
473 'missing': '!',
474 'modified': 'M',
475 'none': ' ',
476 'normal': ' ',
477 'obstructed': '~',
478 'replaced': 'R',
479 'unversioned': '?',
480 }
481 dom = gclient_utils.ParseXML(SVN.Capture(command))
482 results = []
483 if dom:
484 # /status/target/entry/(wc-status|commit|author|date)
485 for target in dom.getElementsByTagName('target'):
486 #base_path = target.getAttribute('path')
487 for entry in target.getElementsByTagName('entry'):
488 file_path = entry.getAttribute('path')
489 wc_status = entry.getElementsByTagName('wc-status')
490 assert len(wc_status) == 1
491 # Emulate svn 1.5 status ouput...
492 statuses = [' '] * 7
493 # Col 0
494 xml_item_status = wc_status[0].getAttribute('item')
495 if xml_item_status in status_letter:
496 statuses[0] = status_letter[xml_item_status]
497 else:
498 raise Exception('Unknown item status "%s"; please implement me!' %
499 xml_item_status)
500 # Col 1
501 xml_props_status = wc_status[0].getAttribute('props')
502 if xml_props_status == 'modified':
503 statuses[1] = 'M'
504 elif xml_props_status == 'conflicted':
505 statuses[1] = 'C'
506 elif (not xml_props_status or xml_props_status == 'none' or
507 xml_props_status == 'normal'):
508 pass
509 else:
510 raise Exception('Unknown props status "%s"; please implement me!' %
511 xml_props_status)
512 # Col 2
513 if wc_status[0].getAttribute('wc-locked') == 'true':
514 statuses[2] = 'L'
515 # Col 3
516 if wc_status[0].getAttribute('copied') == 'true':
517 statuses[3] = '+'
518 # Col 4
519 if wc_status[0].getAttribute('switched') == 'true':
520 statuses[4] = 'S'
521 # TODO(maruel): Col 5 and 6
522 item = (''.join(statuses), file_path)
523 results.append(item)
524 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000525
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000526 @staticmethod
527 def IsMoved(filename):
528 """Determine if a file has been added through svn mv"""
529 info = SVN.CaptureInfo(filename)
530 return (info.get('Copied From URL') and
531 info.get('Copied From Rev') and
532 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000533
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000534 @staticmethod
535 def GetFileProperty(file, property_name):
536 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000537
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000538 Args:
539 file: The file to check
540 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000541
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000542 Returns:
543 The value of the property, which will be the empty string if the property
544 is not set on the file. If the file is not under version control, the
545 empty string is also returned.
546 """
547 output = SVN.Capture(["propget", property_name, file])
548 if (output.startswith("svn: ") and
549 output.endswith("is not under version control")):
550 return ""
551 else:
552 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000553
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000554 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000555 def DiffItem(filename, full_move=False):
556 """Diffs a single file.
557
558 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000559 expected relative path.
560 full_move means that move or copy operations should completely recreate the
561 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000562 # Use svn info output instead of os.path.isdir because the latter fails
563 # when the file is deleted.
564 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
565 return None
566 # If the user specified a custom diff command in their svn config file,
567 # then it'll be used when we do svn diff, which we don't want to happen
568 # since we want the unified diff. Using --diff-cmd=diff doesn't always
569 # work, since they can have another diff executable in their path that
570 # gives different line endings. So we use a bogus temp directory as the
571 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000572 bogus_dir = tempfile.mkdtemp()
573 try:
574 # Grabs the diff data.
575 data = SVN.Capture(["diff", "--config-dir", bogus_dir, filename], None)
576 if data:
577 pass
578 elif SVN.IsMoved(filename):
579 if full_move:
580 file_content = gclient_utils.FileRead(filename, 'rb')
581 # Prepend '+' to every lines.
582 file_content = ['+' + i for i in file_content.splitlines(True)]
583 nb_lines = len(file_content)
584 # We need to use / since patch on unix will fail otherwise.
585 filename = filename.replace('\\', '/')
586 data = "Index: %s\n" % filename
587 data += '=' * 67 + '\n'
588 # Note: Should we use /dev/null instead?
589 data += "--- %s\n" % filename
590 data += "+++ %s\n" % filename
591 data += "@@ -0,0 +1,%d @@\n" % nb_lines
592 data += ''.join(file_content)
593 else:
594 # svn diff on a mv/cp'd file outputs nothing.
595 # We put in an empty Index entry so upload.py knows about them.
596 data = "Index: %s\n" % filename
597 else:
598 # The file is not modified anymore. It should be removed from the set.
599 pass
600 finally:
601 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000602 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000603
604 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000605 def GenerateDiff(filenames, root=None, full_move=False):
606 """Returns a string containing the diff for the given file list.
607
608 The files in the list should either be absolute paths or relative to the
609 given root. If no root directory is provided, the repository root will be
610 used.
611 The diff will always use relative paths.
612 """
613 previous_cwd = os.getcwd()
614 root = os.path.join(root or SVN.GetCheckoutRoot(previous_cwd), '')
615 def RelativePath(path, root):
616 """We must use relative paths."""
617 if path.startswith(root):
618 return path[len(root):]
619 return path
620 try:
621 os.chdir(root)
622 diff = "".join(filter(None,
623 [SVN.DiffItem(RelativePath(f, root),
624 full_move=full_move)
625 for f in filenames]))
626 finally:
627 os.chdir(previous_cwd)
628 return diff
629
630
631 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000632 def GetEmail(repo_root):
633 """Retrieves the svn account which we assume is an email address."""
634 infos = SVN.CaptureInfo(repo_root)
635 uuid = infos.get('UUID')
636 root = infos.get('Repository Root')
637 if not root:
638 return None
639
640 # Should check for uuid but it is incorrectly saved for https creds.
641 realm = root.rsplit('/', 1)[0]
642 if root.startswith('https') or not uuid:
643 regexp = re.compile(r'<%s:\d+>.*' % realm)
644 else:
645 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
646 if regexp is None:
647 return None
648 if sys.platform.startswith('win'):
649 if not 'APPDATA' in os.environ:
650 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000651 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
652 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000653 else:
654 if not 'HOME' in os.environ:
655 return None
656 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
657 'svn.simple')
658 for credfile in os.listdir(auth_dir):
659 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
660 if regexp.match(cred_info.get('svn:realmstring')):
661 return cred_info.get('username')
662
663 @staticmethod
664 def ReadSimpleAuth(filename):
665 f = open(filename, 'r')
666 values = {}
667 def ReadOneItem(type):
668 m = re.match(r'%s (\d+)' % type, f.readline())
669 if not m:
670 return None
671 data = f.read(int(m.group(1)))
672 if f.read(1) != '\n':
673 return None
674 return data
675
676 while True:
677 key = ReadOneItem('K')
678 if not key:
679 break
680 value = ReadOneItem('V')
681 if not value:
682 break
683 values[key] = value
684 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000685
686 @staticmethod
687 def GetCheckoutRoot(directory):
688 """Returns the top level directory of the current repository.
689
690 The directory is returned as an absolute path.
691 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000692 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000693 infos = SVN.CaptureInfo(directory, print_error=False)
694 cur_dir_repo_root = infos.get("Repository Root")
695 if not cur_dir_repo_root:
696 return None
697
698 while True:
699 parent = os.path.dirname(directory)
700 if (SVN.CaptureInfo(parent, print_error=False).get(
701 "Repository Root") != cur_dir_repo_root):
702 break
703 directory = parent
704 return directory