Mercurial > hg > hg-fastimport
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 |
