git_remote_helpers: add fastimport library
[git/dscho.git] / git_remote_helpers / fastimport / commands.py
blobb3c86c4910900a7db99d3a2bd91c913a65d69371
1 # Copyright (C) 2008 Canonical Ltd
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.
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.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 """Import command classes."""
19 import os
21 # There is a bug in git 1.5.4.3 and older by which unquoting a string consumes
22 # one extra character. Set this variable to True to work-around it. It only
23 # happens when renaming a file whose name contains spaces and/or quotes, and
24 # the symptom is:
25 # % git-fast-import
26 # fatal: Missing space after source: R "file 1.txt" file 2.txt
27 # http://git.kernel.org/?p=git/git.git;a=commit;h=c8744d6a8b27115503565041566d97c21e722584
28 GIT_FAST_IMPORT_NEEDS_EXTRA_SPACE_AFTER_QUOTE = False
31 # Lists of command names
32 COMMAND_NAMES = ['blob', 'checkpoint', 'commit', 'feature', 'progress',
33 'reset', 'tag']
34 FILE_COMMAND_NAMES = ['filemodify', 'filedelete', 'filecopy', 'filerename',
35 'filedeleteall']
38 # Feature names
39 MULTIPLE_AUTHORS_FEATURE = "multiple-authors"
40 COMMIT_PROPERTIES_FEATURE = "commit-properties"
41 EMPTY_DIRS_FEATURE = "empty-directories"
42 FEATURE_NAMES = [
43 MULTIPLE_AUTHORS_FEATURE,
44 COMMIT_PROPERTIES_FEATURE,
45 EMPTY_DIRS_FEATURE,
49 # for classes with no meaningful __str__()
50 def _simplerepr(self):
51 return "<%s at 0x%x>" % (self.__class__.__name__, id(self))
53 # classes that define __str__() should use this instead
54 def _detailrepr(self):
55 return ("<%s at 0x%x: %s>"
56 % (self.__class__.__name__, id(self), str(self)))
59 class ImportCommand(object):
60 """Base class for import commands."""
62 def __init__(self, name):
63 self.name = name
64 # List of field names not to display
65 self._binary = []
67 __repr__ = _simplerepr
69 def format(self):
70 """Format this command as a fastimport dump fragment.
72 Returns a (possibly multiline) string that, if seen in a
73 fastimport stream, would parse to an equivalent command object.
74 """
75 raise NotImplementedError("abstract method")
77 def dump_str(self, names=None, child_lists=None, verbose=False):
78 """Dump fields as a string.
80 :param names: the list of fields to include or
81 None for all public fields
82 :param child_lists: dictionary of child command names to
83 fields for that child command to include
84 :param verbose: if True, prefix each line with the command class and
85 display fields as a dictionary; if False, dump just the field
86 values with tabs between them
87 """
88 interesting = {}
89 if names is None:
90 fields = [k for k in self.__dict__.keys() if not k.startswith('_')]
91 else:
92 fields = names
93 for field in fields:
94 value = self.__dict__.get(field)
95 if field in self._binary and value is not None:
96 value = '(...)'
97 interesting[field] = value
98 if verbose:
99 return "%s: %s" % (self.__class__.__name__, interesting)
100 else:
101 return "\t".join([repr(interesting[k]) for k in fields])
104 class _MarkMixin(object):
105 """mixin for fastimport commands with a mark: blob, commit."""
106 def __init__(self, mark, location):
107 self.mark= mark
108 self.location = location
110 # Provide a unique id in case the mark is missing
111 if mark is None:
112 self.id = '%s@%d' % (os.path.basename(location[0]), location[1])
113 else:
114 self.id = ':%s' % mark
116 def __str__(self):
117 return self.id
119 __repr__ = _detailrepr
122 class BlobCommand(ImportCommand, _MarkMixin):
124 def __init__(self, mark, data, location):
125 ImportCommand.__init__(self, 'blob')
126 _MarkMixin.__init__(self, mark, location)
127 self.data = data
128 self._binary = ['data']
130 def format(self):
131 if self.mark is None:
132 mark_line = ""
133 else:
134 mark_line = "\nmark :%s" % self.mark
135 return "blob%s\ndata %d\n%s" % (mark_line, len(self.data), self.data)
138 class CheckpointCommand(ImportCommand):
140 def __init__(self):
141 ImportCommand.__init__(self, 'checkpoint')
143 def format(self):
144 return "checkpoint"
147 class CommitCommand(ImportCommand, _MarkMixin):
149 def __init__(self, ref, mark, author, committer, message, from_,
150 merges, file_cmds, location=None, more_authors=None, properties=None):
151 ImportCommand.__init__(self, 'commit')
152 _MarkMixin.__init__(self, mark, location)
153 self.ref = ref
154 self.author = author
155 self.committer = committer
156 self.message = message
157 self.from_ = from_
158 self.merges = merges
159 self.file_cmds = file_cmds
160 self.more_authors = more_authors
161 self.properties = properties
162 self._binary = ['file_cmds']
164 def format(self, use_features=True, include_file_contents=True):
165 if self.mark is None:
166 mark_line = ""
167 else:
168 mark_line = "\nmark :%s" % self.mark
169 if self.author is None:
170 author_section = ""
171 else:
172 author_section = "\nauthor %s" % format_who_when(self.author)
173 if use_features and self.more_authors:
174 for author in self.more_authors:
175 author_section += "\nauthor %s" % format_who_when(author)
176 committer = "committer %s" % format_who_when(self.committer)
177 if self.message is None:
178 msg_section = ""
179 else:
180 msg = self.message.encode('utf8')
181 msg_section = "\ndata %d\n%s" % (len(msg), msg)
182 if self.from_ is None:
183 from_line = ""
184 else:
185 from_line = "\nfrom %s" % self.from_
186 if self.merges is None:
187 merge_lines = ""
188 else:
189 merge_lines = "".join(["\nmerge %s" % (m,)
190 for m in self.merges])
191 if use_features and self.properties:
192 property_lines = []
193 for name in sorted(self.properties):
194 value = self.properties[name]
195 property_lines.append("\n" + format_property(name, value))
196 properties_section = "".join(property_lines)
197 else:
198 properties_section = ""
199 if self.file_cmds is None:
200 filecommands = ""
201 else:
202 if include_file_contents:
203 format_str = "\n%r"
204 else:
205 format_str = "\n%s"
206 filecommands = "".join(
207 ["\n" + fc.format() for fc in self.file_cmds])
208 return "commit %s%s%s\n%s%s%s%s%s%s" % (self.ref, mark_line,
209 author_section, committer, msg_section, from_line, merge_lines,
210 properties_section, filecommands)
212 def dump_str(self, names=None, child_lists=None, verbose=False):
213 result = [ImportCommand.dump_str(self, names, verbose=verbose)]
214 for f in self.file_cmds:
215 if child_lists is None:
216 continue
217 try:
218 child_names = child_lists[f.name]
219 except KeyError:
220 continue
221 result.append("\t%s" % f.dump_str(child_names, verbose=verbose))
222 return '\n'.join(result)
225 class FeatureCommand(ImportCommand):
227 def __init__(self, feature_name, value=None, location=None):
228 ImportCommand.__init__(self, 'feature')
229 self.feature_name = feature_name
230 self.value = value
231 self.location = location
233 def format(self):
234 if self.value is None:
235 value_text = ""
236 else:
237 value_text = "=%s" % self.value
238 return "feature %s%s" % (self.feature_name, value_text)
241 class ProgressCommand(ImportCommand):
243 def __init__(self, message):
244 ImportCommand.__init__(self, 'progress')
245 self.message = message
247 def format(self):
248 return "progress %s" % (self.message,)
251 class ResetCommand(ImportCommand):
253 def __init__(self, ref, from_):
254 ImportCommand.__init__(self, 'reset')
255 self.ref = ref
256 self.from_ = from_
258 def format(self):
259 if self.from_ is None:
260 from_line = ""
261 else:
262 # According to git-fast-import(1), the extra LF is optional here;
263 # however, versions of git up to 1.5.4.3 had a bug by which the LF
264 # was needed. Always emit it, since it doesn't hurt and maintains
265 # compatibility with older versions.
266 # http://git.kernel.org/?p=git/git.git;a=commit;h=655e8515f279c01f525745d443f509f97cd805ab
267 from_line = "\nfrom %s\n" % self.from_
268 return "reset %s%s" % (self.ref, from_line)
271 class TagCommand(ImportCommand):
273 def __init__(self, id, from_, tagger, message):
274 ImportCommand.__init__(self, 'tag')
275 self.id = id
276 self.from_ = from_
277 self.tagger = tagger
278 self.message = message
280 def __str__(self):
281 return self.id
283 __repr__ = _detailrepr
285 def format(self):
286 if self.from_ is None:
287 from_line = ""
288 else:
289 from_line = "\nfrom %s" % self.from_
290 if self.tagger is None:
291 tagger_line = ""
292 else:
293 tagger_line = "\ntagger %s" % format_who_when(self.tagger)
294 if self.message is None:
295 msg_section = ""
296 else:
297 msg = self.message.encode('utf8')
298 msg_section = "\ndata %d\n%s" % (len(msg), msg)
299 return "tag %s%s%s%s" % (self.id, from_line, tagger_line, msg_section)
302 class FileCommand(ImportCommand):
303 """Base class for file commands."""
304 pass
307 class FileModifyCommand(FileCommand):
309 def __init__(self, path, mode, dataref, data):
310 # Either dataref or data should be null
311 FileCommand.__init__(self, 'filemodify')
312 self.path = check_path(path)
313 self.mode = mode
314 self.dataref = dataref
315 self.data = data
316 self._binary = ['data']
318 def __str__(self):
319 return self.path
321 __repr__ = _detailrepr
323 def format(self, include_file_contents=True):
324 datastr = ""
325 if self.dataref is None:
326 dataref = "inline"
327 if include_file_contents:
328 datastr = "\ndata %d\n%s" % (len(self.data), self.data)
329 else:
330 dataref = "%s" % (self.dataref,)
331 path = format_path(self.path)
332 return "M %s %s %s%s" % (self.mode, dataref, path, datastr)
334 def is_regular(self):
335 """Return true if this is a regular file (mode 644)."""
336 return self.mode.endswith("644")
338 def is_executable(self):
339 """Return true if this is an executable file (mode 755)."""
340 return self.mode.endswith("755")
342 def is_symlink(self):
343 """Return true if this is a symlink (mode 120000)."""
344 return self.mode == "120000"
346 def is_gitlink(self):
347 """Return true if this is a gitlink (mode 160000)."""
348 return self.mode == "160000"
351 class FileDeleteCommand(FileCommand):
353 def __init__(self, path):
354 FileCommand.__init__(self, 'filedelete')
355 self.path = check_path(path)
357 def __str__(self):
358 return self.path
360 __repr__ = _detailrepr
362 def format(self):
363 return "D %s" % (format_path(self.path),)
366 class FileCopyCommand(FileCommand):
368 def __init__(self, src_path, dest_path):
369 FileCommand.__init__(self, 'filecopy')
370 self.src_path = check_path(src_path)
371 self.dest_path = check_path(dest_path)
373 def __str__(self):
374 return "%s -> %s" % (self.src_path, self.dest_path)
376 __repr__ = _detailrepr
378 def format(self):
379 return "C %s %s" % (
380 format_path(self.src_path, quote_spaces=True),
381 format_path(self.dest_path))
384 class FileRenameCommand(FileCommand):
386 def __init__(self, old_path, new_path):
387 FileCommand.__init__(self, 'filerename')
388 self.old_path = check_path(old_path)
389 self.new_path = check_path(new_path)
391 def __str__(self):
392 return "%s -> %s" % (self.old_path, self.new_path)
394 __repr__ = _detailrepr
396 def format(self):
397 return "R %s %s" % (
398 format_path(self.old_path, quote_spaces=True),
399 format_path(self.new_path))
402 class FileDeleteAllCommand(FileCommand):
404 def __init__(self):
405 FileCommand.__init__(self, 'filedeleteall')
407 def format(self):
408 return "deleteall"
411 def check_path(path):
412 """Check that a path is legal.
414 :return: the path if all is OK
415 :raise ValueError: if the path is illegal
417 if path is None or path == '':
418 raise ValueError("illegal path '%s'" % path)
419 return path
422 def format_path(p, quote_spaces=False):
423 """Format a path in utf8, quoting it if necessary."""
424 if '\n' in p:
425 import re
426 p = re.sub('\n', '\\n', p)
427 quote = True
428 else:
429 quote = p[0] == '"' or (quote_spaces and ' ' in p)
430 if quote:
431 extra = GIT_FAST_IMPORT_NEEDS_EXTRA_SPACE_AFTER_QUOTE and ' ' or ''
432 p = '"%s"%s' % (p, extra)
433 return p.encode('utf8')
436 def format_who_when(fields):
437 """Format a tuple of name,email,secs-since-epoch,utc-offset-secs as a string."""
438 offset = fields[3]
439 if offset < 0:
440 offset_sign = '-'
441 offset = abs(offset)
442 else:
443 offset_sign = '+'
444 offset_hours = offset / 3600
445 offset_minutes = offset / 60 - offset_hours * 60
446 offset_str = "%s%02d%02d" % (offset_sign, offset_hours, offset_minutes)
447 name = fields[0]
448 if name == '':
449 sep = ''
450 else:
451 sep = ' '
452 if isinstance(name, unicode):
453 name = name.encode('utf8')
454 email = fields[1]
455 if isinstance(email, unicode):
456 email = email.encode('utf8')
457 result = "%s%s<%s> %d %s" % (name, sep, email, fields[2], offset_str)
458 return result
461 def format_property(name, value):
462 """Format the name and value (both unicode) of a property as a string."""
463 utf8_name = name.encode('utf8')
464 if value is not None:
465 utf8_value = value.encode('utf8')
466 result = "property %s %d %s" % (utf8_name, len(utf8_value), utf8_value)
467 else:
468 result = "property %s" % (utf8_name,)
469 return result