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."""
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
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',
34 FILE_COMMAND_NAMES
= ['filemodify', 'filedelete', 'filecopy', 'filerename',
39 MULTIPLE_AUTHORS_FEATURE
= "multiple-authors"
40 COMMIT_PROPERTIES_FEATURE
= "commit-properties"
41 EMPTY_DIRS_FEATURE
= "empty-directories"
43 MULTIPLE_AUTHORS_FEATURE
,
44 COMMIT_PROPERTIES_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
):
64 # List of field names not to display
67 __repr__
= _simplerepr
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.
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
90 fields
= [k
for k
in self
.__dict
__.keys() if not k
.startswith('_')]
94 value
= self
.__dict
__.get(field
)
95 if field
in self
._binary
and value
is not None:
97 interesting
[field
] = value
99 return "%s: %s" % (self
.__class
__.__name
__, interesting
)
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
):
108 self
.location
= location
110 # Provide a unique id in case the mark is missing
112 self
.id = '%s@%d' % (os
.path
.basename(location
[0]), location
[1])
114 self
.id = ':%s' % mark
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
)
128 self
._binary
= ['data']
131 if self
.mark
is None:
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
):
141 ImportCommand
.__init
__(self
, '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
)
155 self
.committer
= committer
156 self
.message
= message
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:
168 mark_line
= "\nmark :%s" % self
.mark
169 if self
.author
is None:
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:
180 msg
= self
.message
.encode('utf8')
181 msg_section
= "\ndata %d\n%s" % (len(msg
), msg
)
182 if self
.from_
is None:
185 from_line
= "\nfrom %s" % self
.from_
186 if self
.merges
is None:
189 merge_lines
= "".join(["\nmerge %s" % (m
,)
190 for m
in self
.merges
])
191 if use_features
and self
.properties
:
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
)
198 properties_section
= ""
199 if self
.file_cmds
is None:
202 if include_file_contents
:
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:
218 child_names
= child_lists
[f
.name
]
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
231 self
.location
= location
234 if self
.value
is None:
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
248 return "progress %s" % (self
.message
,)
251 class ResetCommand(ImportCommand
):
253 def __init__(self
, ref
, from_
):
254 ImportCommand
.__init
__(self
, 'reset')
259 if self
.from_
is None:
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')
278 self
.message
= message
283 __repr__
= _detailrepr
286 if self
.from_
is None:
289 from_line
= "\nfrom %s" % self
.from_
290 if self
.tagger
is None:
293 tagger_line
= "\ntagger %s" % format_who_when(self
.tagger
)
294 if self
.message
is None:
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."""
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
)
314 self
.dataref
= dataref
316 self
._binary
= ['data']
321 __repr__
= _detailrepr
323 def format(self
, include_file_contents
=True):
325 if self
.dataref
is None:
327 if include_file_contents
:
328 datastr
= "\ndata %d\n%s" % (len(self
.data
), self
.data
)
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
)
360 __repr__
= _detailrepr
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
)
374 return "%s -> %s" % (self
.src_path
, self
.dest_path
)
376 __repr__
= _detailrepr
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
)
392 return "%s -> %s" % (self
.old_path
, self
.new_path
)
394 __repr__
= _detailrepr
398 format_path(self
.old_path
, quote_spaces
=True),
399 format_path(self
.new_path
))
402 class FileDeleteAllCommand(FileCommand
):
405 FileCommand
.__init
__(self
, 'filedeleteall')
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
)
422 def format_path(p
, quote_spaces
=False):
423 """Format a path in utf8, quoting it if necessary."""
426 p
= re
.sub('\n', '\\n', p
)
429 quote
= p
[0] == '"' or (quote_spaces
and ' ' in p
)
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."""
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
)
452 if isinstance(name
, unicode):
453 name
= name
.encode('utf8')
455 if isinstance(email
, unicode):
456 email
= email
.encode('utf8')
457 result
= "%s%s<%s> %d %s" % (name
, sep
, email
, fields
[2], offset_str
)
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
)
468 result
= "property %s" % (utf8_name
,)