blob: d23ab62d60d924f31e0e7eb6716649da0fa67bcd [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
17
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000018class GIT(object):
19 COMMAND = "git"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000020
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000021 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000022 def Capture(args, in_directory=None, print_error=True, error_ok=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000023 """Runs git, capturing output sent to stdout as a string.
24
25 Args:
26 args: A sequence of command line parameters to be passed to git.
27 in_directory: The directory where git is to be run.
28
29 Returns:
30 The output sent to stdout as a string.
31 """
32 c = [GIT.COMMAND]
33 c.extend(args)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000034 try:
35 return gclient_utils.CheckCall(c, in_directory, print_error)
36 except gclient_utils.CheckCallError:
37 if error_ok:
38 return ''
39 raise
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000040
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000041 @staticmethod
42 def CaptureStatus(files, upstream_branch='origin'):
43 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000044
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000045 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000046
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000047 Returns an array of (status, file) tuples."""
48 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch]
49 if not files:
50 pass
51 elif isinstance(files, basestring):
52 command.append(files)
53 else:
54 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000055
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000056 status = GIT.Capture(command).rstrip()
57 results = []
58 if status:
59 for statusline in status.split('\n'):
60 m = re.match('^(\w)\t(.+)$', statusline)
61 if not m:
62 raise Exception("status currently unsupported: %s" % statusline)
63 results.append(('%s ' % m.group(1), m.group(2)))
64 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000065
maruel@chromium.orgc78f2462009-11-21 01:20:57 +000066 @staticmethod
67 def GetEmail(repo_root):
68 """Retrieves the user email address if known."""
69 # We could want to look at the svn cred when it has a svn remote but it
70 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000071 return GIT.Capture(['config', 'user.email'],
72 repo_root, error_ok=True).strip()
73
74 @staticmethod
75 def ShortBranchName(branch):
76 """Converts a name like 'refs/heads/foo' to just 'foo'."""
77 return branch.replace('refs/heads/', '')
78
79 @staticmethod
80 def GetBranchRef(cwd):
81 """Returns the short branch name, e.g. 'master'."""
82 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd).strip()
83
84 @staticmethod
85 def IsGitSvn(cwd):
86 """Returns true if this repo looks like it's using git-svn."""
87 # If you have any "svn-remote.*" config keys, we think you're using svn.
88 try:
89 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd)
90 return True
91 except gclient_utils.CheckCallError:
92 return False
93
94 @staticmethod
95 def GetSVNBranch(cwd):
96 """Returns the svn branch name if found."""
97 # Try to figure out which remote branch we're based on.
98 # Strategy:
99 # 1) find all git-svn branches and note their svn URLs.
100 # 2) iterate through our branch history and match up the URLs.
101
102 # regexp matching the git-svn line that contains the URL.
103 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
104
105 # Get the refname and svn url for all refs/remotes/*.
106 remotes = GIT.Capture(
107 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
108 cwd).splitlines()
109 svn_refs = {}
110 for ref in remotes:
111 match = git_svn_re.search(
112 GIT.Capture(['cat-file', '-p', ref], cwd))
113 if match:
114 svn_refs[match.group(1)] = ref
115
116 svn_branch = ''
117 if len(svn_refs) == 1:
118 # Only one svn branch exists -- seems like a good candidate.
119 svn_branch = svn_refs.values()[0]
120 elif len(svn_refs) > 1:
121 # We have more than one remote branch available. We don't
122 # want to go through all of history, so read a line from the
123 # pipe at a time.
124 # The -100 is an arbitrary limit so we don't search forever.
125 cmd = ['git', 'log', '-100', '--pretty=medium']
126 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
127 for line in proc.stdout:
128 match = git_svn_re.match(line)
129 if match:
130 url = match.group(1)
131 if url in svn_refs:
132 svn_branch = svn_refs[url]
133 proc.stdout.close() # Cut pipe.
134 break
135 return svn_branch
136
137 @staticmethod
138 def FetchUpstreamTuple(cwd):
139 """Returns a tuple containg remote and remote ref,
140 e.g. 'origin', 'refs/heads/master'
141 """
142 remote = '.'
143 branch = GIT.ShortBranchName(GIT.GetBranchRef(cwd))
144 upstream_branch = None
145 upstream_branch = GIT.Capture(
146 ['config', 'branch.%s.merge' % branch], error_ok=True).strip()
147 if upstream_branch:
148 remote = GIT.Capture(
149 ['config', 'branch.%s.remote' % branch],
150 error_ok=True).strip()
151 else:
152 # Fall back on trying a git-svn upstream branch.
153 if GIT.IsGitSvn(cwd):
154 upstream_branch = GIT.GetSVNBranch(cwd)
155 # Fall back on origin/master if it exits.
156 if not upstream_branch:
157 GIT.Capture(['branch', '-r']).split().count('origin/master')
158 remote = 'origin'
159 upstream_branch = 'refs/heads/master'
160 return remote, upstream_branch
161
162 @staticmethod
163 def GetUpstream(cwd):
164 """Gets the current branch's upstream branch."""
165 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
166 if remote is not '.':
167 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
168 return upstream_branch
169
170 @staticmethod
171 def GenerateDiff(cwd, branch=None):
172 """Diffs against the upstream branch or optionally another branch."""
173 if not branch:
174 branch = GIT.GetUpstream(cwd)
175 diff = GIT.Capture(['diff-tree', '-p', '--no-prefix', branch, 'HEAD'],
176 cwd).splitlines(True)
177 for i in range(len(diff)):
178 # In the case of added files, replace /dev/null with the path to the
179 # file being added.
180 if diff[i].startswith('--- /dev/null'):
181 diff[i] = '--- %s' % diff[i+1][4:]
182 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000183
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000184
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000185class SVN(object):
186 COMMAND = "svn"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000187
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000188 @staticmethod
189 def Run(args, in_directory):
190 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000191
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000192 Args:
193 args: A sequence of command line parameters to be passed to svn.
194 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000195
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000196 Raises:
197 Error: An error occurred while running the svn command.
198 """
199 c = [SVN.COMMAND]
200 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000201 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000202 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000203
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000204 @staticmethod
205 def Capture(args, in_directory=None, print_error=True):
206 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000207
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000208 Args:
209 args: A sequence of command line parameters to be passed to svn.
210 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000211
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000212 Returns:
213 The output sent to stdout as a string.
214 """
215 c = [SVN.COMMAND]
216 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000217
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000218 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
219 # the svn.exe executable, but shell=True makes subprocess on Linux fail
220 # when it's called with a list because it only tries to execute the
221 # first string ("svn").
222 stderr = None
223 if not print_error:
224 stderr = subprocess.PIPE
225 return subprocess.Popen(c,
226 cwd=in_directory,
227 shell=(sys.platform == 'win32'),
228 stdout=subprocess.PIPE,
229 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000230
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000231 @staticmethod
232 def RunAndGetFileList(options, args, in_directory, file_list):
233 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000234
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000235 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000236
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000237 svn's stdout is parsed to collect a list of files checked out or updated.
238 These files are appended to file_list. svn's stdout is also printed to
239 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000240
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000241 Args:
242 options: command line options to gclient
243 args: A sequence of command line parameters to be passed to svn.
244 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000245
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000246 Raises:
247 Error: An error occurred while running the svn command.
248 """
249 command = [SVN.COMMAND]
250 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000251
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000252 # svn update and svn checkout use the same pattern: the first three columns
253 # are for file status, property status, and lock status. This is followed
254 # by two spaces, and then the path to the file.
255 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000256
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000257 # The first three columns of svn status are the same as for svn update and
258 # svn checkout. The next three columns indicate addition-with-history,
259 # switch, and remote lock status. This is followed by one space, and then
260 # the path to the file.
261 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000262
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000263 # args[0] must be a supported command. This will blow up if it's something
264 # else, which is good. Note that the patterns are only effective when
265 # these commands are used in their ordinary forms, the patterns are invalid
266 # for "svn status --show-updates", for example.
267 pattern = {
268 'checkout': update_pattern,
269 'status': status_pattern,
270 'update': update_pattern,
271 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000272 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000273 # Place an upper limit.
274 for i in range(1, 10):
275 previous_list_len = len(file_list)
276 failure = []
277 def CaptureMatchingLines(line):
278 match = compiled_pattern.search(line)
279 if match:
280 file_list.append(match.group(1))
281 if line.startswith('svn: '):
282 # We can't raise an exception. We can't alias a variable. Use a cheap
283 # way.
284 failure.append(True)
285 try:
286 SVN.RunAndFilterOutput(args,
287 in_directory,
288 options.verbose,
289 True,
290 CaptureMatchingLines)
291 except gclient_utils.Error:
292 # We enforce that some progress has been made.
293 if len(failure) and len(file_list) > previous_list_len:
294 if args[0] == 'checkout':
295 args = args[:]
296 # An aborted checkout is now an update.
297 args[0] = 'update'
298 continue
299 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000300
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000301 @staticmethod
302 def RunAndFilterOutput(args,
303 in_directory,
304 print_messages,
305 print_stdout,
306 filter):
307 """Runs svn checkout, update, status, or diff, optionally outputting
308 to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000309
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000310 The first item in args must be either "checkout", "update",
311 "status", or "diff".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000312
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000313 svn's stdout is passed line-by-line to the given filter function. If
314 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000315
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000316 Args:
317 args: A sequence of command line parameters to be passed to svn.
318 in_directory: The directory where svn is to be run.
319 print_messages: Whether to print status messages to stdout about
320 which Subversion commands are being run.
321 print_stdout: Whether to forward Subversion's output to stdout.
322 filter: A function taking one argument (a string) which will be
323 passed each line (with the ending newline character removed) of
324 Subversion's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000325
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000326 Raises:
327 Error: An error occurred while running the svn command.
328 """
329 command = [SVN.COMMAND]
330 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000331
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000332 gclient_utils.SubprocessCallAndFilter(command,
333 in_directory,
334 print_messages,
335 print_stdout,
336 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000337
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000338 @staticmethod
339 def CaptureInfo(relpath, in_directory=None, print_error=True):
340 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000341
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000342 Args:
343 relpath: The directory where the working copy resides relative to
344 the directory given by in_directory.
345 in_directory: The directory where svn is to be run.
346 """
347 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
348 dom = gclient_utils.ParseXML(output)
349 result = {}
350 if dom:
351 GetNamedNodeText = gclient_utils.GetNamedNodeText
352 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
353 def C(item, f):
354 if item is not None: return f(item)
355 # /info/entry/
356 # url
357 # reposityory/(root|uuid)
358 # wc-info/(schedule|depth)
359 # commit/(author|date)
360 # str() the results because they may be returned as Unicode, which
361 # interferes with the higher layers matching up things in the deps
362 # dictionary.
363 # TODO(maruel): Fix at higher level instead (!)
364 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
365 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
366 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
367 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
368 'revision'),
369 int)
370 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
371 str)
372 # Differs across versions.
373 if result['Node Kind'] == 'dir':
374 result['Node Kind'] = 'directory'
375 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
376 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
377 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
378 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
379 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000380
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000381 @staticmethod
382 def CaptureHeadRevision(url):
383 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000384
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000385 Returns:
386 Int head revision
387 """
388 info = SVN.Capture(["info", "--xml", url], os.getcwd())
389 dom = xml.dom.minidom.parseString(info)
390 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000391
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000392 @staticmethod
393 def CaptureStatus(files):
394 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000395
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000396 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000397
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000398 Returns an array of (status, file) tuples."""
399 command = ["status", "--xml"]
400 if not files:
401 pass
402 elif isinstance(files, basestring):
403 command.append(files)
404 else:
405 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000406
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000407 status_letter = {
408 None: ' ',
409 '': ' ',
410 'added': 'A',
411 'conflicted': 'C',
412 'deleted': 'D',
413 'external': 'X',
414 'ignored': 'I',
415 'incomplete': '!',
416 'merged': 'G',
417 'missing': '!',
418 'modified': 'M',
419 'none': ' ',
420 'normal': ' ',
421 'obstructed': '~',
422 'replaced': 'R',
423 'unversioned': '?',
424 }
425 dom = gclient_utils.ParseXML(SVN.Capture(command))
426 results = []
427 if dom:
428 # /status/target/entry/(wc-status|commit|author|date)
429 for target in dom.getElementsByTagName('target'):
430 #base_path = target.getAttribute('path')
431 for entry in target.getElementsByTagName('entry'):
432 file_path = entry.getAttribute('path')
433 wc_status = entry.getElementsByTagName('wc-status')
434 assert len(wc_status) == 1
435 # Emulate svn 1.5 status ouput...
436 statuses = [' '] * 7
437 # Col 0
438 xml_item_status = wc_status[0].getAttribute('item')
439 if xml_item_status in status_letter:
440 statuses[0] = status_letter[xml_item_status]
441 else:
442 raise Exception('Unknown item status "%s"; please implement me!' %
443 xml_item_status)
444 # Col 1
445 xml_props_status = wc_status[0].getAttribute('props')
446 if xml_props_status == 'modified':
447 statuses[1] = 'M'
448 elif xml_props_status == 'conflicted':
449 statuses[1] = 'C'
450 elif (not xml_props_status or xml_props_status == 'none' or
451 xml_props_status == 'normal'):
452 pass
453 else:
454 raise Exception('Unknown props status "%s"; please implement me!' %
455 xml_props_status)
456 # Col 2
457 if wc_status[0].getAttribute('wc-locked') == 'true':
458 statuses[2] = 'L'
459 # Col 3
460 if wc_status[0].getAttribute('copied') == 'true':
461 statuses[3] = '+'
462 # Col 4
463 if wc_status[0].getAttribute('switched') == 'true':
464 statuses[4] = 'S'
465 # TODO(maruel): Col 5 and 6
466 item = (''.join(statuses), file_path)
467 results.append(item)
468 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000469
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000470 @staticmethod
471 def IsMoved(filename):
472 """Determine if a file has been added through svn mv"""
473 info = SVN.CaptureInfo(filename)
474 return (info.get('Copied From URL') and
475 info.get('Copied From Rev') and
476 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000477
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000478 @staticmethod
479 def GetFileProperty(file, property_name):
480 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000481
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000482 Args:
483 file: The file to check
484 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000485
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000486 Returns:
487 The value of the property, which will be the empty string if the property
488 is not set on the file. If the file is not under version control, the
489 empty string is also returned.
490 """
491 output = SVN.Capture(["propget", property_name, file])
492 if (output.startswith("svn: ") and
493 output.endswith("is not under version control")):
494 return ""
495 else:
496 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000497
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000498 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000499 def DiffItem(filename, full_move=False):
500 """Diffs a single file.
501
502 Be sure to be in the appropriate directory before calling to have the
503 expected relative path."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000504 # Use svn info output instead of os.path.isdir because the latter fails
505 # when the file is deleted.
506 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
507 return None
508 # If the user specified a custom diff command in their svn config file,
509 # then it'll be used when we do svn diff, which we don't want to happen
510 # since we want the unified diff. Using --diff-cmd=diff doesn't always
511 # work, since they can have another diff executable in their path that
512 # gives different line endings. So we use a bogus temp directory as the
513 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000514 bogus_dir = tempfile.mkdtemp()
515 try:
516 # Grabs the diff data.
517 data = SVN.Capture(["diff", "--config-dir", bogus_dir, filename], None)
518 if data:
519 pass
520 elif SVN.IsMoved(filename):
521 if full_move:
522 file_content = gclient_utils.FileRead(filename, 'rb')
523 # Prepend '+' to every lines.
524 file_content = ['+' + i for i in file_content.splitlines(True)]
525 nb_lines = len(file_content)
526 # We need to use / since patch on unix will fail otherwise.
527 filename = filename.replace('\\', '/')
528 data = "Index: %s\n" % filename
529 data += '=' * 67 + '\n'
530 # Note: Should we use /dev/null instead?
531 data += "--- %s\n" % filename
532 data += "+++ %s\n" % filename
533 data += "@@ -0,0 +1,%d @@\n" % nb_lines
534 data += ''.join(file_content)
535 else:
536 # svn diff on a mv/cp'd file outputs nothing.
537 # We put in an empty Index entry so upload.py knows about them.
538 data = "Index: %s\n" % filename
539 else:
540 # The file is not modified anymore. It should be removed from the set.
541 pass
542 finally:
543 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000544 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000545
546 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000547 def GenerateDiff(filenames, root=None, full_move=False):
548 """Returns a string containing the diff for the given file list.
549
550 The files in the list should either be absolute paths or relative to the
551 given root. If no root directory is provided, the repository root will be
552 used.
553 The diff will always use relative paths.
554 """
555 previous_cwd = os.getcwd()
556 root = os.path.join(root or SVN.GetCheckoutRoot(previous_cwd), '')
557 def RelativePath(path, root):
558 """We must use relative paths."""
559 if path.startswith(root):
560 return path[len(root):]
561 return path
562 try:
563 os.chdir(root)
564 diff = "".join(filter(None,
565 [SVN.DiffItem(RelativePath(f, root),
566 full_move=full_move)
567 for f in filenames]))
568 finally:
569 os.chdir(previous_cwd)
570 return diff
571
572
573 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000574 def GetEmail(repo_root):
575 """Retrieves the svn account which we assume is an email address."""
576 infos = SVN.CaptureInfo(repo_root)
577 uuid = infos.get('UUID')
578 root = infos.get('Repository Root')
579 if not root:
580 return None
581
582 # Should check for uuid but it is incorrectly saved for https creds.
583 realm = root.rsplit('/', 1)[0]
584 if root.startswith('https') or not uuid:
585 regexp = re.compile(r'<%s:\d+>.*' % realm)
586 else:
587 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
588 if regexp is None:
589 return None
590 if sys.platform.startswith('win'):
591 if not 'APPDATA' in os.environ:
592 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000593 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
594 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000595 else:
596 if not 'HOME' in os.environ:
597 return None
598 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
599 'svn.simple')
600 for credfile in os.listdir(auth_dir):
601 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
602 if regexp.match(cred_info.get('svn:realmstring')):
603 return cred_info.get('username')
604
605 @staticmethod
606 def ReadSimpleAuth(filename):
607 f = open(filename, 'r')
608 values = {}
609 def ReadOneItem(type):
610 m = re.match(r'%s (\d+)' % type, f.readline())
611 if not m:
612 return None
613 data = f.read(int(m.group(1)))
614 if f.read(1) != '\n':
615 return None
616 return data
617
618 while True:
619 key = ReadOneItem('K')
620 if not key:
621 break
622 value = ReadOneItem('V')
623 if not value:
624 break
625 values[key] = value
626 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000627
628 @staticmethod
629 def GetCheckoutRoot(directory):
630 """Returns the top level directory of the current repository.
631
632 The directory is returned as an absolute path.
633 """
634 infos = SVN.CaptureInfo(directory, print_error=False)
635 cur_dir_repo_root = infos.get("Repository Root")
636 if not cur_dir_repo_root:
637 return None
638
639 while True:
640 parent = os.path.dirname(directory)
641 if (SVN.CaptureInfo(parent, print_error=False).get(
642 "Repository Root") != cur_dir_repo_root):
643 break
644 directory = parent
645 return directory