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 |