blob: db4711e366bb7b1472d067248dd51e139b777493 [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
maruel@chromium.orga9371762009-12-22 18:27:38 +0000180 def GenerateDiff(cwd, branch=None, full_move=False):
181 """Diffs against the upstream branch or optionally another branch.
182
183 full_move means that move or copy operations should completely recreate the
184 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000185 if not branch:
186 branch = GIT.GetUpstream(cwd)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000187 command = ['diff-tree', '-p', '--no-prefix', branch, 'HEAD']
188 if not full_move:
189 command.append('-C')
190 diff = GIT.Capture(command, cwd).splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000191 for i in range(len(diff)):
192 # In the case of added files, replace /dev/null with the path to the
193 # file being added.
194 if diff[i].startswith('--- /dev/null'):
195 diff[i] = '--- %s' % diff[i+1][4:]
196 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000197
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000198 @staticmethod
199 def GetPatchName(cwd):
200 """Constructs a name for this patch."""
201 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd).strip()
202 return "%s-%s" % (GIT.GetBranch(cwd), short_sha)
203
204 @staticmethod
205 def GetCheckoutRoot(cwd):
206 """Returns the top level directory of the current repository.
207
208 The directory is returned as an absolute path.
209 """
210 return os.path.abspath(GIT.Capture(['rev-parse', '--show-cdup'],
211 cwd).strip())
212
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000213
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000214class SVN(object):
215 COMMAND = "svn"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000216
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000217 @staticmethod
218 def Run(args, in_directory):
219 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000220
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000221 Args:
222 args: A sequence of command line parameters to be passed to svn.
223 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000224
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000225 Raises:
226 Error: An error occurred while running the svn command.
227 """
228 c = [SVN.COMMAND]
229 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000230 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000231 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000232
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000233 @staticmethod
234 def Capture(args, in_directory=None, print_error=True):
235 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000236
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000237 Args:
238 args: A sequence of command line parameters to be passed to svn.
239 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000240
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000241 Returns:
242 The output sent to stdout as a string.
243 """
244 c = [SVN.COMMAND]
245 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000246
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000247 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
248 # the svn.exe executable, but shell=True makes subprocess on Linux fail
249 # when it's called with a list because it only tries to execute the
250 # first string ("svn").
251 stderr = None
252 if not print_error:
253 stderr = subprocess.PIPE
254 return subprocess.Popen(c,
255 cwd=in_directory,
256 shell=(sys.platform == 'win32'),
257 stdout=subprocess.PIPE,
258 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000259
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000260 @staticmethod
261 def RunAndGetFileList(options, args, in_directory, file_list):
262 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000263
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000264 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000265
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000266 svn's stdout is parsed to collect a list of files checked out or updated.
267 These files are appended to file_list. svn's stdout is also printed to
268 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000269
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000270 Args:
271 options: command line options to gclient
272 args: A sequence of command line parameters to be passed to svn.
273 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000274
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000275 Raises:
276 Error: An error occurred while running the svn command.
277 """
278 command = [SVN.COMMAND]
279 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000280
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000281 # svn update and svn checkout use the same pattern: the first three columns
282 # are for file status, property status, and lock status. This is followed
283 # by two spaces, and then the path to the file.
284 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000285
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000286 # The first three columns of svn status are the same as for svn update and
287 # svn checkout. The next three columns indicate addition-with-history,
288 # switch, and remote lock status. This is followed by one space, and then
289 # the path to the file.
290 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000291
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000292 # args[0] must be a supported command. This will blow up if it's something
293 # else, which is good. Note that the patterns are only effective when
294 # these commands are used in their ordinary forms, the patterns are invalid
295 # for "svn status --show-updates", for example.
296 pattern = {
297 'checkout': update_pattern,
298 'status': status_pattern,
299 'update': update_pattern,
300 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000301 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000302 # Place an upper limit.
303 for i in range(1, 10):
304 previous_list_len = len(file_list)
305 failure = []
306 def CaptureMatchingLines(line):
307 match = compiled_pattern.search(line)
308 if match:
309 file_list.append(match.group(1))
310 if line.startswith('svn: '):
311 # We can't raise an exception. We can't alias a variable. Use a cheap
312 # way.
313 failure.append(True)
314 try:
315 SVN.RunAndFilterOutput(args,
316 in_directory,
317 options.verbose,
318 True,
319 CaptureMatchingLines)
320 except gclient_utils.Error:
321 # We enforce that some progress has been made.
322 if len(failure) and len(file_list) > previous_list_len:
323 if args[0] == 'checkout':
324 args = args[:]
325 # An aborted checkout is now an update.
326 args[0] = 'update'
327 continue
328 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000329
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000330 @staticmethod
331 def RunAndFilterOutput(args,
332 in_directory,
333 print_messages,
334 print_stdout,
335 filter):
336 """Runs svn checkout, update, status, or diff, optionally outputting
337 to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000338
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000339 The first item in args must be either "checkout", "update",
340 "status", or "diff".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000341
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000342 svn's stdout is passed line-by-line to the given filter function. If
343 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000344
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000345 Args:
346 args: A sequence of command line parameters to be passed to svn.
347 in_directory: The directory where svn is to be run.
348 print_messages: Whether to print status messages to stdout about
349 which Subversion commands are being run.
350 print_stdout: Whether to forward Subversion's output to stdout.
351 filter: A function taking one argument (a string) which will be
352 passed each line (with the ending newline character removed) of
353 Subversion's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000354
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000355 Raises:
356 Error: An error occurred while running the svn command.
357 """
358 command = [SVN.COMMAND]
359 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000360
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000361 gclient_utils.SubprocessCallAndFilter(command,
362 in_directory,
363 print_messages,
364 print_stdout,
365 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000366
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000367 @staticmethod
368 def CaptureInfo(relpath, in_directory=None, print_error=True):
369 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000370
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000371 Args:
372 relpath: The directory where the working copy resides relative to
373 the directory given by in_directory.
374 in_directory: The directory where svn is to be run.
375 """
376 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
377 dom = gclient_utils.ParseXML(output)
378 result = {}
379 if dom:
380 GetNamedNodeText = gclient_utils.GetNamedNodeText
381 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
382 def C(item, f):
383 if item is not None: return f(item)
384 # /info/entry/
385 # url
386 # reposityory/(root|uuid)
387 # wc-info/(schedule|depth)
388 # commit/(author|date)
389 # str() the results because they may be returned as Unicode, which
390 # interferes with the higher layers matching up things in the deps
391 # dictionary.
392 # TODO(maruel): Fix at higher level instead (!)
393 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
394 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
395 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
396 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
397 'revision'),
398 int)
399 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
400 str)
401 # Differs across versions.
402 if result['Node Kind'] == 'dir':
403 result['Node Kind'] = 'directory'
404 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
405 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
406 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
407 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
408 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000409
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000410 @staticmethod
411 def CaptureHeadRevision(url):
412 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000413
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000414 Returns:
415 Int head revision
416 """
417 info = SVN.Capture(["info", "--xml", url], os.getcwd())
418 dom = xml.dom.minidom.parseString(info)
419 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000420
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000421 @staticmethod
422 def CaptureStatus(files):
423 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000424
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000425 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000426
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000427 Returns an array of (status, file) tuples."""
428 command = ["status", "--xml"]
429 if not files:
430 pass
431 elif isinstance(files, basestring):
432 command.append(files)
433 else:
434 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000435
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000436 status_letter = {
437 None: ' ',
438 '': ' ',
439 'added': 'A',
440 'conflicted': 'C',
441 'deleted': 'D',
442 'external': 'X',
443 'ignored': 'I',
444 'incomplete': '!',
445 'merged': 'G',
446 'missing': '!',
447 'modified': 'M',
448 'none': ' ',
449 'normal': ' ',
450 'obstructed': '~',
451 'replaced': 'R',
452 'unversioned': '?',
453 }
454 dom = gclient_utils.ParseXML(SVN.Capture(command))
455 results = []
456 if dom:
457 # /status/target/entry/(wc-status|commit|author|date)
458 for target in dom.getElementsByTagName('target'):
459 #base_path = target.getAttribute('path')
460 for entry in target.getElementsByTagName('entry'):
461 file_path = entry.getAttribute('path')
462 wc_status = entry.getElementsByTagName('wc-status')
463 assert len(wc_status) == 1
464 # Emulate svn 1.5 status ouput...
465 statuses = [' '] * 7
466 # Col 0
467 xml_item_status = wc_status[0].getAttribute('item')
468 if xml_item_status in status_letter:
469 statuses[0] = status_letter[xml_item_status]
470 else:
471 raise Exception('Unknown item status "%s"; please implement me!' %
472 xml_item_status)
473 # Col 1
474 xml_props_status = wc_status[0].getAttribute('props')
475 if xml_props_status == 'modified':
476 statuses[1] = 'M'
477 elif xml_props_status == 'conflicted':
478 statuses[1] = 'C'
479 elif (not xml_props_status or xml_props_status == 'none' or
480 xml_props_status == 'normal'):
481 pass
482 else:
483 raise Exception('Unknown props status "%s"; please implement me!' %
484 xml_props_status)
485 # Col 2
486 if wc_status[0].getAttribute('wc-locked') == 'true':
487 statuses[2] = 'L'
488 # Col 3
489 if wc_status[0].getAttribute('copied') == 'true':
490 statuses[3] = '+'
491 # Col 4
492 if wc_status[0].getAttribute('switched') == 'true':
493 statuses[4] = 'S'
494 # TODO(maruel): Col 5 and 6
495 item = (''.join(statuses), file_path)
496 results.append(item)
497 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000498
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000499 @staticmethod
500 def IsMoved(filename):
501 """Determine if a file has been added through svn mv"""
502 info = SVN.CaptureInfo(filename)
503 return (info.get('Copied From URL') and
504 info.get('Copied From Rev') and
505 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000506
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000507 @staticmethod
508 def GetFileProperty(file, property_name):
509 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000510
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000511 Args:
512 file: The file to check
513 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000514
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000515 Returns:
516 The value of the property, which will be the empty string if the property
517 is not set on the file. If the file is not under version control, the
518 empty string is also returned.
519 """
520 output = SVN.Capture(["propget", property_name, file])
521 if (output.startswith("svn: ") and
522 output.endswith("is not under version control")):
523 return ""
524 else:
525 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000526
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000527 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000528 def DiffItem(filename, full_move=False):
529 """Diffs a single file.
530
531 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000532 expected relative path.
533 full_move means that move or copy operations should completely recreate the
534 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000535 # Use svn info output instead of os.path.isdir because the latter fails
536 # when the file is deleted.
537 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
538 return None
539 # If the user specified a custom diff command in their svn config file,
540 # then it'll be used when we do svn diff, which we don't want to happen
541 # since we want the unified diff. Using --diff-cmd=diff doesn't always
542 # work, since they can have another diff executable in their path that
543 # gives different line endings. So we use a bogus temp directory as the
544 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000545 bogus_dir = tempfile.mkdtemp()
546 try:
547 # Grabs the diff data.
548 data = SVN.Capture(["diff", "--config-dir", bogus_dir, filename], None)
549 if data:
550 pass
551 elif SVN.IsMoved(filename):
552 if full_move:
553 file_content = gclient_utils.FileRead(filename, 'rb')
554 # Prepend '+' to every lines.
555 file_content = ['+' + i for i in file_content.splitlines(True)]
556 nb_lines = len(file_content)
557 # We need to use / since patch on unix will fail otherwise.
558 filename = filename.replace('\\', '/')
559 data = "Index: %s\n" % filename
560 data += '=' * 67 + '\n'
561 # Note: Should we use /dev/null instead?
562 data += "--- %s\n" % filename
563 data += "+++ %s\n" % filename
564 data += "@@ -0,0 +1,%d @@\n" % nb_lines
565 data += ''.join(file_content)
566 else:
567 # svn diff on a mv/cp'd file outputs nothing.
568 # We put in an empty Index entry so upload.py knows about them.
569 data = "Index: %s\n" % filename
570 else:
571 # The file is not modified anymore. It should be removed from the set.
572 pass
573 finally:
574 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000575 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000576
577 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000578 def GenerateDiff(filenames, root=None, full_move=False):
579 """Returns a string containing the diff for the given file list.
580
581 The files in the list should either be absolute paths or relative to the
582 given root. If no root directory is provided, the repository root will be
583 used.
584 The diff will always use relative paths.
585 """
586 previous_cwd = os.getcwd()
587 root = os.path.join(root or SVN.GetCheckoutRoot(previous_cwd), '')
588 def RelativePath(path, root):
589 """We must use relative paths."""
590 if path.startswith(root):
591 return path[len(root):]
592 return path
593 try:
594 os.chdir(root)
595 diff = "".join(filter(None,
596 [SVN.DiffItem(RelativePath(f, root),
597 full_move=full_move)
598 for f in filenames]))
599 finally:
600 os.chdir(previous_cwd)
601 return diff
602
603
604 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000605 def GetEmail(repo_root):
606 """Retrieves the svn account which we assume is an email address."""
607 infos = SVN.CaptureInfo(repo_root)
608 uuid = infos.get('UUID')
609 root = infos.get('Repository Root')
610 if not root:
611 return None
612
613 # Should check for uuid but it is incorrectly saved for https creds.
614 realm = root.rsplit('/', 1)[0]
615 if root.startswith('https') or not uuid:
616 regexp = re.compile(r'<%s:\d+>.*' % realm)
617 else:
618 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
619 if regexp is None:
620 return None
621 if sys.platform.startswith('win'):
622 if not 'APPDATA' in os.environ:
623 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000624 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
625 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000626 else:
627 if not 'HOME' in os.environ:
628 return None
629 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
630 'svn.simple')
631 for credfile in os.listdir(auth_dir):
632 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
633 if regexp.match(cred_info.get('svn:realmstring')):
634 return cred_info.get('username')
635
636 @staticmethod
637 def ReadSimpleAuth(filename):
638 f = open(filename, 'r')
639 values = {}
640 def ReadOneItem(type):
641 m = re.match(r'%s (\d+)' % type, f.readline())
642 if not m:
643 return None
644 data = f.read(int(m.group(1)))
645 if f.read(1) != '\n':
646 return None
647 return data
648
649 while True:
650 key = ReadOneItem('K')
651 if not key:
652 break
653 value = ReadOneItem('V')
654 if not value:
655 break
656 values[key] = value
657 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000658
659 @staticmethod
660 def GetCheckoutRoot(directory):
661 """Returns the top level directory of the current repository.
662
663 The directory is returned as an absolute path.
664 """
665 infos = SVN.CaptureInfo(directory, print_error=False)
666 cur_dir_repo_root = infos.get("Repository Root")
667 if not cur_dir_repo_root:
668 return None
669
670 while True:
671 parent = os.path.dirname(directory)
672 if (SVN.CaptureInfo(parent, print_error=False).get(
673 "Repository Root") != cur_dir_repo_root):
674 break
675 directory = parent
676 return directory