blob: 90766038029bdbde7a456c613a11cc5abda9ddd1 [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
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000237 def GetCheckoutRoot(path):
238 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000239 """
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000240 root = GIT.Capture(['rev-parse', '--show-cdup'], path).strip()
241 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000242
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000243
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000244class SVN(object):
245 COMMAND = "svn"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000246
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000247 @staticmethod
248 def Run(args, in_directory):
249 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000250
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000251 Args:
252 args: A sequence of command line parameters to be passed to svn.
253 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000254
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000255 Raises:
256 Error: An error occurred while running the svn command.
257 """
258 c = [SVN.COMMAND]
259 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000260 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000261 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000262
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000263 @staticmethod
264 def Capture(args, in_directory=None, print_error=True):
265 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000266
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000267 Args:
268 args: A sequence of command line parameters to be passed to svn.
269 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000270
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000271 Returns:
272 The output sent to stdout as a string.
273 """
274 c = [SVN.COMMAND]
275 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000276
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000277 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
278 # the svn.exe executable, but shell=True makes subprocess on Linux fail
279 # when it's called with a list because it only tries to execute the
280 # first string ("svn").
281 stderr = None
282 if not print_error:
283 stderr = subprocess.PIPE
284 return subprocess.Popen(c,
285 cwd=in_directory,
286 shell=(sys.platform == 'win32'),
287 stdout=subprocess.PIPE,
288 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000289
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000290 @staticmethod
291 def RunAndGetFileList(options, args, in_directory, file_list):
292 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000293
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000294 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000295
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000296 svn's stdout is parsed to collect a list of files checked out or updated.
297 These files are appended to file_list. svn's stdout is also printed to
298 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000299
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000300 Args:
301 options: command line options to gclient
302 args: A sequence of command line parameters to be passed to svn.
303 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000304
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000305 Raises:
306 Error: An error occurred while running the svn command.
307 """
308 command = [SVN.COMMAND]
309 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000310
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000311 # svn update and svn checkout use the same pattern: the first three columns
312 # are for file status, property status, and lock status. This is followed
313 # by two spaces, and then the path to the file.
314 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000315
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000316 # The first three columns of svn status are the same as for svn update and
317 # svn checkout. The next three columns indicate addition-with-history,
318 # switch, and remote lock status. This is followed by one space, and then
319 # the path to the file.
320 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000321
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000322 # args[0] must be a supported command. This will blow up if it's something
323 # else, which is good. Note that the patterns are only effective when
324 # these commands are used in their ordinary forms, the patterns are invalid
325 # for "svn status --show-updates", for example.
326 pattern = {
327 'checkout': update_pattern,
328 'status': status_pattern,
329 'update': update_pattern,
330 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000331 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000332 # Place an upper limit.
333 for i in range(1, 10):
334 previous_list_len = len(file_list)
335 failure = []
336 def CaptureMatchingLines(line):
337 match = compiled_pattern.search(line)
338 if match:
339 file_list.append(match.group(1))
340 if line.startswith('svn: '):
341 # We can't raise an exception. We can't alias a variable. Use a cheap
342 # way.
343 failure.append(True)
344 try:
345 SVN.RunAndFilterOutput(args,
346 in_directory,
347 options.verbose,
348 True,
349 CaptureMatchingLines)
350 except gclient_utils.Error:
351 # We enforce that some progress has been made.
352 if len(failure) and len(file_list) > previous_list_len:
353 if args[0] == 'checkout':
354 args = args[:]
355 # An aborted checkout is now an update.
356 args[0] = 'update'
357 continue
358 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000359
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000360 @staticmethod
361 def RunAndFilterOutput(args,
362 in_directory,
363 print_messages,
364 print_stdout,
365 filter):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000366 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000367
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000368 stdout is passed line-by-line to the given filter function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000369 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000370
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000371 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000372 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000373 in_directory: The directory where svn is to be run.
374 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000375 which commands are being run.
376 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000377 filter: A function taking one argument (a string) which will be
378 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000379 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000380
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000381 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000382 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000383 """
384 command = [SVN.COMMAND]
385 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000386 gclient_utils.SubprocessCallAndFilter(command,
387 in_directory,
388 print_messages,
389 print_stdout,
390 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000391
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000392 @staticmethod
393 def CaptureInfo(relpath, in_directory=None, print_error=True):
394 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000395
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000396 Args:
397 relpath: The directory where the working copy resides relative to
398 the directory given by in_directory.
399 in_directory: The directory where svn is to be run.
400 """
401 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
402 dom = gclient_utils.ParseXML(output)
403 result = {}
404 if dom:
405 GetNamedNodeText = gclient_utils.GetNamedNodeText
406 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
407 def C(item, f):
408 if item is not None: return f(item)
409 # /info/entry/
410 # url
411 # reposityory/(root|uuid)
412 # wc-info/(schedule|depth)
413 # commit/(author|date)
414 # str() the results because they may be returned as Unicode, which
415 # interferes with the higher layers matching up things in the deps
416 # dictionary.
417 # TODO(maruel): Fix at higher level instead (!)
418 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
419 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
420 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
421 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
422 'revision'),
423 int)
424 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
425 str)
426 # Differs across versions.
427 if result['Node Kind'] == 'dir':
428 result['Node Kind'] = 'directory'
429 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
430 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
431 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
432 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
433 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000434
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000435 @staticmethod
436 def CaptureHeadRevision(url):
437 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000438
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000439 Returns:
440 Int head revision
441 """
442 info = SVN.Capture(["info", "--xml", url], os.getcwd())
443 dom = xml.dom.minidom.parseString(info)
444 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000445
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000446 @staticmethod
447 def CaptureStatus(files):
448 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000449
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000450 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000451
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000452 Returns an array of (status, file) tuples."""
453 command = ["status", "--xml"]
454 if not files:
455 pass
456 elif isinstance(files, basestring):
457 command.append(files)
458 else:
459 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000460
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000461 status_letter = {
462 None: ' ',
463 '': ' ',
464 'added': 'A',
465 'conflicted': 'C',
466 'deleted': 'D',
467 'external': 'X',
468 'ignored': 'I',
469 'incomplete': '!',
470 'merged': 'G',
471 'missing': '!',
472 'modified': 'M',
473 'none': ' ',
474 'normal': ' ',
475 'obstructed': '~',
476 'replaced': 'R',
477 'unversioned': '?',
478 }
479 dom = gclient_utils.ParseXML(SVN.Capture(command))
480 results = []
481 if dom:
482 # /status/target/entry/(wc-status|commit|author|date)
483 for target in dom.getElementsByTagName('target'):
484 #base_path = target.getAttribute('path')
485 for entry in target.getElementsByTagName('entry'):
486 file_path = entry.getAttribute('path')
487 wc_status = entry.getElementsByTagName('wc-status')
488 assert len(wc_status) == 1
489 # Emulate svn 1.5 status ouput...
490 statuses = [' '] * 7
491 # Col 0
492 xml_item_status = wc_status[0].getAttribute('item')
493 if xml_item_status in status_letter:
494 statuses[0] = status_letter[xml_item_status]
495 else:
496 raise Exception('Unknown item status "%s"; please implement me!' %
497 xml_item_status)
498 # Col 1
499 xml_props_status = wc_status[0].getAttribute('props')
500 if xml_props_status == 'modified':
501 statuses[1] = 'M'
502 elif xml_props_status == 'conflicted':
503 statuses[1] = 'C'
504 elif (not xml_props_status or xml_props_status == 'none' or
505 xml_props_status == 'normal'):
506 pass
507 else:
508 raise Exception('Unknown props status "%s"; please implement me!' %
509 xml_props_status)
510 # Col 2
511 if wc_status[0].getAttribute('wc-locked') == 'true':
512 statuses[2] = 'L'
513 # Col 3
514 if wc_status[0].getAttribute('copied') == 'true':
515 statuses[3] = '+'
516 # Col 4
517 if wc_status[0].getAttribute('switched') == 'true':
518 statuses[4] = 'S'
519 # TODO(maruel): Col 5 and 6
520 item = (''.join(statuses), file_path)
521 results.append(item)
522 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000523
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000524 @staticmethod
525 def IsMoved(filename):
526 """Determine if a file has been added through svn mv"""
527 info = SVN.CaptureInfo(filename)
528 return (info.get('Copied From URL') and
529 info.get('Copied From Rev') and
530 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000531
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000532 @staticmethod
533 def GetFileProperty(file, property_name):
534 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000535
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000536 Args:
537 file: The file to check
538 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000539
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000540 Returns:
541 The value of the property, which will be the empty string if the property
542 is not set on the file. If the file is not under version control, the
543 empty string is also returned.
544 """
545 output = SVN.Capture(["propget", property_name, file])
546 if (output.startswith("svn: ") and
547 output.endswith("is not under version control")):
548 return ""
549 else:
550 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000551
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000552 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000553 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000554 """Diffs a single file.
555
556 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000557 expected relative path.
558 full_move means that move or copy operations should completely recreate the
559 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000560 # Use svn info output instead of os.path.isdir because the latter fails
561 # when the file is deleted.
562 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
563 return None
564 # If the user specified a custom diff command in their svn config file,
565 # then it'll be used when we do svn diff, which we don't want to happen
566 # since we want the unified diff. Using --diff-cmd=diff doesn't always
567 # work, since they can have another diff executable in their path that
568 # gives different line endings. So we use a bogus temp directory as the
569 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000570 bogus_dir = tempfile.mkdtemp()
571 try:
572 # Grabs the diff data.
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000573 command = ["diff", "--config-dir", bogus_dir, filename]
maruel@chromium.orgce3c8622010-01-07 02:18:16 +0000574 if revision:
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000575 command.extend(['--revision', revision])
576 data = SVN.Capture(command, None)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000577 if data:
578 pass
579 elif SVN.IsMoved(filename):
580 if full_move:
581 file_content = gclient_utils.FileRead(filename, 'rb')
582 # Prepend '+' to every lines.
583 file_content = ['+' + i for i in file_content.splitlines(True)]
584 nb_lines = len(file_content)
585 # We need to use / since patch on unix will fail otherwise.
586 filename = filename.replace('\\', '/')
587 data = "Index: %s\n" % filename
588 data += '=' * 67 + '\n'
589 # Note: Should we use /dev/null instead?
590 data += "--- %s\n" % filename
591 data += "+++ %s\n" % filename
592 data += "@@ -0,0 +1,%d @@\n" % nb_lines
593 data += ''.join(file_content)
594 else:
595 # svn diff on a mv/cp'd file outputs nothing.
596 # We put in an empty Index entry so upload.py knows about them.
597 data = "Index: %s\n" % filename
598 else:
599 # The file is not modified anymore. It should be removed from the set.
600 pass
601 finally:
602 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000603 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000604
605 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000606 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000607 """Returns a string containing the diff for the given file list.
608
609 The files in the list should either be absolute paths or relative to the
610 given root. If no root directory is provided, the repository root will be
611 used.
612 The diff will always use relative paths.
613 """
614 previous_cwd = os.getcwd()
615 root = os.path.join(root or SVN.GetCheckoutRoot(previous_cwd), '')
616 def RelativePath(path, root):
617 """We must use relative paths."""
618 if path.startswith(root):
619 return path[len(root):]
620 return path
621 try:
622 os.chdir(root)
623 diff = "".join(filter(None,
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000624 [SVN.DiffItem(RelativePath(f, root, revision),
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000625 full_move=full_move)
626 for f in filenames]))
627 finally:
628 os.chdir(previous_cwd)
629 return diff
630
631
632 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000633 def GetEmail(repo_root):
634 """Retrieves the svn account which we assume is an email address."""
635 infos = SVN.CaptureInfo(repo_root)
636 uuid = infos.get('UUID')
637 root = infos.get('Repository Root')
638 if not root:
639 return None
640
641 # Should check for uuid but it is incorrectly saved for https creds.
642 realm = root.rsplit('/', 1)[0]
643 if root.startswith('https') or not uuid:
644 regexp = re.compile(r'<%s:\d+>.*' % realm)
645 else:
646 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
647 if regexp is None:
648 return None
649 if sys.platform.startswith('win'):
650 if not 'APPDATA' in os.environ:
651 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000652 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
653 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000654 else:
655 if not 'HOME' in os.environ:
656 return None
657 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
658 'svn.simple')
659 for credfile in os.listdir(auth_dir):
660 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
661 if regexp.match(cred_info.get('svn:realmstring')):
662 return cred_info.get('username')
663
664 @staticmethod
665 def ReadSimpleAuth(filename):
666 f = open(filename, 'r')
667 values = {}
668 def ReadOneItem(type):
669 m = re.match(r'%s (\d+)' % type, f.readline())
670 if not m:
671 return None
672 data = f.read(int(m.group(1)))
673 if f.read(1) != '\n':
674 return None
675 return data
676
677 while True:
678 key = ReadOneItem('K')
679 if not key:
680 break
681 value = ReadOneItem('V')
682 if not value:
683 break
684 values[key] = value
685 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000686
687 @staticmethod
688 def GetCheckoutRoot(directory):
689 """Returns the top level directory of the current repository.
690
691 The directory is returned as an absolute path.
692 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000693 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000694 infos = SVN.CaptureInfo(directory, print_error=False)
695 cur_dir_repo_root = infos.get("Repository Root")
696 if not cur_dir_repo_root:
697 return None
698
699 while True:
700 parent = os.path.dirname(directory)
701 if (SVN.CaptureInfo(parent, print_error=False).get(
702 "Repository Root") != cur_dir_repo_root):
703 break
704 directory = parent
705 return directory