blob: 3481219ba831fd9ed862c350a72c7e8c5b459c3c [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 = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000336
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000337 def CaptureMatchingLines(line):
338 match = compiled_pattern.search(line)
339 if match:
340 file_list.append(match.group(1))
341 if line.startswith('svn: '):
342 # We can't raise an exception. We can't alias a variable. Use a cheap
343 # way.
344 failure.append(True)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000345
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000346 try:
347 SVN.RunAndFilterOutput(args,
348 in_directory,
349 options.verbose,
350 True,
351 CaptureMatchingLines)
352 except gclient_utils.Error:
353 # We enforce that some progress has been made.
354 if len(failure) and len(file_list) > previous_list_len:
355 if args[0] == 'checkout':
356 args = args[:]
357 # An aborted checkout is now an update.
358 args[0] = 'update'
359 continue
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000360 # No progress was made, bail out.
361 raise
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000362 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000363
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000364 @staticmethod
365 def RunAndFilterOutput(args,
366 in_directory,
367 print_messages,
368 print_stdout,
369 filter):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000370 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000371
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000372 stdout is passed line-by-line to the given filter function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000373 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000374
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000375 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000376 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000377 in_directory: The directory where svn is to be run.
378 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000379 which commands are being run.
380 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000381 filter: A function taking one argument (a string) which will be
382 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000383 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000384
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000385 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000386 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000387 """
388 command = [SVN.COMMAND]
389 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000390 gclient_utils.SubprocessCallAndFilter(command,
391 in_directory,
392 print_messages,
393 print_stdout,
394 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000395
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000396 @staticmethod
397 def CaptureInfo(relpath, in_directory=None, print_error=True):
398 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000399
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000400 Args:
401 relpath: The directory where the working copy resides relative to
402 the directory given by in_directory.
403 in_directory: The directory where svn is to be run.
404 """
405 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
406 dom = gclient_utils.ParseXML(output)
407 result = {}
408 if dom:
409 GetNamedNodeText = gclient_utils.GetNamedNodeText
410 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
411 def C(item, f):
412 if item is not None: return f(item)
413 # /info/entry/
414 # url
415 # reposityory/(root|uuid)
416 # wc-info/(schedule|depth)
417 # commit/(author|date)
418 # str() the results because they may be returned as Unicode, which
419 # interferes with the higher layers matching up things in the deps
420 # dictionary.
421 # TODO(maruel): Fix at higher level instead (!)
422 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
423 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
424 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
425 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
426 'revision'),
427 int)
428 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
429 str)
430 # Differs across versions.
431 if result['Node Kind'] == 'dir':
432 result['Node Kind'] = 'directory'
433 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
434 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
435 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
436 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
437 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000438
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000439 @staticmethod
440 def CaptureHeadRevision(url):
441 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000442
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000443 Returns:
444 Int head revision
445 """
446 info = SVN.Capture(["info", "--xml", url], os.getcwd())
447 dom = xml.dom.minidom.parseString(info)
448 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000449
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000450 @staticmethod
451 def CaptureStatus(files):
452 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000453
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000454 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000455
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000456 Returns an array of (status, file) tuples."""
457 command = ["status", "--xml"]
458 if not files:
459 pass
460 elif isinstance(files, basestring):
461 command.append(files)
462 else:
463 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000464
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000465 status_letter = {
466 None: ' ',
467 '': ' ',
468 'added': 'A',
469 'conflicted': 'C',
470 'deleted': 'D',
471 'external': 'X',
472 'ignored': 'I',
473 'incomplete': '!',
474 'merged': 'G',
475 'missing': '!',
476 'modified': 'M',
477 'none': ' ',
478 'normal': ' ',
479 'obstructed': '~',
480 'replaced': 'R',
481 'unversioned': '?',
482 }
483 dom = gclient_utils.ParseXML(SVN.Capture(command))
484 results = []
485 if dom:
486 # /status/target/entry/(wc-status|commit|author|date)
487 for target in dom.getElementsByTagName('target'):
488 #base_path = target.getAttribute('path')
489 for entry in target.getElementsByTagName('entry'):
490 file_path = entry.getAttribute('path')
491 wc_status = entry.getElementsByTagName('wc-status')
492 assert len(wc_status) == 1
493 # Emulate svn 1.5 status ouput...
494 statuses = [' '] * 7
495 # Col 0
496 xml_item_status = wc_status[0].getAttribute('item')
497 if xml_item_status in status_letter:
498 statuses[0] = status_letter[xml_item_status]
499 else:
500 raise Exception('Unknown item status "%s"; please implement me!' %
501 xml_item_status)
502 # Col 1
503 xml_props_status = wc_status[0].getAttribute('props')
504 if xml_props_status == 'modified':
505 statuses[1] = 'M'
506 elif xml_props_status == 'conflicted':
507 statuses[1] = 'C'
508 elif (not xml_props_status or xml_props_status == 'none' or
509 xml_props_status == 'normal'):
510 pass
511 else:
512 raise Exception('Unknown props status "%s"; please implement me!' %
513 xml_props_status)
514 # Col 2
515 if wc_status[0].getAttribute('wc-locked') == 'true':
516 statuses[2] = 'L'
517 # Col 3
518 if wc_status[0].getAttribute('copied') == 'true':
519 statuses[3] = '+'
520 # Col 4
521 if wc_status[0].getAttribute('switched') == 'true':
522 statuses[4] = 'S'
523 # TODO(maruel): Col 5 and 6
524 item = (''.join(statuses), file_path)
525 results.append(item)
526 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000527
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000528 @staticmethod
529 def IsMoved(filename):
530 """Determine if a file has been added through svn mv"""
531 info = SVN.CaptureInfo(filename)
532 return (info.get('Copied From URL') and
533 info.get('Copied From Rev') and
534 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000535
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000536 @staticmethod
537 def GetFileProperty(file, property_name):
538 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000539
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000540 Args:
541 file: The file to check
542 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000543
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000544 Returns:
545 The value of the property, which will be the empty string if the property
546 is not set on the file. If the file is not under version control, the
547 empty string is also returned.
548 """
549 output = SVN.Capture(["propget", property_name, file])
550 if (output.startswith("svn: ") and
551 output.endswith("is not under version control")):
552 return ""
553 else:
554 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000555
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000556 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000557 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000558 """Diffs a single file.
559
560 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000561 expected relative path.
562 full_move means that move or copy operations should completely recreate the
563 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000564 # Use svn info output instead of os.path.isdir because the latter fails
565 # when the file is deleted.
566 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
567 return None
568 # If the user specified a custom diff command in their svn config file,
569 # then it'll be used when we do svn diff, which we don't want to happen
570 # since we want the unified diff. Using --diff-cmd=diff doesn't always
571 # work, since they can have another diff executable in their path that
572 # gives different line endings. So we use a bogus temp directory as the
573 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000574 bogus_dir = tempfile.mkdtemp()
575 try:
576 # Grabs the diff data.
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000577 command = ["diff", "--config-dir", bogus_dir, filename]
maruel@chromium.orgce3c8622010-01-07 02:18:16 +0000578 if revision:
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000579 command.extend(['--revision', revision])
580 data = SVN.Capture(command, None)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000581 if data:
582 pass
583 elif SVN.IsMoved(filename):
584 if full_move:
585 file_content = gclient_utils.FileRead(filename, 'rb')
586 # Prepend '+' to every lines.
587 file_content = ['+' + i for i in file_content.splitlines(True)]
588 nb_lines = len(file_content)
589 # We need to use / since patch on unix will fail otherwise.
590 filename = filename.replace('\\', '/')
591 data = "Index: %s\n" % filename
592 data += '=' * 67 + '\n'
593 # Note: Should we use /dev/null instead?
594 data += "--- %s\n" % filename
595 data += "+++ %s\n" % filename
596 data += "@@ -0,0 +1,%d @@\n" % nb_lines
597 data += ''.join(file_content)
598 else:
599 # svn diff on a mv/cp'd file outputs nothing.
600 # We put in an empty Index entry so upload.py knows about them.
601 data = "Index: %s\n" % filename
602 else:
603 # The file is not modified anymore. It should be removed from the set.
604 pass
605 finally:
606 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000607 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000608
609 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000610 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000611 """Returns a string containing the diff for the given file list.
612
613 The files in the list should either be absolute paths or relative to the
614 given root. If no root directory is provided, the repository root will be
615 used.
616 The diff will always use relative paths.
617 """
618 previous_cwd = os.getcwd()
619 root = os.path.join(root or SVN.GetCheckoutRoot(previous_cwd), '')
620 def RelativePath(path, root):
621 """We must use relative paths."""
622 if path.startswith(root):
623 return path[len(root):]
624 return path
625 try:
626 os.chdir(root)
627 diff = "".join(filter(None,
maruel@chromium.org0c401692010-01-07 02:44:20 +0000628 [SVN.DiffItem(RelativePath(f, root),
629 full_move=full_move,
630 revision=revision)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000631 for f in filenames]))
632 finally:
633 os.chdir(previous_cwd)
634 return diff
635
636
637 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000638 def GetEmail(repo_root):
639 """Retrieves the svn account which we assume is an email address."""
640 infos = SVN.CaptureInfo(repo_root)
641 uuid = infos.get('UUID')
642 root = infos.get('Repository Root')
643 if not root:
644 return None
645
646 # Should check for uuid but it is incorrectly saved for https creds.
647 realm = root.rsplit('/', 1)[0]
648 if root.startswith('https') or not uuid:
649 regexp = re.compile(r'<%s:\d+>.*' % realm)
650 else:
651 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
652 if regexp is None:
653 return None
654 if sys.platform.startswith('win'):
655 if not 'APPDATA' in os.environ:
656 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000657 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
658 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000659 else:
660 if not 'HOME' in os.environ:
661 return None
662 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
663 'svn.simple')
664 for credfile in os.listdir(auth_dir):
665 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
666 if regexp.match(cred_info.get('svn:realmstring')):
667 return cred_info.get('username')
668
669 @staticmethod
670 def ReadSimpleAuth(filename):
671 f = open(filename, 'r')
672 values = {}
673 def ReadOneItem(type):
674 m = re.match(r'%s (\d+)' % type, f.readline())
675 if not m:
676 return None
677 data = f.read(int(m.group(1)))
678 if f.read(1) != '\n':
679 return None
680 return data
681
682 while True:
683 key = ReadOneItem('K')
684 if not key:
685 break
686 value = ReadOneItem('V')
687 if not value:
688 break
689 values[key] = value
690 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000691
692 @staticmethod
693 def GetCheckoutRoot(directory):
694 """Returns the top level directory of the current repository.
695
696 The directory is returned as an absolute path.
697 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000698 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000699 infos = SVN.CaptureInfo(directory, print_error=False)
700 cur_dir_repo_root = infos.get("Repository Root")
701 if not cur_dir_repo_root:
702 return None
703
704 while True:
705 parent = os.path.dirname(directory)
706 if (SVN.CaptureInfo(parent, print_error=False).get(
707 "Repository Root") != cur_dir_repo_root):
708 break
709 directory = parent
710 return directory