blob: 2e135558801223059bba50f4fa0b892f1f7e5571 [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
71 def GetEmail(repo_root):
72 """Retrieves the user email address if known."""
73 # We could want to look at the svn cred when it has a svn remote but it
74 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000075 return GIT.Capture(['config', 'user.email'],
76 repo_root, error_ok=True).strip()
77
78 @staticmethod
79 def ShortBranchName(branch):
80 """Converts a name like 'refs/heads/foo' to just 'foo'."""
81 return branch.replace('refs/heads/', '')
82
83 @staticmethod
84 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000085 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000086 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd).strip()
87
88 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000089 def GetBranch(cwd):
90 """Returns the short branch name, e.g. 'master'."""
91 return GIT.ShortBranchName(GIT.BranchRef(cwd))
92
93 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000094 def IsGitSvn(cwd):
95 """Returns true if this repo looks like it's using git-svn."""
96 # If you have any "svn-remote.*" config keys, we think you're using svn.
97 try:
98 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd)
99 return True
100 except gclient_utils.CheckCallError:
101 return False
102
103 @staticmethod
104 def GetSVNBranch(cwd):
105 """Returns the svn branch name if found."""
106 # Try to figure out which remote branch we're based on.
107 # Strategy:
108 # 1) find all git-svn branches and note their svn URLs.
109 # 2) iterate through our branch history and match up the URLs.
110
111 # regexp matching the git-svn line that contains the URL.
112 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
113
114 # Get the refname and svn url for all refs/remotes/*.
115 remotes = GIT.Capture(
116 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
117 cwd).splitlines()
118 svn_refs = {}
119 for ref in remotes:
120 match = git_svn_re.search(
121 GIT.Capture(['cat-file', '-p', ref], cwd))
122 if match:
123 svn_refs[match.group(1)] = ref
124
125 svn_branch = ''
126 if len(svn_refs) == 1:
127 # Only one svn branch exists -- seems like a good candidate.
128 svn_branch = svn_refs.values()[0]
129 elif len(svn_refs) > 1:
130 # We have more than one remote branch available. We don't
131 # want to go through all of history, so read a line from the
132 # pipe at a time.
133 # The -100 is an arbitrary limit so we don't search forever.
134 cmd = ['git', 'log', '-100', '--pretty=medium']
135 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
136 for line in proc.stdout:
137 match = git_svn_re.match(line)
138 if match:
139 url = match.group(1)
140 if url in svn_refs:
141 svn_branch = svn_refs[url]
142 proc.stdout.close() # Cut pipe.
143 break
144 return svn_branch
145
146 @staticmethod
147 def FetchUpstreamTuple(cwd):
148 """Returns a tuple containg remote and remote ref,
149 e.g. 'origin', 'refs/heads/master'
150 """
151 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000152 branch = GIT.GetBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000153 upstream_branch = None
154 upstream_branch = GIT.Capture(
155 ['config', 'branch.%s.merge' % branch], error_ok=True).strip()
156 if upstream_branch:
157 remote = GIT.Capture(
158 ['config', 'branch.%s.remote' % branch],
159 error_ok=True).strip()
160 else:
161 # Fall back on trying a git-svn upstream branch.
162 if GIT.IsGitSvn(cwd):
163 upstream_branch = GIT.GetSVNBranch(cwd)
164 # Fall back on origin/master if it exits.
165 if not upstream_branch:
166 GIT.Capture(['branch', '-r']).split().count('origin/master')
167 remote = 'origin'
168 upstream_branch = 'refs/heads/master'
169 return remote, upstream_branch
170
171 @staticmethod
172 def GetUpstream(cwd):
173 """Gets the current branch's upstream branch."""
174 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
175 if remote is not '.':
176 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
177 return upstream_branch
178
179 @staticmethod
180 def GenerateDiff(cwd, branch=None):
181 """Diffs against the upstream branch or optionally another branch."""
182 if not branch:
183 branch = GIT.GetUpstream(cwd)
184 diff = GIT.Capture(['diff-tree', '-p', '--no-prefix', branch, 'HEAD'],
185 cwd).splitlines(True)
186 for i in range(len(diff)):
187 # In the case of added files, replace /dev/null with the path to the
188 # file being added.
189 if diff[i].startswith('--- /dev/null'):
190 diff[i] = '--- %s' % diff[i+1][4:]
191 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000192
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000193 @staticmethod
194 def GetPatchName(cwd):
195 """Constructs a name for this patch."""
196 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd).strip()
197 return "%s-%s" % (GIT.GetBranch(cwd), short_sha)
198
199 @staticmethod
200 def GetCheckoutRoot(cwd):
201 """Returns the top level directory of the current repository.
202
203 The directory is returned as an absolute path.
204 """
205 return os.path.abspath(GIT.Capture(['rev-parse', '--show-cdup'],
206 cwd).strip())
207
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000208
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000209class SVN(object):
210 COMMAND = "svn"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000211
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000212 @staticmethod
213 def Run(args, in_directory):
214 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000215
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000216 Args:
217 args: A sequence of command line parameters to be passed to svn.
218 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000219
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000220 Raises:
221 Error: An error occurred while running the svn command.
222 """
223 c = [SVN.COMMAND]
224 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000225 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000226 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000227
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000228 @staticmethod
229 def Capture(args, in_directory=None, print_error=True):
230 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000231
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000232 Args:
233 args: A sequence of command line parameters to be passed to svn.
234 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000235
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000236 Returns:
237 The output sent to stdout as a string.
238 """
239 c = [SVN.COMMAND]
240 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000241
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000242 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
243 # the svn.exe executable, but shell=True makes subprocess on Linux fail
244 # when it's called with a list because it only tries to execute the
245 # first string ("svn").
246 stderr = None
247 if not print_error:
248 stderr = subprocess.PIPE
249 return subprocess.Popen(c,
250 cwd=in_directory,
251 shell=(sys.platform == 'win32'),
252 stdout=subprocess.PIPE,
253 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000254
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000255 @staticmethod
256 def RunAndGetFileList(options, args, in_directory, file_list):
257 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000258
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000259 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000260
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000261 svn's stdout is parsed to collect a list of files checked out or updated.
262 These files are appended to file_list. svn's stdout is also printed to
263 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000264
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000265 Args:
266 options: command line options to gclient
267 args: A sequence of command line parameters to be passed to svn.
268 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000269
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000270 Raises:
271 Error: An error occurred while running the svn command.
272 """
273 command = [SVN.COMMAND]
274 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000275
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000276 # svn update and svn checkout use the same pattern: the first three columns
277 # are for file status, property status, and lock status. This is followed
278 # by two spaces, and then the path to the file.
279 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000280
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000281 # The first three columns of svn status are the same as for svn update and
282 # svn checkout. The next three columns indicate addition-with-history,
283 # switch, and remote lock status. This is followed by one space, and then
284 # the path to the file.
285 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000286
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000287 # args[0] must be a supported command. This will blow up if it's something
288 # else, which is good. Note that the patterns are only effective when
289 # these commands are used in their ordinary forms, the patterns are invalid
290 # for "svn status --show-updates", for example.
291 pattern = {
292 'checkout': update_pattern,
293 'status': status_pattern,
294 'update': update_pattern,
295 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000296 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000297 # Place an upper limit.
298 for i in range(1, 10):
299 previous_list_len = len(file_list)
300 failure = []
301 def CaptureMatchingLines(line):
302 match = compiled_pattern.search(line)
303 if match:
304 file_list.append(match.group(1))
305 if line.startswith('svn: '):
306 # We can't raise an exception. We can't alias a variable. Use a cheap
307 # way.
308 failure.append(True)
309 try:
310 SVN.RunAndFilterOutput(args,
311 in_directory,
312 options.verbose,
313 True,
314 CaptureMatchingLines)
315 except gclient_utils.Error:
316 # We enforce that some progress has been made.
317 if len(failure) and len(file_list) > previous_list_len:
318 if args[0] == 'checkout':
319 args = args[:]
320 # An aborted checkout is now an update.
321 args[0] = 'update'
322 continue
323 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000324
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000325 @staticmethod
326 def RunAndFilterOutput(args,
327 in_directory,
328 print_messages,
329 print_stdout,
330 filter):
331 """Runs svn checkout, update, status, or diff, optionally outputting
332 to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000333
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000334 The first item in args must be either "checkout", "update",
335 "status", or "diff".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000336
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000337 svn's stdout is passed line-by-line to the given filter function. If
338 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000339
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000340 Args:
341 args: A sequence of command line parameters to be passed to svn.
342 in_directory: The directory where svn is to be run.
343 print_messages: Whether to print status messages to stdout about
344 which Subversion commands are being run.
345 print_stdout: Whether to forward Subversion's output to stdout.
346 filter: A function taking one argument (a string) which will be
347 passed each line (with the ending newline character removed) of
348 Subversion's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000349
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000350 Raises:
351 Error: An error occurred while running the svn command.
352 """
353 command = [SVN.COMMAND]
354 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000355
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000356 gclient_utils.SubprocessCallAndFilter(command,
357 in_directory,
358 print_messages,
359 print_stdout,
360 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000361
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000362 @staticmethod
363 def CaptureInfo(relpath, in_directory=None, print_error=True):
364 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000365
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000366 Args:
367 relpath: The directory where the working copy resides relative to
368 the directory given by in_directory.
369 in_directory: The directory where svn is to be run.
370 """
371 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
372 dom = gclient_utils.ParseXML(output)
373 result = {}
374 if dom:
375 GetNamedNodeText = gclient_utils.GetNamedNodeText
376 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
377 def C(item, f):
378 if item is not None: return f(item)
379 # /info/entry/
380 # url
381 # reposityory/(root|uuid)
382 # wc-info/(schedule|depth)
383 # commit/(author|date)
384 # str() the results because they may be returned as Unicode, which
385 # interferes with the higher layers matching up things in the deps
386 # dictionary.
387 # TODO(maruel): Fix at higher level instead (!)
388 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
389 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
390 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
391 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
392 'revision'),
393 int)
394 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
395 str)
396 # Differs across versions.
397 if result['Node Kind'] == 'dir':
398 result['Node Kind'] = 'directory'
399 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
400 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
401 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
402 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
403 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000404
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000405 @staticmethod
406 def CaptureHeadRevision(url):
407 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000408
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000409 Returns:
410 Int head revision
411 """
412 info = SVN.Capture(["info", "--xml", url], os.getcwd())
413 dom = xml.dom.minidom.parseString(info)
414 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000415
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000416 @staticmethod
417 def CaptureStatus(files):
418 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000419
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000420 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000421
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000422 Returns an array of (status, file) tuples."""
423 command = ["status", "--xml"]
424 if not files:
425 pass
426 elif isinstance(files, basestring):
427 command.append(files)
428 else:
429 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000430
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000431 status_letter = {
432 None: ' ',
433 '': ' ',
434 'added': 'A',
435 'conflicted': 'C',
436 'deleted': 'D',
437 'external': 'X',
438 'ignored': 'I',
439 'incomplete': '!',
440 'merged': 'G',
441 'missing': '!',
442 'modified': 'M',
443 'none': ' ',
444 'normal': ' ',
445 'obstructed': '~',
446 'replaced': 'R',
447 'unversioned': '?',
448 }
449 dom = gclient_utils.ParseXML(SVN.Capture(command))
450 results = []
451 if dom:
452 # /status/target/entry/(wc-status|commit|author|date)
453 for target in dom.getElementsByTagName('target'):
454 #base_path = target.getAttribute('path')
455 for entry in target.getElementsByTagName('entry'):
456 file_path = entry.getAttribute('path')
457 wc_status = entry.getElementsByTagName('wc-status')
458 assert len(wc_status) == 1
459 # Emulate svn 1.5 status ouput...
460 statuses = [' '] * 7
461 # Col 0
462 xml_item_status = wc_status[0].getAttribute('item')
463 if xml_item_status in status_letter:
464 statuses[0] = status_letter[xml_item_status]
465 else:
466 raise Exception('Unknown item status "%s"; please implement me!' %
467 xml_item_status)
468 # Col 1
469 xml_props_status = wc_status[0].getAttribute('props')
470 if xml_props_status == 'modified':
471 statuses[1] = 'M'
472 elif xml_props_status == 'conflicted':
473 statuses[1] = 'C'
474 elif (not xml_props_status or xml_props_status == 'none' or
475 xml_props_status == 'normal'):
476 pass
477 else:
478 raise Exception('Unknown props status "%s"; please implement me!' %
479 xml_props_status)
480 # Col 2
481 if wc_status[0].getAttribute('wc-locked') == 'true':
482 statuses[2] = 'L'
483 # Col 3
484 if wc_status[0].getAttribute('copied') == 'true':
485 statuses[3] = '+'
486 # Col 4
487 if wc_status[0].getAttribute('switched') == 'true':
488 statuses[4] = 'S'
489 # TODO(maruel): Col 5 and 6
490 item = (''.join(statuses), file_path)
491 results.append(item)
492 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000493
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000494 @staticmethod
495 def IsMoved(filename):
496 """Determine if a file has been added through svn mv"""
497 info = SVN.CaptureInfo(filename)
498 return (info.get('Copied From URL') and
499 info.get('Copied From Rev') and
500 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000501
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000502 @staticmethod
503 def GetFileProperty(file, property_name):
504 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000505
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000506 Args:
507 file: The file to check
508 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000509
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000510 Returns:
511 The value of the property, which will be the empty string if the property
512 is not set on the file. If the file is not under version control, the
513 empty string is also returned.
514 """
515 output = SVN.Capture(["propget", property_name, file])
516 if (output.startswith("svn: ") and
517 output.endswith("is not under version control")):
518 return ""
519 else:
520 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000521
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000522 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000523 def DiffItem(filename, full_move=False):
524 """Diffs a single file.
525
526 Be sure to be in the appropriate directory before calling to have the
527 expected relative path."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000528 # Use svn info output instead of os.path.isdir because the latter fails
529 # when the file is deleted.
530 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
531 return None
532 # If the user specified a custom diff command in their svn config file,
533 # then it'll be used when we do svn diff, which we don't want to happen
534 # since we want the unified diff. Using --diff-cmd=diff doesn't always
535 # work, since they can have another diff executable in their path that
536 # gives different line endings. So we use a bogus temp directory as the
537 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000538 bogus_dir = tempfile.mkdtemp()
539 try:
540 # Grabs the diff data.
541 data = SVN.Capture(["diff", "--config-dir", bogus_dir, filename], None)
542 if data:
543 pass
544 elif SVN.IsMoved(filename):
545 if full_move:
546 file_content = gclient_utils.FileRead(filename, 'rb')
547 # Prepend '+' to every lines.
548 file_content = ['+' + i for i in file_content.splitlines(True)]
549 nb_lines = len(file_content)
550 # We need to use / since patch on unix will fail otherwise.
551 filename = filename.replace('\\', '/')
552 data = "Index: %s\n" % filename
553 data += '=' * 67 + '\n'
554 # Note: Should we use /dev/null instead?
555 data += "--- %s\n" % filename
556 data += "+++ %s\n" % filename
557 data += "@@ -0,0 +1,%d @@\n" % nb_lines
558 data += ''.join(file_content)
559 else:
560 # svn diff on a mv/cp'd file outputs nothing.
561 # We put in an empty Index entry so upload.py knows about them.
562 data = "Index: %s\n" % filename
563 else:
564 # The file is not modified anymore. It should be removed from the set.
565 pass
566 finally:
567 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000568 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000569
570 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000571 def GenerateDiff(filenames, root=None, full_move=False):
572 """Returns a string containing the diff for the given file list.
573
574 The files in the list should either be absolute paths or relative to the
575 given root. If no root directory is provided, the repository root will be
576 used.
577 The diff will always use relative paths.
578 """
579 previous_cwd = os.getcwd()
580 root = os.path.join(root or SVN.GetCheckoutRoot(previous_cwd), '')
581 def RelativePath(path, root):
582 """We must use relative paths."""
583 if path.startswith(root):
584 return path[len(root):]
585 return path
586 try:
587 os.chdir(root)
588 diff = "".join(filter(None,
589 [SVN.DiffItem(RelativePath(f, root),
590 full_move=full_move)
591 for f in filenames]))
592 finally:
593 os.chdir(previous_cwd)
594 return diff
595
596
597 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000598 def GetEmail(repo_root):
599 """Retrieves the svn account which we assume is an email address."""
600 infos = SVN.CaptureInfo(repo_root)
601 uuid = infos.get('UUID')
602 root = infos.get('Repository Root')
603 if not root:
604 return None
605
606 # Should check for uuid but it is incorrectly saved for https creds.
607 realm = root.rsplit('/', 1)[0]
608 if root.startswith('https') or not uuid:
609 regexp = re.compile(r'<%s:\d+>.*' % realm)
610 else:
611 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
612 if regexp is None:
613 return None
614 if sys.platform.startswith('win'):
615 if not 'APPDATA' in os.environ:
616 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000617 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
618 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000619 else:
620 if not 'HOME' in os.environ:
621 return None
622 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
623 'svn.simple')
624 for credfile in os.listdir(auth_dir):
625 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
626 if regexp.match(cred_info.get('svn:realmstring')):
627 return cred_info.get('username')
628
629 @staticmethod
630 def ReadSimpleAuth(filename):
631 f = open(filename, 'r')
632 values = {}
633 def ReadOneItem(type):
634 m = re.match(r'%s (\d+)' % type, f.readline())
635 if not m:
636 return None
637 data = f.read(int(m.group(1)))
638 if f.read(1) != '\n':
639 return None
640 return data
641
642 while True:
643 key = ReadOneItem('K')
644 if not key:
645 break
646 value = ReadOneItem('V')
647 if not value:
648 break
649 values[key] = value
650 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000651
652 @staticmethod
653 def GetCheckoutRoot(directory):
654 """Returns the top level directory of the current repository.
655
656 The directory is returned as an absolute path.
657 """
658 infos = SVN.CaptureInfo(directory, print_error=False)
659 cur_dir_repo_root = infos.get("Repository Root")
660 if not cur_dir_repo_root:
661 return None
662
663 while True:
664 parent = os.path.dirname(directory)
665 if (SVN.CaptureInfo(parent, print_error=False).get(
666 "Repository Root") != cur_dir_repo_root):
667 break
668 directory = parent
669 return directory