blob: 1dce4bd7ccd9d35dd6e3a8b09788c70c03a8036d [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
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +00007import glob
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00008import os
9import re
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000010import shutil
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000011import subprocess
12import sys
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000013import tempfile
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000014import xml.dom.minidom
15
16import gclient_utils
17
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000018def ValidateEmail(email):
19 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
20 is not None)
21
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000022
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000023def GetCasedPath(path):
24 """Elcheapos way to get the real path case on Windows."""
25 if sys.platform.startswith('win') and os.path.exists(path):
26 # Reconstruct the path.
27 path = os.path.abspath(path)
28 paths = path.split('\\')
29 for i in range(len(paths)):
30 if i == 0:
31 # Skip drive letter.
32 continue
33 subpath = '\\'.join(paths[:i+1])
34 prev = len('\\'.join(paths[:i]))
35 # glob.glob will return the cased path for the last item only. This is why
36 # we are calling it in a loop. Extract the data we want and put it back
37 # into the list.
38 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)]
39 path = '\\'.join(paths)
40 return path
41
42
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000043class GIT(object):
44 COMMAND = "git"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000045
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000046 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000047 def Capture(args, in_directory=None, print_error=True, error_ok=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000048 """Runs git, capturing output sent to stdout as a string.
49
50 Args:
51 args: A sequence of command line parameters to be passed to git.
52 in_directory: The directory where git is to be run.
53
54 Returns:
55 The output sent to stdout as a string.
56 """
57 c = [GIT.COMMAND]
58 c.extend(args)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000059 try:
60 return gclient_utils.CheckCall(c, in_directory, print_error)
61 except gclient_utils.CheckCallError:
62 if error_ok:
nasser@codeaurora.orgcd968c12010-02-01 06:05:00 +000063 return ('', '')
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000064 raise
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000065
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000066 @staticmethod
67 def CaptureStatus(files, upstream_branch='origin'):
68 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000069
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000070 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000071
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000072 Returns an array of (status, file) tuples."""
73 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch]
74 if not files:
75 pass
76 elif isinstance(files, basestring):
77 command.append(files)
78 else:
79 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000080
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000081 status = GIT.Capture(command)[0].rstrip()
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000082 results = []
83 if status:
84 for statusline in status.split('\n'):
85 m = re.match('^(\w)\t(.+)$', statusline)
86 if not m:
87 raise Exception("status currently unsupported: %s" % statusline)
88 results.append(('%s ' % m.group(1), m.group(2)))
89 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000090
maruel@chromium.orgc78f2462009-11-21 01:20:57 +000091 @staticmethod
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000092 def RunAndFilterOutput(args,
93 in_directory,
94 print_messages,
95 print_stdout,
96 filter):
97 """Runs a command, optionally outputting to stdout.
98
99 stdout is passed line-by-line to the given filter function. If
100 print_stdout is true, it is also printed to sys.stdout as in Run.
101
102 Args:
103 args: A sequence of command line parameters to be passed.
msb@chromium.orgd6504212010-01-13 17:34:31 +0000104 in_directory: The directory where git is to be run.
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000105 print_messages: Whether to print status messages to stdout about
106 which commands are being run.
107 print_stdout: Whether to forward program's output to stdout.
108 filter: A function taking one argument (a string) which will be
109 passed each line (with the ending newline character removed) of
110 program's output for filtering.
111
112 Raises:
113 gclient_utils.Error: An error occurred while running the command.
114 """
115 command = [GIT.COMMAND]
116 command.extend(args)
117 gclient_utils.SubprocessCallAndFilter(command,
118 in_directory,
119 print_messages,
120 print_stdout,
121 filter=filter)
122
123 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000124 def GetEmail(repo_root):
125 """Retrieves the user email address if known."""
126 # We could want to look at the svn cred when it has a svn remote but it
127 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000128 return GIT.Capture(['config', 'user.email'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000129 repo_root, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000130
131 @staticmethod
132 def ShortBranchName(branch):
133 """Converts a name like 'refs/heads/foo' to just 'foo'."""
134 return branch.replace('refs/heads/', '')
135
136 @staticmethod
137 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000138 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000139 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000140
141 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000142 def GetBranch(cwd):
143 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000144 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000145
146 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000147 def IsGitSvn(cwd):
148 """Returns true if this repo looks like it's using git-svn."""
149 # If you have any "svn-remote.*" config keys, we think you're using svn.
150 try:
151 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd)
152 return True
153 except gclient_utils.CheckCallError:
154 return False
155
156 @staticmethod
157 def GetSVNBranch(cwd):
158 """Returns the svn branch name if found."""
159 # Try to figure out which remote branch we're based on.
160 # Strategy:
161 # 1) find all git-svn branches and note their svn URLs.
162 # 2) iterate through our branch history and match up the URLs.
163
164 # regexp matching the git-svn line that contains the URL.
165 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
166
167 # Get the refname and svn url for all refs/remotes/*.
168 remotes = GIT.Capture(
169 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000170 cwd)[0].splitlines()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000171 svn_refs = {}
172 for ref in remotes:
173 match = git_svn_re.search(
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000174 GIT.Capture(['cat-file', '-p', ref], cwd)[0])
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000175 if match:
176 svn_refs[match.group(1)] = ref
177
178 svn_branch = ''
179 if len(svn_refs) == 1:
180 # Only one svn branch exists -- seems like a good candidate.
181 svn_branch = svn_refs.values()[0]
182 elif len(svn_refs) > 1:
183 # We have more than one remote branch available. We don't
184 # want to go through all of history, so read a line from the
185 # pipe at a time.
186 # The -100 is an arbitrary limit so we don't search forever.
187 cmd = ['git', 'log', '-100', '--pretty=medium']
188 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
189 for line in proc.stdout:
190 match = git_svn_re.match(line)
191 if match:
192 url = match.group(1)
193 if url in svn_refs:
194 svn_branch = svn_refs[url]
195 proc.stdout.close() # Cut pipe.
196 break
197 return svn_branch
198
199 @staticmethod
200 def FetchUpstreamTuple(cwd):
201 """Returns a tuple containg remote and remote ref,
202 e.g. 'origin', 'refs/heads/master'
203 """
204 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000205 branch = GIT.GetBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000206 upstream_branch = None
207 upstream_branch = GIT.Capture(
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000208 ['config', 'branch.%s.merge' % branch], in_directory=cwd,
209 error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000210 if upstream_branch:
211 remote = GIT.Capture(
212 ['config', 'branch.%s.remote' % branch],
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000213 in_directory=cwd, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000214 else:
215 # Fall back on trying a git-svn upstream branch.
216 if GIT.IsGitSvn(cwd):
217 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000218 return remote, upstream_branch
219
220 @staticmethod
221 def GetUpstream(cwd):
222 """Gets the current branch's upstream branch."""
223 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
224 if remote is not '.':
225 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
226 return upstream_branch
227
228 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000229 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
230 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000231 """Diffs against the upstream branch or optionally another branch.
232
233 full_move means that move or copy operations should completely recreate the
234 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000235 if not branch:
236 branch = GIT.GetUpstream(cwd)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000237 command = ['diff-tree', '-p', '--no-prefix', branch, branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000238 if not full_move:
239 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000240 # TODO(maruel): --binary support.
241 if files:
242 command.append('--')
243 command.extend(files)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000244 diff = GIT.Capture(command, cwd)[0].splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000245 for i in range(len(diff)):
246 # In the case of added files, replace /dev/null with the path to the
247 # file being added.
248 if diff[i].startswith('--- /dev/null'):
249 diff[i] = '--- %s' % diff[i+1][4:]
250 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000251
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000252 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000253 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
254 """Returns the list of modified files between two branches."""
255 if not branch:
256 branch = GIT.GetUpstream(cwd)
257 command = ['diff', '--name-only', branch, branch_head]
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000258 return GIT.Capture(command, cwd)[0].splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000259
260 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000261 def GetPatchName(cwd):
262 """Constructs a name for this patch."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000263 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000264 return "%s-%s" % (GIT.GetBranch(cwd), short_sha)
265
266 @staticmethod
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000267 def GetCheckoutRoot(path):
268 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000269 """
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000270 root = GIT.Capture(['rev-parse', '--show-cdup'], path)[0].strip()
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000271 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000272
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000273
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000274class SVN(object):
275 COMMAND = "svn"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000276
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000277 @staticmethod
278 def Run(args, in_directory):
279 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000280
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000281 Args:
282 args: A sequence of command line parameters to be passed to svn.
283 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000284
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000285 Raises:
286 Error: An error occurred while running the svn command.
287 """
288 c = [SVN.COMMAND]
289 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000290 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000291 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000292
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000293 @staticmethod
294 def Capture(args, in_directory=None, print_error=True):
295 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000296
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000297 Args:
298 args: A sequence of command line parameters to be passed to svn.
299 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000300
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000301 Returns:
302 The output sent to stdout as a string.
303 """
304 c = [SVN.COMMAND]
305 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000306
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000307 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
308 # the svn.exe executable, but shell=True makes subprocess on Linux fail
309 # when it's called with a list because it only tries to execute the
310 # first string ("svn").
311 stderr = None
312 if not print_error:
313 stderr = subprocess.PIPE
314 return subprocess.Popen(c,
315 cwd=in_directory,
316 shell=(sys.platform == 'win32'),
317 stdout=subprocess.PIPE,
318 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000319
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000320 @staticmethod
321 def RunAndGetFileList(options, args, in_directory, file_list):
322 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000323
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000324 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000325
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000326 svn's stdout is parsed to collect a list of files checked out or updated.
327 These files are appended to file_list. svn's stdout is also printed to
328 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000329
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000330 Args:
331 options: command line options to gclient
332 args: A sequence of command line parameters to be passed to svn.
333 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000334
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000335 Raises:
336 Error: An error occurred while running the svn command.
337 """
338 command = [SVN.COMMAND]
339 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000340
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000341 # svn update and svn checkout use the same pattern: the first three columns
342 # are for file status, property status, and lock status. This is followed
343 # by two spaces, and then the path to the file.
344 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000345
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000346 # The first three columns of svn status are the same as for svn update and
347 # svn checkout. The next three columns indicate addition-with-history,
348 # switch, and remote lock status. This is followed by one space, and then
349 # the path to the file.
350 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000351
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000352 # args[0] must be a supported command. This will blow up if it's something
353 # else, which is good. Note that the patterns are only effective when
354 # these commands are used in their ordinary forms, the patterns are invalid
355 # for "svn status --show-updates", for example.
356 pattern = {
357 'checkout': update_pattern,
358 'status': status_pattern,
359 'update': update_pattern,
360 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000361 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000362 # Place an upper limit.
363 for i in range(1, 10):
364 previous_list_len = len(file_list)
365 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000366
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000367 def CaptureMatchingLines(line):
368 match = compiled_pattern.search(line)
369 if match:
370 file_list.append(match.group(1))
371 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000372 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000373
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000374 try:
375 SVN.RunAndFilterOutput(args,
376 in_directory,
377 options.verbose,
378 True,
379 CaptureMatchingLines)
380 except gclient_utils.Error:
maruel@chromium.org2de10252010-02-08 01:10:39 +0000381 # We enforce that some progress has been made or HTTP 502.
382 if ([True for f in failure if '502 Bad Gateway' in f] or
383 (len(failure) and len(file_list) > previous_list_len)):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000384 if args[0] == 'checkout':
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000385 # An aborted checkout is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000386 args = ['update'] + args[1:]
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000387 continue
maruel@chromium.org2de10252010-02-08 01:10:39 +0000388 # No progress was made or an unknown error we aren't sure, bail out.
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000389 raise
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000390 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000391
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000392 @staticmethod
393 def RunAndFilterOutput(args,
394 in_directory,
395 print_messages,
396 print_stdout,
397 filter):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000398 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000399
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000400 stdout is passed line-by-line to the given filter function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000401 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000402
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000403 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000404 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000405 in_directory: The directory where svn is to be run.
406 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000407 which commands are being run.
408 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000409 filter: A function taking one argument (a string) which will be
410 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000411 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000412
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000413 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000414 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000415 """
416 command = [SVN.COMMAND]
417 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000418 gclient_utils.SubprocessCallAndFilter(command,
419 in_directory,
420 print_messages,
421 print_stdout,
422 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000423
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000424 @staticmethod
425 def CaptureInfo(relpath, in_directory=None, print_error=True):
426 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000427
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000428 Args:
429 relpath: The directory where the working copy resides relative to
430 the directory given by in_directory.
431 in_directory: The directory where svn is to be run.
432 """
433 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
434 dom = gclient_utils.ParseXML(output)
435 result = {}
436 if dom:
437 GetNamedNodeText = gclient_utils.GetNamedNodeText
438 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
439 def C(item, f):
440 if item is not None: return f(item)
441 # /info/entry/
442 # url
443 # reposityory/(root|uuid)
444 # wc-info/(schedule|depth)
445 # commit/(author|date)
446 # str() the results because they may be returned as Unicode, which
447 # interferes with the higher layers matching up things in the deps
448 # dictionary.
449 # TODO(maruel): Fix at higher level instead (!)
450 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
451 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
452 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
453 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
454 'revision'),
455 int)
456 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
457 str)
458 # Differs across versions.
459 if result['Node Kind'] == 'dir':
460 result['Node Kind'] = 'directory'
461 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
462 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
463 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
464 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
465 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000466
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000467 @staticmethod
468 def CaptureHeadRevision(url):
469 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000470
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000471 Returns:
472 Int head revision
473 """
474 info = SVN.Capture(["info", "--xml", url], os.getcwd())
475 dom = xml.dom.minidom.parseString(info)
476 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000477
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000478 @staticmethod
479 def CaptureStatus(files):
480 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000481
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000482 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000483
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000484 Returns an array of (status, file) tuples."""
485 command = ["status", "--xml"]
486 if not files:
487 pass
488 elif isinstance(files, basestring):
489 command.append(files)
490 else:
491 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000492
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000493 status_letter = {
494 None: ' ',
495 '': ' ',
496 'added': 'A',
497 'conflicted': 'C',
498 'deleted': 'D',
499 'external': 'X',
500 'ignored': 'I',
501 'incomplete': '!',
502 'merged': 'G',
503 'missing': '!',
504 'modified': 'M',
505 'none': ' ',
506 'normal': ' ',
507 'obstructed': '~',
508 'replaced': 'R',
509 'unversioned': '?',
510 }
511 dom = gclient_utils.ParseXML(SVN.Capture(command))
512 results = []
513 if dom:
514 # /status/target/entry/(wc-status|commit|author|date)
515 for target in dom.getElementsByTagName('target'):
516 #base_path = target.getAttribute('path')
517 for entry in target.getElementsByTagName('entry'):
518 file_path = entry.getAttribute('path')
519 wc_status = entry.getElementsByTagName('wc-status')
520 assert len(wc_status) == 1
521 # Emulate svn 1.5 status ouput...
522 statuses = [' '] * 7
523 # Col 0
524 xml_item_status = wc_status[0].getAttribute('item')
525 if xml_item_status in status_letter:
526 statuses[0] = status_letter[xml_item_status]
527 else:
528 raise Exception('Unknown item status "%s"; please implement me!' %
529 xml_item_status)
530 # Col 1
531 xml_props_status = wc_status[0].getAttribute('props')
532 if xml_props_status == 'modified':
533 statuses[1] = 'M'
534 elif xml_props_status == 'conflicted':
535 statuses[1] = 'C'
536 elif (not xml_props_status or xml_props_status == 'none' or
537 xml_props_status == 'normal'):
538 pass
539 else:
540 raise Exception('Unknown props status "%s"; please implement me!' %
541 xml_props_status)
542 # Col 2
543 if wc_status[0].getAttribute('wc-locked') == 'true':
544 statuses[2] = 'L'
545 # Col 3
546 if wc_status[0].getAttribute('copied') == 'true':
547 statuses[3] = '+'
548 # Col 4
549 if wc_status[0].getAttribute('switched') == 'true':
550 statuses[4] = 'S'
551 # TODO(maruel): Col 5 and 6
552 item = (''.join(statuses), file_path)
553 results.append(item)
554 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000555
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000556 @staticmethod
557 def IsMoved(filename):
558 """Determine if a file has been added through svn mv"""
559 info = SVN.CaptureInfo(filename)
560 return (info.get('Copied From URL') and
561 info.get('Copied From Rev') and
562 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000563
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000564 @staticmethod
565 def GetFileProperty(file, property_name):
566 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000567
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000568 Args:
569 file: The file to check
570 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000571
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000572 Returns:
573 The value of the property, which will be the empty string if the property
574 is not set on the file. If the file is not under version control, the
575 empty string is also returned.
576 """
577 output = SVN.Capture(["propget", property_name, file])
578 if (output.startswith("svn: ") and
579 output.endswith("is not under version control")):
580 return ""
581 else:
582 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000583
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000584 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000585 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000586 """Diffs a single file.
587
588 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000589 expected relative path.
590 full_move means that move or copy operations should completely recreate the
591 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000592 # Use svn info output instead of os.path.isdir because the latter fails
593 # when the file is deleted.
594 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
595 return None
596 # If the user specified a custom diff command in their svn config file,
597 # then it'll be used when we do svn diff, which we don't want to happen
598 # since we want the unified diff. Using --diff-cmd=diff doesn't always
599 # work, since they can have another diff executable in their path that
600 # gives different line endings. So we use a bogus temp directory as the
601 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000602 bogus_dir = tempfile.mkdtemp()
603 try:
604 # Grabs the diff data.
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000605 command = ["diff", "--config-dir", bogus_dir, filename]
maruel@chromium.orgce3c8622010-01-07 02:18:16 +0000606 if revision:
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000607 command.extend(['--revision', revision])
maruel@chromium.org0836c562010-01-22 01:10:06 +0000608 if SVN.IsMoved(filename):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000609 if full_move:
610 file_content = gclient_utils.FileRead(filename, 'rb')
611 # Prepend '+' to every lines.
612 file_content = ['+' + i for i in file_content.splitlines(True)]
613 nb_lines = len(file_content)
614 # We need to use / since patch on unix will fail otherwise.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000615 data = "Index: %s\n" % filename
616 data += '=' * 67 + '\n'
617 # Note: Should we use /dev/null instead?
618 data += "--- %s\n" % filename
619 data += "+++ %s\n" % filename
620 data += "@@ -0,0 +1,%d @@\n" % nb_lines
621 data += ''.join(file_content)
622 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000623 # svn diff on a mv/cp'd file outputs nothing if there was no change.
624 data = SVN.Capture(command, None)
625 if not data:
626 # We put in an empty Index entry so upload.py knows about them.
627 data = "Index: %s\n" % filename
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000628 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000629 data = SVN.Capture(command, None)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000630 finally:
631 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000632 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000633
634 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000635 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000636 """Returns a string containing the diff for the given file list.
637
638 The files in the list should either be absolute paths or relative to the
639 given root. If no root directory is provided, the repository root will be
640 used.
641 The diff will always use relative paths.
642 """
643 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000644 root = root or SVN.GetCheckoutRoot(previous_cwd)
645 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000646 def RelativePath(path, root):
647 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000648 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000649 return path[len(root):]
650 return path
651 try:
652 os.chdir(root)
653 diff = "".join(filter(None,
maruel@chromium.org0c401692010-01-07 02:44:20 +0000654 [SVN.DiffItem(RelativePath(f, root),
655 full_move=full_move,
656 revision=revision)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000657 for f in filenames]))
658 finally:
659 os.chdir(previous_cwd)
660 return diff
661
662
663 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000664 def GetEmail(repo_root):
665 """Retrieves the svn account which we assume is an email address."""
666 infos = SVN.CaptureInfo(repo_root)
667 uuid = infos.get('UUID')
668 root = infos.get('Repository Root')
669 if not root:
670 return None
671
672 # Should check for uuid but it is incorrectly saved for https creds.
673 realm = root.rsplit('/', 1)[0]
674 if root.startswith('https') or not uuid:
675 regexp = re.compile(r'<%s:\d+>.*' % realm)
676 else:
677 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
678 if regexp is None:
679 return None
680 if sys.platform.startswith('win'):
681 if not 'APPDATA' in os.environ:
682 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000683 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
684 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000685 else:
686 if not 'HOME' in os.environ:
687 return None
688 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
689 'svn.simple')
690 for credfile in os.listdir(auth_dir):
691 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
692 if regexp.match(cred_info.get('svn:realmstring')):
693 return cred_info.get('username')
694
695 @staticmethod
696 def ReadSimpleAuth(filename):
697 f = open(filename, 'r')
698 values = {}
699 def ReadOneItem(type):
700 m = re.match(r'%s (\d+)' % type, f.readline())
701 if not m:
702 return None
703 data = f.read(int(m.group(1)))
704 if f.read(1) != '\n':
705 return None
706 return data
707
708 while True:
709 key = ReadOneItem('K')
710 if not key:
711 break
712 value = ReadOneItem('V')
713 if not value:
714 break
715 values[key] = value
716 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000717
718 @staticmethod
719 def GetCheckoutRoot(directory):
720 """Returns the top level directory of the current repository.
721
722 The directory is returned as an absolute path.
723 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000724 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000725 infos = SVN.CaptureInfo(directory, print_error=False)
726 cur_dir_repo_root = infos.get("Repository Root")
727 if not cur_dir_repo_root:
728 return None
729
730 while True:
731 parent = os.path.dirname(directory)
732 if (SVN.CaptureInfo(parent, print_error=False).get(
733 "Repository Root") != cur_dir_repo_root):
734 break
735 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000736 return GetCasedPath(directory)