[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 1 | # 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 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 5 | """SCM-specific functions.""" |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 6 | |
| 7 | import os |
| 8 | import re |
| 9 | import subprocess |
| 10 | import sys |
| 11 | import xml.dom.minidom |
| 12 | |
| 13 | import gclient_utils |
| 14 | |
| 15 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 16 | SVN_COMMAND = "svn" |
| 17 | GIT_COMMAND = "git" |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 18 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 19 | # ----------------------------------------------------------------------------- |
| 20 | # Git utils: |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 21 | |
| 22 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 23 | def CaptureGit(args, in_directory=None, print_error=True): |
| 24 | """Runs git, capturing output sent to stdout as a string. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 25 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 26 | Args: |
| 27 | args: A sequence of command line parameters to be passed to git. |
| 28 | in_directory: The directory where git is to be run. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 29 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 30 | Returns: |
| 31 | The output sent to stdout as a string. |
| 32 | """ |
| 33 | c = [GIT_COMMAND] |
| 34 | c.extend(args) |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 35 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 36 | # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for |
| 37 | # the git.exe executable, but shell=True makes subprocess on Linux fail |
| 38 | # when it's called with a list because it only tries to execute the |
| 39 | # first string ("git"). |
| 40 | stderr = None |
| 41 | if not print_error: |
| 42 | stderr = subprocess.PIPE |
| 43 | return subprocess.Popen(c, |
| 44 | cwd=in_directory, |
| 45 | shell=sys.platform.startswith('win'), |
| 46 | stdout=subprocess.PIPE, |
| 47 | stderr=stderr).communicate()[0] |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 48 | |
| 49 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 50 | def CaptureGitStatus(files, upstream_branch='origin'): |
| 51 | """Returns git status. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 52 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 53 | @files can be a string (one file) or a list of files. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 54 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 55 | Returns an array of (status, file) tuples.""" |
| 56 | command = ["diff", "--name-status", "-r", "%s.." % upstream_branch] |
| 57 | if not files: |
| 58 | pass |
| 59 | elif isinstance(files, basestring): |
| 60 | command.append(files) |
| 61 | else: |
| 62 | command.extend(files) |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 63 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 64 | status = CaptureGit(command).rstrip() |
| 65 | results = [] |
| 66 | if status: |
| 67 | for statusline in status.split('\n'): |
| 68 | m = re.match('^(\w)\t(.+)$', statusline) |
| 69 | if not m: |
| 70 | raise Exception("status currently unsupported: %s" % statusline) |
| 71 | results.append(('%s ' % m.group(1), m.group(2))) |
| 72 | return results |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 73 | |
| 74 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 75 | # ----------------------------------------------------------------------------- |
| 76 | # SVN utils: |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 77 | |
| 78 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 79 | def RunSVN(args, in_directory): |
| 80 | """Runs svn, sending output to stdout. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 81 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 82 | Args: |
| 83 | args: A sequence of command line parameters to be passed to svn. |
| 84 | in_directory: The directory where svn is to be run. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 85 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 86 | Raises: |
| 87 | Error: An error occurred while running the svn command. |
| 88 | """ |
| 89 | c = [SVN_COMMAND] |
| 90 | c.extend(args) |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 91 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 92 | gclient_utils.SubprocessCall(c, in_directory) |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 93 | |
| 94 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 95 | def CaptureSVN(args, in_directory=None, print_error=True): |
| 96 | """Runs svn, capturing output sent to stdout as a string. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 97 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 98 | Args: |
| 99 | args: A sequence of command line parameters to be passed to svn. |
| 100 | in_directory: The directory where svn is to be run. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 101 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 102 | Returns: |
| 103 | The output sent to stdout as a string. |
| 104 | """ |
| 105 | c = [SVN_COMMAND] |
| 106 | c.extend(args) |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 107 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 108 | # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for |
| 109 | # the svn.exe executable, but shell=True makes subprocess on Linux fail |
| 110 | # when it's called with a list because it only tries to execute the |
| 111 | # first string ("svn"). |
| 112 | stderr = None |
| 113 | if not print_error: |
| 114 | stderr = subprocess.PIPE |
| 115 | return subprocess.Popen(c, |
| 116 | cwd=in_directory, |
| 117 | shell=(sys.platform == 'win32'), |
| 118 | stdout=subprocess.PIPE, |
| 119 | stderr=stderr).communicate()[0] |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 120 | |
| 121 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 122 | def RunSVNAndGetFileList(options, args, in_directory, file_list): |
| 123 | """Runs svn checkout, update, or status, output to stdout. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 124 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 125 | The first item in args must be either "checkout", "update", or "status". |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 126 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 127 | svn's stdout is parsed to collect a list of files checked out or updated. |
| 128 | These files are appended to file_list. svn's stdout is also printed to |
| 129 | sys.stdout as in RunSVN. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 130 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 131 | Args: |
| 132 | options: command line options to gclient |
| 133 | args: A sequence of command line parameters to be passed to svn. |
| 134 | in_directory: The directory where svn is to be run. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 135 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 136 | Raises: |
| 137 | Error: An error occurred while running the svn command. |
| 138 | """ |
| 139 | command = [SVN_COMMAND] |
| 140 | command.extend(args) |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 141 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 142 | # svn update and svn checkout use the same pattern: the first three columns |
| 143 | # are for file status, property status, and lock status. This is followed |
| 144 | # by two spaces, and then the path to the file. |
| 145 | update_pattern = '^... (.*)$' |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 146 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 147 | # The first three columns of svn status are the same as for svn update and |
| 148 | # svn checkout. The next three columns indicate addition-with-history, |
| 149 | # switch, and remote lock status. This is followed by one space, and then |
| 150 | # the path to the file. |
| 151 | status_pattern = '^...... (.*)$' |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 152 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 153 | # args[0] must be a supported command. This will blow up if it's something |
| 154 | # else, which is good. Note that the patterns are only effective when |
| 155 | # these commands are used in their ordinary forms, the patterns are invalid |
| 156 | # for "svn status --show-updates", for example. |
| 157 | pattern = { |
| 158 | 'checkout': update_pattern, |
| 159 | 'status': status_pattern, |
| 160 | 'update': update_pattern, |
| 161 | }[args[0]] |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 162 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 163 | compiled_pattern = re.compile(pattern) |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 164 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 165 | def CaptureMatchingLines(line): |
| 166 | match = compiled_pattern.search(line) |
| 167 | if match: |
| 168 | file_list.append(match.group(1)) |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 169 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 170 | RunSVNAndFilterOutput(args, |
| 171 | in_directory, |
| 172 | options.verbose, |
| 173 | True, |
| 174 | CaptureMatchingLines) |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 175 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 176 | def RunSVNAndFilterOutput(args, |
| 177 | in_directory, |
| 178 | print_messages, |
| 179 | print_stdout, |
| 180 | filter): |
| 181 | """Runs svn checkout, update, status, or diff, optionally outputting |
| 182 | to stdout. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 183 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 184 | The first item in args must be either "checkout", "update", |
| 185 | "status", or "diff". |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 186 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 187 | svn's stdout is passed line-by-line to the given filter function. If |
| 188 | print_stdout is true, it is also printed to sys.stdout as in RunSVN. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 189 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 190 | Args: |
| 191 | args: A sequence of command line parameters to be passed to svn. |
| 192 | in_directory: The directory where svn is to be run. |
| 193 | print_messages: Whether to print status messages to stdout about |
| 194 | which Subversion commands are being run. |
| 195 | print_stdout: Whether to forward Subversion's output to stdout. |
| 196 | filter: A function taking one argument (a string) which will be |
| 197 | passed each line (with the ending newline character removed) of |
| 198 | Subversion's output for filtering. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 199 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 200 | Raises: |
| 201 | Error: An error occurred while running the svn command. |
| 202 | """ |
| 203 | command = [SVN_COMMAND] |
| 204 | command.extend(args) |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 205 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 206 | gclient_utils.SubprocessCallAndFilter(command, |
| 207 | in_directory, |
| 208 | print_messages, |
| 209 | print_stdout, |
| 210 | filter=filter) |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 211 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 212 | def CaptureSVNInfo(relpath, in_directory=None, print_error=True): |
| 213 | """Returns a dictionary from the svn info output for the given file. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 214 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 215 | Args: |
| 216 | relpath: The directory where the working copy resides relative to |
| 217 | the directory given by in_directory. |
| 218 | in_directory: The directory where svn is to be run. |
| 219 | """ |
| 220 | output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error) |
| 221 | dom = gclient_utils.ParseXML(output) |
| 222 | result = {} |
| 223 | if dom: |
| 224 | GetNamedNodeText = gclient_utils.GetNamedNodeText |
| 225 | GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText |
| 226 | def C(item, f): |
| 227 | if item is not None: return f(item) |
| 228 | # /info/entry/ |
| 229 | # url |
| 230 | # reposityory/(root|uuid) |
| 231 | # wc-info/(schedule|depth) |
| 232 | # commit/(author|date) |
| 233 | # str() the results because they may be returned as Unicode, which |
| 234 | # interferes with the higher layers matching up things in the deps |
| 235 | # dictionary. |
| 236 | # TODO(maruel): Fix at higher level instead (!) |
| 237 | result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str) |
| 238 | result['URL'] = C(GetNamedNodeText(dom, 'url'), str) |
| 239 | result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str) |
| 240 | result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'), |
| 241 | int) |
| 242 | result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'), |
| 243 | str) |
| 244 | result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str) |
| 245 | result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str) |
| 246 | result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str) |
| 247 | result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str) |
| 248 | return result |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 249 | |
| 250 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 251 | def CaptureSVNHeadRevision(url): |
| 252 | """Get the head revision of a SVN repository. |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 253 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 254 | Returns: |
| 255 | Int head revision |
| 256 | """ |
| 257 | info = CaptureSVN(["info", "--xml", url], os.getcwd()) |
| 258 | dom = xml.dom.minidom.parseString(info) |
| 259 | return dom.getElementsByTagName('entry')[0].getAttribute('revision') |
[email protected] | d5800f1 | 2009-11-12 20:03:43 | [diff] [blame] | 260 | |
[email protected] | 261eeb5 | 2009-11-16 18:25:45 | [diff] [blame^] | 261 | |
| 262 | def CaptureSVNStatus(files): |
| 263 | """Returns the svn 1.5 svn status emulated output. |
| 264 | |
| 265 | @files can be a string (one file) or a list of files. |
| 266 | |
| 267 | Returns an array of (status, file) tuples.""" |
| 268 | command = ["status", "--xml"] |
| 269 | if not files: |
| 270 | pass |
| 271 | elif isinstance(files, basestring): |
| 272 | command.append(files) |
| 273 | else: |
| 274 | command.extend(files) |
| 275 | |
| 276 | status_letter = { |
| 277 | None: ' ', |
| 278 | '': ' ', |
| 279 | 'added': 'A', |
| 280 | 'conflicted': 'C', |
| 281 | 'deleted': 'D', |
| 282 | 'external': 'X', |
| 283 | 'ignored': 'I', |
| 284 | 'incomplete': '!', |
| 285 | 'merged': 'G', |
| 286 | 'missing': '!', |
| 287 | 'modified': 'M', |
| 288 | 'none': ' ', |
| 289 | 'normal': ' ', |
| 290 | 'obstructed': '~', |
| 291 | 'replaced': 'R', |
| 292 | 'unversioned': '?', |
| 293 | } |
| 294 | dom = gclient_utils.ParseXML(CaptureSVN(command)) |
| 295 | results = [] |
| 296 | if dom: |
| 297 | # /status/target/entry/(wc-status|commit|author|date) |
| 298 | for target in dom.getElementsByTagName('target'): |
| 299 | for entry in target.getElementsByTagName('entry'): |
| 300 | file_path = entry.getAttribute('path') |
| 301 | wc_status = entry.getElementsByTagName('wc-status') |
| 302 | assert len(wc_status) == 1 |
| 303 | # Emulate svn 1.5 status ouput... |
| 304 | statuses = [' '] * 7 |
| 305 | # Col 0 |
| 306 | xml_item_status = wc_status[0].getAttribute('item') |
| 307 | if xml_item_status in status_letter: |
| 308 | statuses[0] = status_letter[xml_item_status] |
| 309 | else: |
| 310 | raise Exception('Unknown item status "%s"; please implement me!' % |
| 311 | xml_item_status) |
| 312 | # Col 1 |
| 313 | xml_props_status = wc_status[0].getAttribute('props') |
| 314 | if xml_props_status == 'modified': |
| 315 | statuses[1] = 'M' |
| 316 | elif xml_props_status == 'conflicted': |
| 317 | statuses[1] = 'C' |
| 318 | elif (not xml_props_status or xml_props_status == 'none' or |
| 319 | xml_props_status == 'normal'): |
| 320 | pass |
| 321 | else: |
| 322 | raise Exception('Unknown props status "%s"; please implement me!' % |
| 323 | xml_props_status) |
| 324 | # Col 2 |
| 325 | if wc_status[0].getAttribute('wc-locked') == 'true': |
| 326 | statuses[2] = 'L' |
| 327 | # Col 3 |
| 328 | if wc_status[0].getAttribute('copied') == 'true': |
| 329 | statuses[3] = '+' |
| 330 | # Col 4 |
| 331 | if wc_status[0].getAttribute('switched') == 'true': |
| 332 | statuses[4] = 'S' |
| 333 | # TODO(maruel): Col 5 and 6 |
| 334 | item = (''.join(statuses), file_path) |
| 335 | results.append(item) |
| 336 | return results |