| OLD | NEW |
| 1 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """SCM-specific utility classes.""" | 5 """SCM-specific functions.""" |
| 6 | 6 |
| 7 import os | 7 import os |
| 8 import re | 8 import re |
| 9 import subprocess | 9 import subprocess |
| 10 import sys | 10 import sys |
| 11 import tempfile | |
| 12 import xml.dom.minidom | 11 import xml.dom.minidom |
| 13 | 12 |
| 14 import gclient_utils | 13 import gclient_utils |
| 15 | 14 |
| 16 | 15 |
| 17 class GIT(object): | 16 SVN_COMMAND = "svn" |
| 18 COMMAND = "git" | 17 GIT_COMMAND = "git" |
| 19 | 18 |
| 20 @staticmethod | 19 # ----------------------------------------------------------------------------- |
| 21 def Capture(args, in_directory=None, print_error=True): | 20 # Git utils: |
| 22 """Runs git, capturing output sent to stdout as a string. | 21 |
| 23 | 22 |
| 24 Args: | 23 def CaptureGit(args, in_directory=None, print_error=True): |
| 25 args: A sequence of command line parameters to be passed to git. | 24 """Runs git, capturing output sent to stdout as a string. |
| 26 in_directory: The directory where git is to be run. | 25 |
| 27 | 26 Args: |
| 28 Returns: | 27 args: A sequence of command line parameters to be passed to git. |
| 29 The output sent to stdout as a string. | 28 in_directory: The directory where git is to be run. |
| 30 """ | 29 |
| 31 c = [GIT.COMMAND] | 30 Returns: |
| 32 c.extend(args) | 31 The output sent to stdout as a string. |
| 33 | 32 """ |
| 34 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for | 33 c = [GIT_COMMAND] |
| 35 # the git.exe executable, but shell=True makes subprocess on Linux fail | 34 c.extend(args) |
| 36 # when it's called with a list because it only tries to execute the | 35 |
| 37 # first string ("git"). | 36 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for |
| 38 stderr = None | 37 # the git.exe executable, but shell=True makes subprocess on Linux fail |
| 39 if not print_error: | 38 # when it's called with a list because it only tries to execute the |
| 40 stderr = subprocess.PIPE | 39 # first string ("git"). |
| 41 return subprocess.Popen(c, | 40 stderr = None |
| 42 cwd=in_directory, | 41 if not print_error: |
| 43 shell=sys.platform.startswith('win'), | 42 stderr = subprocess.PIPE |
| 44 stdout=subprocess.PIPE, | 43 return subprocess.Popen(c, |
| 45 stderr=stderr).communicate()[0] | 44 cwd=in_directory, |
| 46 | 45 shell=sys.platform.startswith('win'), |
| 47 | 46 stdout=subprocess.PIPE, |
| 48 @staticmethod | 47 stderr=stderr).communicate()[0] |
| 49 def CaptureStatus(files, upstream_branch='origin'): | 48 |
| 50 """Returns git status. | 49 |
| 51 | 50 def CaptureGitStatus(files, upstream_branch='origin'): |
| 52 @files can be a string (one file) or a list of files. | 51 """Returns git status. |
| 53 | 52 |
| 54 Returns an array of (status, file) tuples.""" | 53 @files can be a string (one file) or a list of files. |
| 55 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch] | 54 |
| 56 if not files: | 55 Returns an array of (status, file) tuples.""" |
| 57 pass | 56 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch] |
| 58 elif isinstance(files, basestring): | 57 if not files: |
| 59 command.append(files) | 58 pass |
| 60 else: | 59 elif isinstance(files, basestring): |
| 61 command.extend(files) | 60 command.append(files) |
| 62 | 61 else: |
| 63 status = GIT.Capture(command).rstrip() | 62 command.extend(files) |
| 64 results = [] | 63 |
| 65 if status: | 64 status = CaptureGit(command).rstrip() |
| 66 for statusline in status.split('\n'): | 65 results = [] |
| 67 m = re.match('^(\w)\t(.+)$', statusline) | 66 if status: |
| 68 if not m: | 67 for statusline in status.split('\n'): |
| 69 raise Exception("status currently unsupported: %s" % statusline) | 68 m = re.match('^(\w)\t(.+)$', statusline) |
| 70 results.append(('%s ' % m.group(1), m.group(2))) | 69 if not m: |
| 71 return results | 70 raise Exception("status currently unsupported: %s" % statusline) |
| 72 | 71 results.append(('%s ' % m.group(1), m.group(2))) |
| 73 | 72 return results |
| 74 class SVN(object): | 73 |
| 75 COMMAND = "svn" | 74 |
| 76 | 75 # ----------------------------------------------------------------------------- |
| 77 @staticmethod | 76 # SVN utils: |
| 78 def Run(args, in_directory): | 77 |
| 79 """Runs svn, sending output to stdout. | 78 |
| 80 | 79 def RunSVN(args, in_directory): |
| 81 Args: | 80 """Runs svn, sending output to stdout. |
| 82 args: A sequence of command line parameters to be passed to svn. | 81 |
| 83 in_directory: The directory where svn is to be run. | 82 Args: |
| 84 | 83 args: A sequence of command line parameters to be passed to svn. |
| 85 Raises: | 84 in_directory: The directory where svn is to be run. |
| 86 Error: An error occurred while running the svn command. | 85 |
| 87 """ | 86 Raises: |
| 88 c = [SVN.COMMAND] | 87 Error: An error occurred while running the svn command. |
| 89 c.extend(args) | 88 """ |
| 90 | 89 c = [SVN_COMMAND] |
| 91 gclient_utils.SubprocessCall(c, in_directory) | 90 c.extend(args) |
| 92 | 91 |
| 93 @staticmethod | 92 gclient_utils.SubprocessCall(c, in_directory) |
| 94 def Capture(args, in_directory=None, print_error=True): | 93 |
| 95 """Runs svn, capturing output sent to stdout as a string. | 94 |
| 96 | 95 def CaptureSVN(args, in_directory=None, print_error=True): |
| 97 Args: | 96 """Runs svn, capturing output sent to stdout as a string. |
| 98 args: A sequence of command line parameters to be passed to svn. | 97 |
| 99 in_directory: The directory where svn is to be run. | 98 Args: |
| 100 | 99 args: A sequence of command line parameters to be passed to svn. |
| 101 Returns: | 100 in_directory: The directory where svn is to be run. |
| 102 The output sent to stdout as a string. | 101 |
| 103 """ | 102 Returns: |
| 104 c = [SVN.COMMAND] | 103 The output sent to stdout as a string. |
| 105 c.extend(args) | 104 """ |
| 106 | 105 c = [SVN_COMMAND] |
| 107 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for | 106 c.extend(args) |
| 108 # the svn.exe executable, but shell=True makes subprocess on Linux fail | 107 |
| 109 # when it's called with a list because it only tries to execute the | 108 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for |
| 110 # first string ("svn"). | 109 # the svn.exe executable, but shell=True makes subprocess on Linux fail |
| 111 stderr = None | 110 # when it's called with a list because it only tries to execute the |
| 112 if not print_error: | 111 # first string ("svn"). |
| 113 stderr = subprocess.PIPE | 112 stderr = None |
| 114 return subprocess.Popen(c, | 113 if not print_error: |
| 115 cwd=in_directory, | 114 stderr = subprocess.PIPE |
| 116 shell=(sys.platform == 'win32'), | 115 return subprocess.Popen(c, |
| 117 stdout=subprocess.PIPE, | 116 cwd=in_directory, |
| 118 stderr=stderr).communicate()[0] | 117 shell=(sys.platform == 'win32'), |
| 119 | 118 stdout=subprocess.PIPE, |
| 120 @staticmethod | 119 stderr=stderr).communicate()[0] |
| 121 def RunAndGetFileList(options, args, in_directory, file_list): | 120 |
| 122 """Runs svn checkout, update, or status, output to stdout. | 121 |
| 123 | 122 def RunSVNAndGetFileList(options, args, in_directory, file_list): |
| 124 The first item in args must be either "checkout", "update", or "status". | 123 """Runs svn checkout, update, or status, output to stdout. |
| 125 | 124 |
| 126 svn's stdout is parsed to collect a list of files checked out or updated. | 125 The first item in args must be either "checkout", "update", or "status". |
| 127 These files are appended to file_list. svn's stdout is also printed to | 126 |
| 128 sys.stdout as in Run. | 127 svn's stdout is parsed to collect a list of files checked out or updated. |
| 129 | 128 These files are appended to file_list. svn's stdout is also printed to |
| 130 Args: | 129 sys.stdout as in RunSVN. |
| 131 options: command line options to gclient | 130 |
| 132 args: A sequence of command line parameters to be passed to svn. | 131 Args: |
| 133 in_directory: The directory where svn is to be run. | 132 options: command line options to gclient |
| 134 | 133 args: A sequence of command line parameters to be passed to svn. |
| 135 Raises: | 134 in_directory: The directory where svn is to be run. |
| 136 Error: An error occurred while running the svn command. | 135 |
| 137 """ | 136 Raises: |
| 138 command = [SVN.COMMAND] | 137 Error: An error occurred while running the svn command. |
| 139 command.extend(args) | 138 """ |
| 140 | 139 command = [SVN_COMMAND] |
| 141 # svn update and svn checkout use the same pattern: the first three columns | 140 command.extend(args) |
| 142 # are for file status, property status, and lock status. This is followed | 141 |
| 143 # by two spaces, and then the path to the file. | 142 # svn update and svn checkout use the same pattern: the first three columns |
| 144 update_pattern = '^... (.*)$' | 143 # are for file status, property status, and lock status. This is followed |
| 145 | 144 # by two spaces, and then the path to the file. |
| 146 # The first three columns of svn status are the same as for svn update and | 145 update_pattern = '^... (.*)$' |
| 147 # svn checkout. The next three columns indicate addition-with-history, | 146 |
| 148 # switch, and remote lock status. This is followed by one space, and then | 147 # The first three columns of svn status are the same as for svn update and |
| 149 # the path to the file. | 148 # svn checkout. The next three columns indicate addition-with-history, |
| 150 status_pattern = '^...... (.*)$' | 149 # switch, and remote lock status. This is followed by one space, and then |
| 151 | 150 # the path to the file. |
| 152 # args[0] must be a supported command. This will blow up if it's something | 151 status_pattern = '^...... (.*)$' |
| 153 # else, which is good. Note that the patterns are only effective when | 152 |
| 154 # these commands are used in their ordinary forms, the patterns are invalid | 153 # args[0] must be a supported command. This will blow up if it's something |
| 155 # for "svn status --show-updates", for example. | 154 # else, which is good. Note that the patterns are only effective when |
| 156 pattern = { | 155 # these commands are used in their ordinary forms, the patterns are invalid |
| 157 'checkout': update_pattern, | 156 # for "svn status --show-updates", for example. |
| 158 'status': status_pattern, | 157 pattern = { |
| 159 'update': update_pattern, | 158 'checkout': update_pattern, |
| 160 }[args[0]] | 159 'status': status_pattern, |
| 161 | 160 'update': update_pattern, |
| 162 compiled_pattern = re.compile(pattern) | 161 }[args[0]] |
| 163 | 162 |
| 164 def CaptureMatchingLines(line): | 163 compiled_pattern = re.compile(pattern) |
| 165 match = compiled_pattern.search(line) | 164 |
| 166 if match: | 165 def CaptureMatchingLines(line): |
| 167 file_list.append(match.group(1)) | 166 match = compiled_pattern.search(line) |
| 168 | 167 if match: |
| 169 SVN.RunAndFilterOutput(args, | 168 file_list.append(match.group(1)) |
| 170 in_directory, | 169 |
| 171 options.verbose, | 170 RunSVNAndFilterOutput(args, |
| 172 True, | 171 in_directory, |
| 173 CaptureMatchingLines) | 172 options.verbose, |
| 174 | 173 True, |
| 175 @staticmethod | 174 CaptureMatchingLines) |
| 176 def RunAndFilterOutput(args, | 175 |
| 177 in_directory, | 176 def RunSVNAndFilterOutput(args, |
| 178 print_messages, | 177 in_directory, |
| 179 print_stdout, | 178 print_messages, |
| 180 filter): | 179 print_stdout, |
| 181 """Runs svn checkout, update, status, or diff, optionally outputting | 180 filter): |
| 182 to stdout. | 181 """Runs svn checkout, update, status, or diff, optionally outputting |
| 183 | 182 to stdout. |
| 184 The first item in args must be either "checkout", "update", | 183 |
| 185 "status", or "diff". | 184 The first item in args must be either "checkout", "update", |
| 186 | 185 "status", or "diff". |
| 187 svn's stdout is passed line-by-line to the given filter function. If | 186 |
| 188 print_stdout is true, it is also printed to sys.stdout as in Run. | 187 svn's stdout is passed line-by-line to the given filter function. If |
| 189 | 188 print_stdout is true, it is also printed to sys.stdout as in RunSVN. |
| 190 Args: | 189 |
| 191 args: A sequence of command line parameters to be passed to svn. | 190 Args: |
| 192 in_directory: The directory where svn is to be run. | 191 args: A sequence of command line parameters to be passed to svn. |
| 193 print_messages: Whether to print status messages to stdout about | 192 in_directory: The directory where svn is to be run. |
| 194 which Subversion commands are being run. | 193 print_messages: Whether to print status messages to stdout about |
| 195 print_stdout: Whether to forward Subversion's output to stdout. | 194 which Subversion commands are being run. |
| 196 filter: A function taking one argument (a string) which will be | 195 print_stdout: Whether to forward Subversion's output to stdout. |
| 197 passed each line (with the ending newline character removed) of | 196 filter: A function taking one argument (a string) which will be |
| 198 Subversion's output for filtering. | 197 passed each line (with the ending newline character removed) of |
| 199 | 198 Subversion's output for filtering. |
| 200 Raises: | 199 |
| 201 Error: An error occurred while running the svn command. | 200 Raises: |
| 202 """ | 201 Error: An error occurred while running the svn command. |
| 203 command = [SVN.COMMAND] | 202 """ |
| 204 command.extend(args) | 203 command = [SVN_COMMAND] |
| 205 | 204 command.extend(args) |
| 206 gclient_utils.SubprocessCallAndFilter(command, | 205 |
| 207 in_directory, | 206 gclient_utils.SubprocessCallAndFilter(command, |
| 208 print_messages, | 207 in_directory, |
| 209 print_stdout, | 208 print_messages, |
| 210 filter=filter) | 209 print_stdout, |
| 211 | 210 filter=filter) |
| 212 @staticmethod | 211 |
| 213 def CaptureInfo(relpath, in_directory=None, print_error=True): | 212 def CaptureSVNInfo(relpath, in_directory=None, print_error=True): |
| 214 """Returns a dictionary from the svn info output for the given file. | 213 """Returns a dictionary from the svn info output for the given file. |
| 215 | 214 |
| 216 Args: | 215 Args: |
| 217 relpath: The directory where the working copy resides relative to | 216 relpath: The directory where the working copy resides relative to |
| 218 the directory given by in_directory. | 217 the directory given by in_directory. |
| 219 in_directory: The directory where svn is to be run. | 218 in_directory: The directory where svn is to be run. |
| 220 """ | 219 """ |
| 221 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error) | 220 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error) |
| 222 dom = gclient_utils.ParseXML(output) | 221 dom = gclient_utils.ParseXML(output) |
| 223 result = {} | 222 result = {} |
| 224 if dom: | 223 if dom: |
| 225 GetNamedNodeText = gclient_utils.GetNamedNodeText | 224 GetNamedNodeText = gclient_utils.GetNamedNodeText |
| 226 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText | 225 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText |
| 227 def C(item, f): | 226 def C(item, f): |
| 228 if item is not None: return f(item) | 227 if item is not None: return f(item) |
| 229 # /info/entry/ | 228 # /info/entry/ |
| 230 # url | 229 # url |
| 231 # reposityory/(root|uuid) | 230 # reposityory/(root|uuid) |
| 232 # wc-info/(schedule|depth) | 231 # wc-info/(schedule|depth) |
| 233 # commit/(author|date) | 232 # commit/(author|date) |
| 234 # str() the results because they may be returned as Unicode, which | 233 # str() the results because they may be returned as Unicode, which |
| 235 # interferes with the higher layers matching up things in the deps | 234 # interferes with the higher layers matching up things in the deps |
| 236 # dictionary. | 235 # dictionary. |
| 237 # TODO(maruel): Fix at higher level instead (!) | 236 # TODO(maruel): Fix at higher level instead (!) |
| 238 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str) | 237 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str) |
| 239 result['URL'] = C(GetNamedNodeText(dom, 'url'), str) | 238 result['URL'] = C(GetNamedNodeText(dom, 'url'), str) |
| 240 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str) | 239 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str) |
| 241 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', | 240 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'), |
| 242 'revision'), | 241 int) |
| 243 int) | 242 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'), |
| 244 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'), | 243 str) |
| 245 str) | 244 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str) |
| 246 # Differs across versions. | 245 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str) |
| 247 if result['Node Kind'] == 'dir': | 246 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str) |
| 248 result['Node Kind'] = 'directory' | 247 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str) |
| 249 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str) | 248 return result |
| 250 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str) | 249 |
| 251 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str) | 250 |
| 252 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str) | 251 def CaptureSVNHeadRevision(url): |
| 253 return result | 252 """Get the head revision of a SVN repository. |
| 254 | 253 |
| 255 @staticmethod | 254 Returns: |
| 256 def CaptureHeadRevision(url): | 255 Int head revision |
| 257 """Get the head revision of a SVN repository. | 256 """ |
| 258 | 257 info = CaptureSVN(["info", "--xml", url], os.getcwd()) |
| 259 Returns: | 258 dom = xml.dom.minidom.parseString(info) |
| 260 Int head revision | 259 return dom.getElementsByTagName('entry')[0].getAttribute('revision') |
| 261 """ | 260 |
| 262 info = SVN.Capture(["info", "--xml", url], os.getcwd()) | 261 |
| 263 dom = xml.dom.minidom.parseString(info) | 262 def CaptureSVNStatus(files): |
| 264 return dom.getElementsByTagName('entry')[0].getAttribute('revision') | 263 """Returns the svn 1.5 svn status emulated output. |
| 265 | 264 |
| 266 @staticmethod | 265 @files can be a string (one file) or a list of files. |
| 267 def CaptureStatus(files): | 266 |
| 268 """Returns the svn 1.5 svn status emulated output. | 267 Returns an array of (status, file) tuples.""" |
| 269 | 268 command = ["status", "--xml"] |
| 270 @files can be a string (one file) or a list of files. | 269 if not files: |
| 271 | 270 pass |
| 272 Returns an array of (status, file) tuples.""" | 271 elif isinstance(files, basestring): |
| 273 command = ["status", "--xml"] | 272 command.append(files) |
| 274 if not files: | 273 else: |
| 275 pass | 274 command.extend(files) |
| 276 elif isinstance(files, basestring): | 275 |
| 277 command.append(files) | 276 status_letter = { |
| 278 else: | 277 None: ' ', |
| 279 command.extend(files) | 278 '': ' ', |
| 280 | 279 'added': 'A', |
| 281 status_letter = { | 280 'conflicted': 'C', |
| 282 None: ' ', | 281 'deleted': 'D', |
| 283 '': ' ', | 282 'external': 'X', |
| 284 'added': 'A', | 283 'ignored': 'I', |
| 285 'conflicted': 'C', | 284 'incomplete': '!', |
| 286 'deleted': 'D', | 285 'merged': 'G', |
| 287 'external': 'X', | 286 'missing': '!', |
| 288 'ignored': 'I', | 287 'modified': 'M', |
| 289 'incomplete': '!', | 288 'none': ' ', |
| 290 'merged': 'G', | 289 'normal': ' ', |
| 291 'missing': '!', | 290 'obstructed': '~', |
| 292 'modified': 'M', | 291 'replaced': 'R', |
| 293 'none': ' ', | 292 'unversioned': '?', |
| 294 'normal': ' ', | 293 } |
| 295 'obstructed': '~', | 294 dom = gclient_utils.ParseXML(CaptureSVN(command)) |
| 296 'replaced': 'R', | 295 results = [] |
| 297 'unversioned': '?', | 296 if dom: |
| 298 } | 297 # /status/target/entry/(wc-status|commit|author|date) |
| 299 dom = gclient_utils.ParseXML(SVN.Capture(command)) | 298 for target in dom.getElementsByTagName('target'): |
| 300 results = [] | 299 for entry in target.getElementsByTagName('entry'): |
| 301 if dom: | 300 file_path = entry.getAttribute('path') |
| 302 # /status/target/entry/(wc-status|commit|author|date) | 301 wc_status = entry.getElementsByTagName('wc-status') |
| 303 for target in dom.getElementsByTagName('target'): | 302 assert len(wc_status) == 1 |
| 304 #base_path = target.getAttribute('path') | 303 # Emulate svn 1.5 status ouput... |
| 305 for entry in target.getElementsByTagName('entry'): | 304 statuses = [' '] * 7 |
| 306 file_path = entry.getAttribute('path') | 305 # Col 0 |
| 307 wc_status = entry.getElementsByTagName('wc-status') | 306 xml_item_status = wc_status[0].getAttribute('item') |
| 308 assert len(wc_status) == 1 | 307 if xml_item_status in status_letter: |
| 309 # Emulate svn 1.5 status ouput... | 308 statuses[0] = status_letter[xml_item_status] |
| 310 statuses = [' '] * 7 | 309 else: |
| 311 # Col 0 | 310 raise Exception('Unknown item status "%s"; please implement me!' % |
| 312 xml_item_status = wc_status[0].getAttribute('item') | 311 xml_item_status) |
| 313 if xml_item_status in status_letter: | 312 # Col 1 |
| 314 statuses[0] = status_letter[xml_item_status] | 313 xml_props_status = wc_status[0].getAttribute('props') |
| 315 else: | 314 if xml_props_status == 'modified': |
| 316 raise Exception('Unknown item status "%s"; please implement me!' % | 315 statuses[1] = 'M' |
| 317 xml_item_status) | 316 elif xml_props_status == 'conflicted': |
| 318 # Col 1 | 317 statuses[1] = 'C' |
| 319 xml_props_status = wc_status[0].getAttribute('props') | 318 elif (not xml_props_status or xml_props_status == 'none' or |
| 320 if xml_props_status == 'modified': | 319 xml_props_status == 'normal'): |
| 321 statuses[1] = 'M' | 320 pass |
| 322 elif xml_props_status == 'conflicted': | 321 else: |
| 323 statuses[1] = 'C' | 322 raise Exception('Unknown props status "%s"; please implement me!' % |
| 324 elif (not xml_props_status or xml_props_status == 'none' or | 323 xml_props_status) |
| 325 xml_props_status == 'normal'): | 324 # Col 2 |
| 326 pass | 325 if wc_status[0].getAttribute('wc-locked') == 'true': |
| 327 else: | 326 statuses[2] = 'L' |
| 328 raise Exception('Unknown props status "%s"; please implement me!' % | 327 # Col 3 |
| 329 xml_props_status) | 328 if wc_status[0].getAttribute('copied') == 'true': |
| 330 # Col 2 | 329 statuses[3] = '+' |
| 331 if wc_status[0].getAttribute('wc-locked') == 'true': | 330 # Col 4 |
| 332 statuses[2] = 'L' | 331 if wc_status[0].getAttribute('switched') == 'true': |
| 333 # Col 3 | 332 statuses[4] = 'S' |
| 334 if wc_status[0].getAttribute('copied') == 'true': | 333 # TODO(maruel): Col 5 and 6 |
| 335 statuses[3] = '+' | 334 item = (''.join(statuses), file_path) |
| 336 # Col 4 | 335 results.append(item) |
| 337 if wc_status[0].getAttribute('switched') == 'true': | 336 return results |
| 338 statuses[4] = 'S' | |
| 339 # TODO(maruel): Col 5 and 6 | |
| 340 item = (''.join(statuses), file_path) | |
| 341 results.append(item) | |
| 342 return results | |
| 343 | |
| 344 @staticmethod | |
| 345 def IsMoved(filename): | |
| 346 """Determine if a file has been added through svn mv""" | |
| 347 info = SVN.CaptureInfo(filename) | |
| 348 return (info.get('Copied From URL') and | |
| 349 info.get('Copied From Rev') and | |
| 350 info.get('Schedule') == 'add') | |
| 351 | |
| 352 @staticmethod | |
| 353 def GetFileProperty(file, property_name): | |
| 354 """Returns the value of an SVN property for the given file. | |
| 355 | |
| 356 Args: | |
| 357 file: The file to check | |
| 358 property_name: The name of the SVN property, e.g. "svn:mime-type" | |
| 359 | |
| 360 Returns: | |
| 361 The value of the property, which will be the empty string if the property | |
| 362 is not set on the file. If the file is not under version control, the | |
| 363 empty string is also returned. | |
| 364 """ | |
| 365 output = SVN.Run(["propget", property_name, file], None) | |
| 366 if (output and | |
| 367 output.startswith("svn: ") and | |
| 368 output.endswith("is not under version control")): | |
| 369 return "" | |
| 370 else: | |
| 371 return output | |
| 372 | |
| 373 @staticmethod | |
| 374 def DiffItem(filename): | |
| 375 """Diff a single file""" | |
| 376 # Use svn info output instead of os.path.isdir because the latter fails | |
| 377 # when the file is deleted. | |
| 378 if SVN.CaptureInfo(filename).get("Node Kind") == "directory": | |
| 379 return None | |
| 380 # If the user specified a custom diff command in their svn config file, | |
| 381 # then it'll be used when we do svn diff, which we don't want to happen | |
| 382 # since we want the unified diff. Using --diff-cmd=diff doesn't always | |
| 383 # work, since they can have another diff executable in their path that | |
| 384 # gives different line endings. So we use a bogus temp directory as the | |
| 385 # config directory, which gets around these problems. | |
| 386 if sys.platform.startswith("win"): | |
| 387 parent_dir = tempfile.gettempdir() | |
| 388 else: | |
| 389 parent_dir = sys.path[0] # tempdir is not secure. | |
| 390 bogus_dir = os.path.join(parent_dir, "temp_svn_config") | |
| 391 if not os.path.exists(bogus_dir): | |
| 392 os.mkdir(bogus_dir) | |
| 393 # Grabs the diff data. | |
| 394 data = SVN.Capture(["diff", "--config-dir", bogus_dir, filename], None) | |
| 395 | |
| 396 # We know the diff will be incorrectly formatted. Fix it. | |
| 397 if SVN.IsMoved(filename): | |
| 398 # The file is "new" in the patch sense. Generate a homebrew diff. | |
| 399 # We can't use ReadFile() since it's not using binary mode. | |
| 400 file_handle = open(filename, 'rb') | |
| 401 file_content = file_handle.read() | |
| 402 file_handle.close() | |
| 403 # Prepend '+' to every lines. | |
| 404 file_content = ['+' + i for i in file_content.splitlines(True)] | |
| 405 nb_lines = len(file_content) | |
| 406 # We need to use / since patch on unix will fail otherwise. | |
| 407 filename = filename.replace('\\', '/') | |
| 408 data = "Index: %s\n" % filename | |
| 409 data += ("=============================================================" | |
| 410 "======\n") | |
| 411 # Note: Should we use /dev/null instead? | |
| 412 data += "--- %s\n" % filename | |
| 413 data += "+++ %s\n" % filename | |
| 414 data += "@@ -0,0 +1,%d @@\n" % nb_lines | |
| 415 data += ''.join(file_content) | |
| 416 return data | |
| OLD | NEW |