comparison hgext3rd/fastimport/vendor/python_fastimport/commands.py @ 86:28704a2a7461 vendor/python-fastimport

Import python-fastimport-0.9.8
author Roy Marples <roy@marples.name>
date Tue, 19 Jan 2021 22:56:34 +0000
parents
children 2fc99e3479d9
comparison
equal deleted inserted replaced
85:1f5544a8870b 86:28704a2a7461
1 # Copyright (C) 2008 Canonical Ltd
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16 """fast-import command classes.
17
18 These objects are used by the parser to represent the content of
19 a fast-import stream.
20 """
21 from __future__ import division
22
23 import re
24 import stat
25 import sys
26
27 from fastimport.helpers import (
28 newobject as object,
29 utf8_bytes_string,
30 repr_bytes,
31 )
32
33
34 # There is a bug in git 1.5.4.3 and older by which unquoting a string consumes
35 # one extra character. Set this variable to True to work-around it. It only
36 # happens when renaming a file whose name contains spaces and/or quotes, and
37 # the symptom is:
38 # % git-fast-import
39 # fatal: Missing space after source: R "file 1.txt" file 2.txt
40 # http://git.kernel.org/?p=git/git.git;a=commit;h=c8744d6a8b27115503565041566d97c21e722584
41 GIT_FAST_IMPORT_NEEDS_EXTRA_SPACE_AFTER_QUOTE = False
42
43
44 # Lists of command names
45 COMMAND_NAMES = [b'blob', b'checkpoint', b'commit', b'feature', b'progress',
46 b'reset', b'tag']
47 FILE_COMMAND_NAMES = [b'filemodify', b'filedelete', b'filecopy', b'filerename',
48 b'filedeleteall']
49
50 # Feature names
51 MULTIPLE_AUTHORS_FEATURE = b'multiple-authors'
52 COMMIT_PROPERTIES_FEATURE = b'commit-properties'
53 EMPTY_DIRS_FEATURE = b'empty-directories'
54 FEATURE_NAMES = [
55 MULTIPLE_AUTHORS_FEATURE,
56 COMMIT_PROPERTIES_FEATURE,
57 EMPTY_DIRS_FEATURE,
58 ]
59
60
61 class ImportCommand(object):
62 """Base class for import commands."""
63
64 def __init__(self, name):
65 self.name = name
66 # List of field names not to display
67 self._binary = []
68
69 def __str__(self):
70 return repr(self)
71
72 def __repr__(self):
73 if sys.version_info[0] == 2:
74 return self.__bytes__()
75 else:
76 return bytes(self).decode('utf8')
77
78 def __bytes__(self):
79 raise NotImplementedError(
80 'An implementation of __bytes__ is required'
81 )
82
83 def dump_str(self, names=None, child_lists=None, verbose=False):
84 """Dump fields as a string.
85
86 For debugging.
87
88 :param names: the list of fields to include or
89 None for all public fields
90 :param child_lists: dictionary of child command names to
91 fields for that child command to include
92 :param verbose: if True, prefix each line with the command class and
93 display fields as a dictionary; if False, dump just the field
94 values with tabs between them
95 """
96 interesting = {}
97 if names is None:
98 fields = [
99 k for k in list(self.__dict__.keys())
100 if not k.startswith(b'_')
101 ]
102 else:
103 fields = names
104 for field in fields:
105 value = self.__dict__.get(field)
106 if field in self._binary and value is not None:
107 value = b'(...)'
108 interesting[field] = value
109 if verbose:
110 return "%s: %s" % (self.__class__.__name__, interesting)
111 else:
112 return "\t".join([repr(interesting[k]) for k in fields])
113
114
115 class BlobCommand(ImportCommand):
116
117 def __init__(self, mark, data, lineno=0):
118 ImportCommand.__init__(self, b'blob')
119 self.mark = mark
120 self.data = data
121 self.lineno = lineno
122 # Provide a unique id in case the mark is missing
123 if mark is None:
124 self.id = b'@' + ("%d" % lineno).encode('utf-8')
125 else:
126 self.id = b':' + mark
127 self._binary = [b'data']
128
129 def __bytes__(self):
130 if self.mark is None:
131 mark_line = b''
132 else:
133 mark_line = b"\nmark :" + self.mark
134 return (b'blob' + mark_line + b'\n' +
135 ('data %d\n' % len(self.data)).encode('utf-8') + self.data)
136
137
138 class CheckpointCommand(ImportCommand):
139
140 def __init__(self):
141 ImportCommand.__init__(self, b'checkpoint')
142
143 def __bytes__(self):
144 return b'checkpoint'
145
146
147 class CommitCommand(ImportCommand):
148
149 def __init__(self, ref, mark, author, committer, message, from_,
150 merges, file_iter, lineno=0, more_authors=None, properties=None):
151 ImportCommand.__init__(self, b'commit')
152 self.ref = ref
153 self.mark = mark
154 self.author = author
155 self.committer = committer
156 self.message = message
157 self.from_ = from_
158 self.merges = merges
159 self.file_iter = file_iter
160 self.more_authors = more_authors
161 self.properties = properties
162 self.lineno = lineno
163 self._binary = [b'file_iter']
164 # Provide a unique id in case the mark is missing
165 if self.mark is None:
166 self.id = b'@' + ('%d' % lineno).encode('utf-8')
167 else:
168 if isinstance(self.mark, (int)):
169 self.id = b':' + str(self.mark).encode('utf-8')
170 else:
171 self.id = b':' + self.mark
172
173 def copy(self, **kwargs):
174 if not isinstance(self.file_iter, list):
175 self.file_iter = list(self.file_iter)
176
177 fields = dict(
178 (key, value)
179 for key, value in self.__dict__.items()
180 if key not in ('id', 'name')
181 if not key.startswith('_')
182 )
183
184 fields.update(kwargs)
185
186 return CommitCommand(**fields)
187
188 def __bytes__(self):
189 return self.to_string(include_file_contents=True)
190
191
192 def to_string(self, use_features=True, include_file_contents=False):
193 """
194 @todo the name to_string is ambiguous since the method actually
195 returns bytes.
196 """
197 if self.mark is None:
198 mark_line = b''
199 else:
200 if isinstance(self.mark, (int)):
201 mark_line = b'\nmark :' + str(self.mark).encode('utf-8')
202 else:
203 mark_line = b'\nmark :' + self.mark
204
205 if self.author is None:
206 author_section = b''
207 else:
208 author_section = b'\nauthor ' + format_who_when(self.author)
209 if use_features and self.more_authors:
210 for author in self.more_authors:
211 author_section += b'\nauthor ' + format_who_when(author)
212
213 committer = b'committer ' + format_who_when(self.committer)
214
215 if self.message is None:
216 msg_section = b''
217 else:
218 msg = self.message
219 msg_section = ('\ndata %d\n' % len(msg)).encode('ascii') + msg
220 if self.from_ is None:
221 from_line = b''
222 else:
223 from_line = b'\nfrom ' + self.from_
224 if self.merges is None:
225 merge_lines = b''
226 else:
227 merge_lines = b''.join([b'\nmerge ' + m
228 for m in self.merges])
229 if use_features and self.properties:
230 property_lines = []
231 for name in sorted(self.properties):
232 value = self.properties[name]
233 property_lines.append(b'\n' + format_property(name, value))
234 properties_section = b''.join(property_lines)
235 else:
236 properties_section = b''
237 if self.file_iter is None:
238 filecommands = b''
239 else:
240 if include_file_contents:
241 filecommands = b''.join([b'\n' + repr_bytes(c)
242 for c in self.iter_files()])
243 else:
244 filecommands = b''.join([b'\n' + str(c)
245 for c in self.iter_files()])
246 return b''.join([
247 b'commit ',
248 self.ref,
249 mark_line,
250 author_section + b'\n',
251 committer,
252 msg_section,
253 from_line,
254 merge_lines,
255 properties_section,
256 filecommands])
257
258 def dump_str(self, names=None, child_lists=None, verbose=False):
259 result = [ImportCommand.dump_str(self, names, verbose=verbose)]
260 for f in self.iter_files():
261 if child_lists is None:
262 continue
263 try:
264 child_names = child_lists[f.name]
265 except KeyError:
266 continue
267 result.append('\t%s' % f.dump_str(child_names, verbose=verbose))
268 return '\n'.join(result)
269
270 def iter_files(self):
271 """Iterate over files."""
272 # file_iter may be a callable or an iterator
273 if callable(self.file_iter):
274 return self.file_iter()
275 return iter(self.file_iter)
276
277
278 class FeatureCommand(ImportCommand):
279
280 def __init__(self, feature_name, value=None, lineno=0):
281 ImportCommand.__init__(self, b'feature')
282 self.feature_name = feature_name
283 self.value = value
284 self.lineno = lineno
285
286 def __bytes__(self):
287 if self.value is None:
288 value_text = b''
289 else:
290 value_text = b'=' + self.value
291 return b'feature ' + self.feature_name + value_text
292
293
294 class ProgressCommand(ImportCommand):
295
296 def __init__(self, message):
297 ImportCommand.__init__(self, b'progress')
298 self.message = message
299
300 def __bytes__(self):
301 return b'progress ' + self.message
302
303
304 class ResetCommand(ImportCommand):
305
306 def __init__(self, ref, from_):
307 ImportCommand.__init__(self, b'reset')
308 self.ref = ref
309 self.from_ = from_
310
311 def __bytes__(self):
312 if self.from_ is None:
313 from_line = b''
314 else:
315 # According to git-fast-import(1), the extra LF is optional here;
316 # however, versions of git up to 1.5.4.3 had a bug by which the LF
317 # was needed. Always emit it, since it doesn't hurt and maintains
318 # compatibility with older versions.
319 # http://git.kernel.org/?p=git/git.git;a=commit;h=655e8515f279c01f525745d443f509f97cd805ab
320 from_line = b'\nfrom ' + self.from_ + b'\n'
321 return b'reset ' + self.ref + from_line
322
323
324 class TagCommand(ImportCommand):
325
326 def __init__(self, id, from_, tagger, message):
327 ImportCommand.__init__(self, b'tag')
328 self.id = id
329 self.from_ = from_
330 self.tagger = tagger
331 self.message = message
332
333 def __bytes__(self):
334 if self.from_ is None:
335 from_line = b''
336 else:
337 from_line = b'\nfrom ' + self.from_
338 if self.tagger is None:
339 tagger_line = b''
340 else:
341 tagger_line = b'\ntagger ' + format_who_when(self.tagger)
342 if self.message is None:
343 msg_section = b''
344 else:
345 msg = self.message
346 msg_section = ('\ndata %d\n' % len(msg)).encode('ascii') + msg
347 return b'tag ' + self.id + from_line + tagger_line + msg_section
348
349
350 class FileCommand(ImportCommand):
351 """Base class for file commands."""
352 pass
353
354
355 class FileModifyCommand(FileCommand):
356
357 def __init__(self, path, mode, dataref, data):
358 # Either dataref or data should be null
359 FileCommand.__init__(self, b'filemodify')
360 self.path = check_path(path)
361 self.mode = mode
362 self.dataref = dataref
363 self.data = data
364 self._binary = [b'data']
365
366 def __bytes__(self):
367 return self.to_string(include_file_contents=True)
368
369 def __str__(self):
370 return self.to_string(include_file_contents=False)
371
372 def _format_mode(self, mode):
373 if mode in (0o755, 0o100755):
374 return b'755'
375 elif mode in (0o644, 0o100644):
376 return b'644'
377 elif mode == 0o40000:
378 return b'040000'
379 elif mode == 0o120000:
380 return b'120000'
381 elif mode == 0o160000:
382 return b'160000'
383 else:
384 raise AssertionError('Unknown mode %o' % mode)
385
386 def to_string(self, include_file_contents=False):
387 datastr = b''
388 if stat.S_ISDIR(self.mode):
389 dataref = b'-'
390 elif self.dataref is None:
391 dataref = b'inline'
392 if include_file_contents:
393 datastr = ('\ndata %d\n' % len(self.data)).encode('ascii') + self.data
394 else:
395 dataref = self.dataref
396 path = format_path(self.path)
397
398 return b' '.join(
399 [b'M', self._format_mode(self.mode), dataref, path + datastr])
400
401
402 class FileDeleteCommand(FileCommand):
403
404 def __init__(self, path):
405 FileCommand.__init__(self, b'filedelete')
406 self.path = check_path(path)
407
408 def __bytes__(self):
409 return b' '.join([b'D', format_path(self.path)])
410
411
412 class FileCopyCommand(FileCommand):
413
414 def __init__(self, src_path, dest_path):
415 FileCommand.__init__(self, b'filecopy')
416 self.src_path = check_path(src_path)
417 self.dest_path = check_path(dest_path)
418
419 def __bytes__(self):
420 return b' '.join([b'C',
421 format_path(self.src_path, quote_spaces=True),
422 format_path(self.dest_path)])
423
424
425 class FileRenameCommand(FileCommand):
426
427 def __init__(self, old_path, new_path):
428 FileCommand.__init__(self, b'filerename')
429 self.old_path = check_path(old_path)
430 self.new_path = check_path(new_path)
431
432 def __bytes__(self):
433 return b' '.join([
434 b'R',
435 format_path(self.old_path, quote_spaces=True),
436 format_path(self.new_path)]
437 )
438
439
440 class FileDeleteAllCommand(FileCommand):
441
442 def __init__(self):
443 FileCommand.__init__(self, b'filedeleteall')
444
445 def __bytes__(self):
446 return b'deleteall'
447
448
449 class NoteModifyCommand(FileCommand):
450
451 def __init__(self, from_, data):
452 super(NoteModifyCommand, self).__init__(b'notemodify')
453 self.from_ = from_
454 self.data = data
455 self._binary = ['data']
456
457 def __bytes__(self):
458 return (b'N inline :' + self.from_ +
459 ('\ndata %d\n'% len(self.data)).encode('ascii') + self.data)
460
461
462 def check_path(path):
463 """Check that a path is legal.
464
465 :return: the path if all is OK
466 :raise ValueError: if the path is illegal
467 """
468 if path is None or path == b'' or path.startswith(b'/'):
469 raise ValueError("illegal path '%s'" % path)
470
471 if (
472 (sys.version_info[0] >= 3 and not isinstance(path, bytes)) and
473 (sys.version_info[0] == 2 and not isinstance(path, str))
474 ):
475 raise TypeError("illegale type for path '%r'" % path)
476
477 return path
478
479
480 def format_path(p, quote_spaces=False):
481 """Format a path in utf8, quoting it if necessary."""
482 if b'\n' in p:
483 p = re.sub(b'\n', b'\\n', p)
484 quote = True
485 else:
486 quote = p[0] == b'"' or (quote_spaces and b' ' in p)
487 if quote:
488 extra = GIT_FAST_IMPORT_NEEDS_EXTRA_SPACE_AFTER_QUOTE and b' ' or b''
489 p = b'"' + p + b'"' + extra
490 return p
491
492
493 def format_who_when(fields):
494 """Format a tuple of name,email,secs-since-epoch,utc-offset-secs as a string."""
495 offset = fields[3]
496 if offset < 0:
497 offset_sign = b'-'
498 offset = abs(offset)
499 else:
500 offset_sign = b'+'
501 offset_hours = offset // 3600
502 offset_minutes = offset // 60 - offset_hours * 60
503 offset_str = offset_sign + ('%02d%02d' % (offset_hours, offset_minutes)).encode('ascii')
504 name = fields[0]
505
506 if name == b'':
507 sep = b''
508 else:
509 sep = b' '
510
511 name = utf8_bytes_string(name)
512
513 email = fields[1]
514
515 email = utf8_bytes_string(email)
516
517 return b''.join((name, sep, b'<', email, b'> ', ("%d" % fields[2]).encode('ascii'), b' ', offset_str))
518
519
520 def format_property(name, value):
521 """Format the name and value (both unicode) of a property as a string."""
522 result = b''
523 utf8_name = utf8_bytes_string(name)
524
525 result = b'property ' + utf8_name
526 if value is not None:
527 utf8_value = utf8_bytes_string(value)
528 result += b' ' + ('%d' % len(utf8_value)).encode('ascii') + b' ' + utf8_value
529
530 return result