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