summaryrefslogtreecommitdiff
path: root/fastimport/commands.py
diff options
context:
space:
mode:
Diffstat (limited to 'fastimport/commands.py')
-rw-r--r--fastimport/commands.py444
1 files changed, 444 insertions, 0 deletions
diff --git a/fastimport/commands.py b/fastimport/commands.py
new file mode 100644
index 0000000..7368070
--- /dev/null
+++ b/fastimport/commands.py
@@ -0,0 +1,444 @@
+# Copyright (C) 2008 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+"""Import command classes."""
+
+
+# There is a bug in git 1.5.4.3 and older by which unquoting a string consumes
+# one extra character. Set this variable to True to work-around it. It only
+# happens when renaming a file whose name contains spaces and/or quotes, and
+# the symptom is:
+# % git-fast-import
+# fatal: Missing space after source: R "file 1.txt" file 2.txt
+# http://git.kernel.org/?p=git/git.git;a=commit;h=c8744d6a8b27115503565041566d97c21e722584
+GIT_FAST_IMPORT_NEEDS_EXTRA_SPACE_AFTER_QUOTE = False
+
+
+# Lists of command names
+COMMAND_NAMES = ['blob', 'checkpoint', 'commit', 'feature', 'progress',
+ 'reset', 'tag']
+FILE_COMMAND_NAMES = ['filemodify', 'filedelete', 'filecopy', 'filerename',
+ 'filedeleteall']
+
+# Bazaar file kinds
+FILE_KIND = 'file'
+DIRECTORY_KIND = 'directory'
+SYMLINK_KIND = 'symlink'
+TREE_REFERENCE_KIND = 'tree-reference'
+
+# Feature names
+MULTIPLE_AUTHORS_FEATURE = "multiple-authors"
+COMMIT_PROPERTIES_FEATURE = "commit-properties"
+EMPTY_DIRS_FEATURE = "empty-directories"
+FEATURE_NAMES = [
+ MULTIPLE_AUTHORS_FEATURE,
+ COMMIT_PROPERTIES_FEATURE,
+ EMPTY_DIRS_FEATURE,
+ ]
+
+
+class ImportCommand(object):
+ """Base class for import commands."""
+
+ def __init__(self, name):
+ self.name = name
+ # List of field names not to display
+ self._binary = []
+
+ def __str__(self):
+ return repr(self)
+
+ def dump_str(self, names=None, child_lists=None, verbose=False):
+ """Dump fields as a string.
+
+ :param names: the list of fields to include or
+ None for all public fields
+ :param child_lists: dictionary of child command names to
+ fields for that child command to include
+ :param verbose: if True, prefix each line with the command class and
+ display fields as a dictionary; if False, dump just the field
+ values with tabs between them
+ """
+ interesting = {}
+ if names is None:
+ fields = [k for k in self.__dict__.keys() if not k.startswith('_')]
+ else:
+ fields = names
+ for field in fields:
+ value = self.__dict__.get(field)
+ if field in self._binary and value is not None:
+ value = '(...)'
+ interesting[field] = value
+ if verbose:
+ return "%s: %s" % (self.__class__.__name__, interesting)
+ else:
+ return "\t".join([repr(interesting[k]) for k in fields])
+
+
+class BlobCommand(ImportCommand):
+
+ def __init__(self, mark, data, lineno=0):
+ ImportCommand.__init__(self, 'blob')
+ self.mark = mark
+ self.data = data
+ self.lineno = lineno
+ # Provide a unique id in case the mark is missing
+ if mark is None:
+ self.id = '@%d' % lineno
+ else:
+ self.id = ':' + mark
+ self._binary = ['data']
+
+ def __repr__(self):
+ if self.mark is None:
+ mark_line = ""
+ else:
+ mark_line = "\nmark :%s" % self.mark
+ return "blob%s\ndata %d\n%s" % (mark_line, len(self.data), self.data)
+
+
+class CheckpointCommand(ImportCommand):
+
+ def __init__(self):
+ ImportCommand.__init__(self, 'checkpoint')
+
+ def __repr__(self):
+ return "checkpoint"
+
+
+class CommitCommand(ImportCommand):
+
+ def __init__(self, ref, mark, author, committer, message, from_,
+ merges, file_iter, lineno=0, more_authors=None, properties=None):
+ ImportCommand.__init__(self, 'commit')
+ self.ref = ref
+ self.mark = mark
+ self.author = author
+ self.committer = committer
+ self.message = message
+ self.from_ = from_
+ self.merges = merges
+ self.file_iter = file_iter
+ self.more_authors = more_authors
+ self.properties = properties
+ self.lineno = lineno
+ self._binary = ['file_iter']
+ # Provide a unique id in case the mark is missing
+ if mark is None:
+ self.id = '@%d' % lineno
+ else:
+ self.id = ':%s' % mark
+
+ def __repr__(self):
+ return self.to_string(include_file_contents=True)
+
+ def __str__(self):
+ return self.to_string(include_file_contents=False)
+
+ def to_string(self, use_features=True, include_file_contents=False):
+ if self.mark is None:
+ mark_line = ""
+ else:
+ mark_line = "\nmark :%s" % self.mark
+ if self.author is None:
+ author_section = ""
+ else:
+ author_section = "\nauthor %s" % format_who_when(self.author)
+ if use_features and self.more_authors:
+ for author in self.more_authors:
+ author_section += "\nauthor %s" % format_who_when(author)
+ committer = "committer %s" % format_who_when(self.committer)
+ if self.message is None:
+ msg_section = ""
+ else:
+ msg = self.message.encode('utf8')
+ msg_section = "\ndata %d\n%s" % (len(msg), msg)
+ if self.from_ is None:
+ from_line = ""
+ else:
+ from_line = "\nfrom %s" % self.from_
+ if self.merges is None:
+ merge_lines = ""
+ else:
+ merge_lines = "".join(["\nmerge %s" % (m,)
+ for m in self.merges])
+ if use_features and self.properties:
+ property_lines = []
+ for name in sorted(self.properties):
+ value = self.properties[name]
+ property_lines.append("\n" + format_property(name, value))
+ properties_section = "".join(property_lines)
+ else:
+ properties_section = ""
+ if self.file_iter is None:
+ filecommands = ""
+ else:
+ if include_file_contents:
+ format_str = "\n%r"
+ else:
+ format_str = "\n%s"
+ filecommands = "".join([format_str % (c,)
+ for c in self.iter_files()])
+ return "commit %s%s%s\n%s%s%s%s%s%s" % (self.ref, mark_line,
+ author_section, committer, msg_section, from_line, merge_lines,
+ properties_section, filecommands)
+
+ def dump_str(self, names=None, child_lists=None, verbose=False):
+ result = [ImportCommand.dump_str(self, names, verbose=verbose)]
+ for f in self.iter_files():
+ if child_lists is None:
+ continue
+ try:
+ child_names = child_lists[f.name]
+ except KeyError:
+ continue
+ result.append("\t%s" % f.dump_str(child_names, verbose=verbose))
+ return '\n'.join(result)
+
+ def iter_files(self):
+ """Iterate over files."""
+ # file_iter may be a callable or an iterator
+ if callable(self.file_iter):
+ return self.file_iter()
+ elif self.file_iter:
+ return iter(self.file_iter)
+
+
+class FeatureCommand(ImportCommand):
+
+ def __init__(self, feature_name, value=None, lineno=0):
+ ImportCommand.__init__(self, 'feature')
+ self.feature_name = feature_name
+ self.value = value
+ self.lineno = lineno
+
+ def __repr__(self):
+ if self.value is None:
+ value_text = ""
+ else:
+ value_text = "=%s" % self.value
+ return "feature %s%s" % (self.feature_name, value_text)
+
+
+class ProgressCommand(ImportCommand):
+
+ def __init__(self, message):
+ ImportCommand.__init__(self, 'progress')
+ self.message = message
+
+ def __repr__(self):
+ return "progress %s" % (self.message,)
+
+
+class ResetCommand(ImportCommand):
+
+ def __init__(self, ref, from_):
+ ImportCommand.__init__(self, 'reset')
+ self.ref = ref
+ self.from_ = from_
+
+ def __repr__(self):
+ if self.from_ is None:
+ from_line = ""
+ else:
+ # According to git-fast-import(1), the extra LF is optional here;
+ # however, versions of git up to 1.5.4.3 had a bug by which the LF
+ # was needed. Always emit it, since it doesn't hurt and maintains
+ # compatibility with older versions.
+ # http://git.kernel.org/?p=git/git.git;a=commit;h=655e8515f279c01f525745d443f509f97cd805ab
+ from_line = "\nfrom %s\n" % self.from_
+ return "reset %s%s" % (self.ref, from_line)
+
+
+class TagCommand(ImportCommand):
+
+ def __init__(self, id, from_, tagger, message):
+ ImportCommand.__init__(self, 'tag')
+ self.id = id
+ self.from_ = from_
+ self.tagger = tagger
+ self.message = message
+
+ def __repr__(self):
+ if self.from_ is None:
+ from_line = ""
+ else:
+ from_line = "\nfrom %s" % self.from_
+ if self.tagger is None:
+ tagger_line = ""
+ else:
+ tagger_line = "\ntagger %s" % format_who_when(self.tagger)
+ if self.message is None:
+ msg_section = ""
+ else:
+ msg = self.message.encode('utf8')
+ msg_section = "\ndata %d\n%s" % (len(msg), msg)
+ return "tag %s%s%s%s" % (self.id, from_line, tagger_line, msg_section)
+
+
+class FileCommand(ImportCommand):
+ """Base class for file commands."""
+ pass
+
+
+class FileModifyCommand(FileCommand):
+
+ def __init__(self, path, kind, is_executable, dataref, data):
+ # Either dataref or data should be null
+ FileCommand.__init__(self, 'filemodify')
+ self.path = check_path(path)
+ self.kind = kind
+ self.is_executable = is_executable
+ self.dataref = dataref
+ self.data = data
+ self._binary = ['data']
+
+ def __repr__(self):
+ return self.to_string(include_file_contents=True)
+
+ def __str__(self):
+ return self.to_string(include_file_contents=False)
+
+ def to_string(self, include_file_contents=False):
+ if self.is_executable:
+ mode = "755"
+ elif self.kind == 'file':
+ mode = "644"
+ elif self.kind == 'directory':
+ mode = "040000"
+ elif self.kind == 'symlink':
+ mode = "120000"
+ elif self.kind == 'tree-reference':
+ mode = "160000"
+ else:
+ raise AssertionError("unknown kind %s" % (self.kind,))
+ datastr = ""
+ if self.kind == 'directory':
+ dataref = '-'
+ elif self.dataref is None:
+ dataref = "inline"
+ if include_file_contents:
+ datastr = "\ndata %d\n%s" % (len(self.data), self.data)
+ else:
+ dataref = "%s" % (self.dataref,)
+ path = format_path(self.path)
+ return "M %s %s %s%s" % (mode, dataref, path, datastr)
+
+
+class FileDeleteCommand(FileCommand):
+
+ def __init__(self, path):
+ FileCommand.__init__(self, 'filedelete')
+ self.path = check_path(path)
+
+ def __repr__(self):
+ return "D %s" % (format_path(self.path),)
+
+
+class FileCopyCommand(FileCommand):
+
+ def __init__(self, src_path, dest_path):
+ FileCommand.__init__(self, 'filecopy')
+ self.src_path = check_path(src_path)
+ self.dest_path = check_path(dest_path)
+
+ def __repr__(self):
+ return "C %s %s" % (
+ format_path(self.src_path, quote_spaces=True),
+ format_path(self.dest_path))
+
+
+class FileRenameCommand(FileCommand):
+
+ def __init__(self, old_path, new_path):
+ FileCommand.__init__(self, 'filerename')
+ self.old_path = check_path(old_path)
+ self.new_path = check_path(new_path)
+
+ def __repr__(self):
+ return "R %s %s" % (
+ format_path(self.old_path, quote_spaces=True),
+ format_path(self.new_path))
+
+
+class FileDeleteAllCommand(FileCommand):
+
+ def __init__(self):
+ FileCommand.__init__(self, 'filedeleteall')
+
+ def __repr__(self):
+ return "deleteall"
+
+
+def check_path(path):
+ """Check that a path is legal.
+
+ :return: the path if all is OK
+ :raise ValueError: if the path is illegal
+ """
+ if path is None or path == '':
+ raise ValueError("illegal path '%s'" % path)
+ return path
+
+
+def format_path(p, quote_spaces=False):
+ """Format a path in utf8, quoting it if necessary."""
+ if '\n' in p:
+ import re
+ p = re.sub('\n', '\\n', p)
+ quote = True
+ else:
+ quote = p[0] == '"' or (quote_spaces and ' ' in p)
+ if quote:
+ extra = GIT_FAST_IMPORT_NEEDS_EXTRA_SPACE_AFTER_QUOTE and ' ' or ''
+ p = '"%s"%s' % (p, extra)
+ return p.encode('utf8')
+
+
+def format_who_when(fields):
+ """Format a tuple of name,email,secs-since-epoch,utc-offset-secs as a string."""
+ offset = fields[3]
+ if offset < 0:
+ offset_sign = '-'
+ offset = abs(offset)
+ else:
+ offset_sign = '+'
+ offset_hours = offset / 3600
+ offset_minutes = offset / 60 - offset_hours * 60
+ offset_str = "%s%02d%02d" % (offset_sign, offset_hours, offset_minutes)
+ name = fields[0]
+ if name == '':
+ sep = ''
+ else:
+ sep = ' '
+ if isinstance(name, unicode):
+ name = name.encode('utf8')
+ email = fields[1]
+ if isinstance(email, unicode):
+ email = email.encode('utf8')
+ result = "%s%s<%s> %d %s" % (name, sep, email, fields[2], offset_str)
+ return result
+
+
+def format_property(name, value):
+ """Format the name and value (both unicode) of a property as a string."""
+ utf8_name = name.encode('utf8')
+ if value is not None:
+ utf8_value = value.encode('utf8')
+ result = "property %s %d %s" % (utf8_name, len(utf8_value), utf8_value)
+ else:
+ result = "property %s" % (utf8_name,)
+ return result