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, see <http://www.gnu.org/licenses/>.
16
"""CommitHandlers that build and save revisions & their inventories."""
18
from __future__ import absolute_import
29
from ...trace import (
34
from fastimport import (
39
from .helpers import (
44
_serializer_handles_escaping = hasattr(serializer.Serializer,
45
'squashes_xml_invalid_characters')
48
def copy_inventory(inv):
49
entries = inv.iter_entries_by_dir()
50
inv = inventory.Inventory(None, inv.revision_id)
51
for path, inv_entry in entries:
52
inv.add(inv_entry.copy())
56
class GenericCommitHandler(processor.CommitHandler):
57
"""Base class for Bazaar CommitHandlers."""
59
def __init__(self, command, cache_mgr, rev_store, verbose=False,
60
prune_empty_dirs=True):
61
super(GenericCommitHandler, self).__init__(command)
62
self.cache_mgr = cache_mgr
63
self.rev_store = rev_store
64
self.verbose = verbose
65
self.branch_ref = command.ref
66
self.prune_empty_dirs = prune_empty_dirs
67
# This tracks path->file-id for things we're creating this commit.
68
# If the same path is created multiple times, we need to warn the
69
# user and add it just once.
70
# If a path is added then renamed or copied, we need to handle that.
71
self._new_file_ids = {}
72
# This tracks path->file-id for things we're modifying this commit.
73
# If a path is modified then renamed or copied, we need the make
74
# sure we grab the new content.
75
self._modified_file_ids = {}
76
# This tracks the paths for things we're deleting this commit.
77
# If the same path is added or the destination of a rename say,
78
# then a fresh file-id is required.
79
self._paths_deleted_this_commit = set()
81
def mutter(self, msg, *args):
82
"""Output a mutter but add context."""
83
msg = "%s (%s)" % (msg, self.command.id)
86
def debug(self, msg, *args):
87
"""Output a mutter if the appropriate -D option was given."""
88
if "fast-import" in debug.debug_flags:
89
msg = "%s (%s)" % (msg, self.command.id)
92
def note(self, msg, *args):
93
"""Output a note but add context."""
94
msg = "%s (%s)" % (msg, self.command.id)
97
def warning(self, msg, *args):
98
"""Output a warning but add context."""
99
msg = "%s (%s)" % (msg, self.command.id)
102
def pre_process_files(self):
103
"""Prepare for committing."""
104
self.revision_id = self.gen_revision_id()
105
# cache of texts for this commit, indexed by file-id
106
self.data_for_commit = {}
107
#if self.rev_store.expects_rich_root():
108
self.data_for_commit[inventory.ROOT_ID] = []
110
# Track the heads and get the real parent list
111
parents = self.cache_mgr.reftracker.track_heads(self.command)
113
# Convert the parent commit-ids to bzr revision-ids
115
self.parents = [self.cache_mgr.lookup_committish(p)
119
self.debug("%s id: %s, parents: %s", self.command.id,
120
self.revision_id, str(self.parents))
122
# Tell the RevisionStore we're starting a new commit
123
self.revision = self.build_revision()
124
self.parent_invs = [self.get_inventory(p) for p in self.parents]
125
self.rev_store.start_new_revision(self.revision, self.parents,
128
# cache of per-file parents for this commit, indexed by file-id
129
self.per_file_parents_for_commit = {}
130
if self.rev_store.expects_rich_root():
131
self.per_file_parents_for_commit[inventory.ROOT_ID] = ()
133
# Keep the basis inventory. This needs to be treated as read-only.
134
if len(self.parents) == 0:
135
self.basis_inventory = self._init_inventory()
137
self.basis_inventory = self.get_inventory(self.parents[0])
138
if hasattr(self.basis_inventory, "root_id"):
139
self.inventory_root_id = self.basis_inventory.root_id
141
self.inventory_root_id = self.basis_inventory.root.file_id
143
# directory-path -> inventory-entry for current inventory
144
self.directory_entries = {}
146
def _init_inventory(self):
147
return self.rev_store.init_inventory(self.revision_id)
149
def get_inventory(self, revision_id):
150
"""Get the inventory for a revision id."""
152
inv = self.cache_mgr.inventories[revision_id]
155
self.mutter("get_inventory cache miss for %s", revision_id)
156
# Not cached so reconstruct from the RevisionStore
157
inv = self.rev_store.get_inventory(revision_id)
158
self.cache_mgr.inventories[revision_id] = inv
161
def _get_data(self, file_id):
162
"""Get the data bytes for a file-id."""
163
return self.data_for_commit[file_id]
165
def _get_lines(self, file_id):
166
"""Get the lines for a file-id."""
167
return osutils.split_lines(self._get_data(file_id))
169
def _get_per_file_parents(self, file_id):
170
"""Get the lines for a file-id."""
171
return self.per_file_parents_for_commit[file_id]
173
def _get_inventories(self, revision_ids):
174
"""Get the inventories for revision-ids.
176
This is a callback used by the RepositoryStore to
177
speed up inventory reconstruction.
181
# If an inventory is in the cache, we assume it was
182
# successfully loaded into the revision store
183
for revision_id in revision_ids:
185
inv = self.cache_mgr.inventories[revision_id]
186
present.append(revision_id)
189
self.note("get_inventories cache miss for %s", revision_id)
190
# Not cached so reconstruct from the revision store
192
inv = self.get_inventory(revision_id)
193
present.append(revision_id)
195
inv = self._init_inventory()
196
self.cache_mgr.inventories[revision_id] = inv
197
inventories.append(inv)
198
return present, inventories
200
def bzr_file_id_and_new(self, path):
201
"""Get a Bazaar file identifier and new flag for a path.
203
:return: file_id, is_new where
204
is_new = True if the file_id is newly created
206
if path not in self._paths_deleted_this_commit:
207
# Try file-ids renamed in this commit
208
id = self._modified_file_ids.get(path)
212
# Try the basis inventory
213
id = self.basis_inventory.path2id(path)
217
# Try the other inventories
218
if len(self.parents) > 1:
219
for inv in self.parent_invs[1:]:
220
id = self.basis_inventory.path2id(path)
224
# Doesn't exist yet so create it
225
dirname, basename = osutils.split(path)
226
id = generate_ids.gen_file_id(basename)
227
self.debug("Generated new file id %s for '%s' in revision-id '%s'",
228
id, path, self.revision_id)
229
self._new_file_ids[path] = id
232
def bzr_file_id(self, path):
233
"""Get a Bazaar file identifier for a path."""
234
return self.bzr_file_id_and_new(path)[0]
236
def _utf8_decode(self, field, value):
238
return value.decode('utf-8')
239
except UnicodeDecodeError:
240
# The spec says fields are *typically* utf8 encoded
241
# but that isn't enforced by git-fast-export (at least)
242
self.warning("%s not in utf8 - replacing unknown "
243
"characters" % (field,))
244
return value.decode('utf-8', 'replace')
246
def _decode_path(self, path):
248
return path.decode('utf-8')
249
except UnicodeDecodeError:
250
# The spec says fields are *typically* utf8 encoded
251
# but that isn't enforced by git-fast-export (at least)
252
self.warning("path %r not in utf8 - replacing unknown "
253
"characters" % (path,))
254
return path.decode('utf-8', 'replace')
256
def _format_name_email(self, section, name, email):
257
"""Format name & email as a string."""
258
name = self._utf8_decode("%s name" % section, name)
259
email = self._utf8_decode("%s email" % section, email)
262
return "%s <%s>" % (name, email)
266
def gen_revision_id(self):
267
"""Generate a revision id.
269
Subclasses may override this to produce deterministic ids say.
271
committer = self.command.committer
272
# Perhaps 'who' being the person running the import is ok? If so,
273
# it might be a bit quicker and give slightly better compression?
274
who = self._format_name_email("committer", committer[0], committer[1])
275
timestamp = committer[2]
276
return generate_ids.gen_revision_id(who, timestamp)
278
def build_revision(self):
279
rev_props = self._legal_revision_properties(self.command.properties)
280
if 'branch-nick' not in rev_props:
281
rev_props['branch-nick'] = self.cache_mgr.branch_mapper.git_to_bzr(
283
self._save_author_info(rev_props)
284
committer = self.command.committer
285
who = self._format_name_email("committer", committer[0], committer[1])
287
message = self.command.message.decode("utf-8")
289
except UnicodeDecodeError:
291
"commit message not in utf8 - replacing unknown characters")
292
message = self.command.message.decode('utf-8', 'replace')
293
if not _serializer_handles_escaping:
294
# We need to assume the bad ol' days
295
message = helpers.escape_commit_message(message)
296
return revision.Revision(
297
timestamp=committer[2],
298
timezone=committer[3],
301
revision_id=self.revision_id,
302
properties=rev_props,
303
parent_ids=self.parents)
305
def _legal_revision_properties(self, props):
306
"""Clean-up any revision properties we can't handle."""
307
# For now, we just check for None because that's not allowed in 2.0rc1
309
if props is not None:
310
for name, value in props.items():
313
"converting None to empty string for property %s"
320
def _save_author_info(self, rev_props):
321
author = self.command.author
324
if self.command.more_authors:
325
authors = [author] + self.command.more_authors
326
author_ids = [self._format_name_email("author", a[0], a[1]) for a in authors]
327
elif author != self.command.committer:
328
author_ids = [self._format_name_email("author", author[0], author[1])]
331
# If we reach here, there are authors worth storing
332
rev_props['authors'] = "\n".join(author_ids)
334
def _modify_item(self, path, kind, is_executable, data, inv):
335
"""Add to or change an item in the inventory."""
336
# If we've already added this, warn the user that we're ignoring it.
337
# In the future, it might be nice to double check that the new data
338
# is the same as the old but, frankly, exporters should be fixed
339
# not to produce bad data streams in the first place ...
340
existing = self._new_file_ids.get(path)
342
# We don't warn about directories because it's fine for them
343
# to be created already by a previous rename
344
if kind != 'directory':
345
self.warning("%s already added in this commit - ignoring" %
349
# Create the new InventoryEntry
350
basename, parent_id = self._ensure_directory(path, inv)
351
file_id = self.bzr_file_id(path)
352
ie = inventory.make_entry(kind, basename, parent_id, file_id)
353
ie.revision = self.revision_id
355
ie.executable = is_executable
356
# lines = osutils.split_lines(data)
357
ie.text_sha1 = osutils.sha_string(data)
358
ie.text_size = len(data)
359
self.data_for_commit[file_id] = data
360
elif kind == 'directory':
361
self.directory_entries[path] = ie
362
# There are no lines stored for a directory so
363
# make sure the cache used by get_lines knows that
364
self.data_for_commit[file_id] = ''
365
elif kind == 'symlink':
366
ie.symlink_target = self._decode_path(data)
367
# There are no lines stored for a symlink so
368
# make sure the cache used by get_lines knows that
369
self.data_for_commit[file_id] = ''
371
self.warning("Cannot import items of kind '%s' yet - ignoring '%s'"
375
if inv.has_id(file_id):
376
old_ie = inv[file_id]
377
if old_ie.kind == 'directory':
378
self.record_delete(path, old_ie)
379
self.record_changed(path, ie, parent_id)
382
self.record_new(path, ie)
384
print "failed to add path '%s' with entry '%s' in command %s" \
385
% (path, ie, self.command.id)
386
print "parent's children are:\n%r\n" % (ie.parent_id.children,)
389
def _ensure_directory(self, path, inv):
390
"""Ensure that the containing directory exists for 'path'"""
391
dirname, basename = osutils.split(path)
393
# the root node doesn't get updated
394
return basename, self.inventory_root_id
396
ie = self._get_directory_entry(inv, dirname)
398
# We will create this entry, since it doesn't exist
401
return basename, ie.file_id
403
# No directory existed, we will just create one, first, make sure
405
dir_basename, parent_id = self._ensure_directory(dirname, inv)
406
dir_file_id = self.bzr_file_id(dirname)
407
ie = inventory.entry_factory['directory'](dir_file_id,
408
dir_basename, parent_id)
409
ie.revision = self.revision_id
410
self.directory_entries[dirname] = ie
411
# There are no lines stored for a directory so
412
# make sure the cache used by get_lines knows that
413
self.data_for_commit[dir_file_id] = ''
415
# It's possible that a file or symlink with that file-id
416
# already exists. If it does, we need to delete it.
417
if inv.has_id(dir_file_id):
418
self.record_delete(dirname, ie)
419
self.record_new(dirname, ie)
420
return basename, ie.file_id
422
def _get_directory_entry(self, inv, dirname):
423
"""Get the inventory entry for a directory.
425
Raises KeyError if dirname is not a directory in inv.
427
result = self.directory_entries.get(dirname)
429
if dirname in self._paths_deleted_this_commit:
432
file_id = inv.path2id(dirname)
433
except errors.NoSuchId:
434
# In a CHKInventory, this is raised if there's no root yet
438
result = inv[file_id]
439
# dirname must be a directory for us to return it
440
if result.kind == 'directory':
441
self.directory_entries[dirname] = result
446
def _delete_item(self, path, inv):
447
newly_added = self._new_file_ids.get(path)
449
# We've only just added this path earlier in this commit.
450
file_id = newly_added
451
# note: delta entries look like (old, new, file-id, ie)
452
ie = self._delta_entries_by_fileid[file_id][3]
454
file_id = inv.path2id(path)
456
self.mutter("ignoring delete of %s as not in inventory", path)
460
except errors.NoSuchId:
461
self.mutter("ignoring delete of %s as not in inventory", path)
463
self.record_delete(path, ie)
465
def _copy_item(self, src_path, dest_path, inv):
466
newly_changed = self._new_file_ids.get(src_path) or \
467
self._modified_file_ids.get(src_path)
469
# We've only just added/changed this path earlier in this commit.
470
file_id = newly_changed
471
# note: delta entries look like (old, new, file-id, ie)
472
ie = self._delta_entries_by_fileid[file_id][3]
474
file_id = inv.path2id(src_path)
476
self.warning("ignoring copy of %s to %s - source does not exist",
483
content = self.data_for_commit[file_id]
485
content = self.rev_store.get_file_text(self.parents[0], file_id)
486
self._modify_item(dest_path, kind, ie.executable, content, inv)
487
elif kind == 'symlink':
488
self._modify_item(dest_path, kind, False,
489
ie.symlink_target.encode("utf-8"), inv)
491
self.warning("ignoring copy of %s %s - feature not yet supported",
494
def _rename_item(self, old_path, new_path, inv):
495
existing = self._new_file_ids.get(old_path) or \
496
self._modified_file_ids.get(old_path)
498
# We've only just added/modified this path earlier in this commit.
499
# Change the add/modify of old_path to an add of new_path
500
self._rename_pending_change(old_path, new_path, existing)
503
file_id = inv.path2id(old_path)
506
"ignoring rename of %s to %s - old path does not exist" %
507
(old_path, new_path))
511
new_file_id = inv.path2id(new_path)
512
if new_file_id is not None:
513
self.record_delete(new_path, inv[new_file_id])
514
self.record_rename(old_path, new_path, file_id, ie)
516
# The revision-id for this entry will be/has been updated and
517
# that means the loader then needs to know what the "new" text is.
518
# We therefore must go back to the revision store to get it.
519
lines = self.rev_store.get_file_lines(rev_id, file_id)
520
self.data_for_commit[file_id] = ''.join(lines)
522
def _delete_all_items(self, inv):
525
for path, ie in inv.iter_entries_by_dir():
527
self.record_delete(path, ie)
529
def _warn_unless_in_merges(self, fileid, path):
530
if len(self.parents) <= 1:
532
for parent in self.parents[1:]:
533
if fileid in self.get_inventory(parent):
535
self.warning("ignoring delete of %s as not in parent inventories", path)
538
class InventoryCommitHandler(GenericCommitHandler):
539
"""A CommitHandler that builds and saves Inventory objects."""
541
def pre_process_files(self):
542
super(InventoryCommitHandler, self).pre_process_files()
544
# Seed the inventory from the previous one. Note that
545
# the parent class version of pre_process_files() has
546
# already set the right basis_inventory for this branch
547
# but we need to copy it in order to mutate it safely
548
# without corrupting the cached inventory value.
549
if len(self.parents) == 0:
550
self.inventory = self.basis_inventory
552
self.inventory = copy_inventory(self.basis_inventory)
553
self.inventory_root = self.inventory.root
555
# directory-path -> inventory-entry for current inventory
556
self.directory_entries = dict(self.inventory.directories())
558
# Initialise the inventory revision info as required
559
if self.rev_store.expects_rich_root():
560
self.inventory.revision_id = self.revision_id
562
# In this revision store, root entries have no knit or weave.
563
# When serializing out to disk and back in, root.revision is
564
# always the new revision_id.
565
self.inventory.root.revision = self.revision_id
567
def post_process_files(self):
568
"""Save the revision."""
569
self.cache_mgr.inventories[self.revision_id] = self.inventory
570
self.rev_store.load(self.revision, self.inventory, None,
571
lambda file_id: self._get_data(file_id),
572
lambda file_id: self._get_per_file_parents(file_id),
573
lambda revision_ids: self._get_inventories(revision_ids))
575
def record_new(self, path, ie):
577
# If this is a merge, the file was most likely added already.
578
# The per-file parent(s) must therefore be calculated and
579
# we can't assume there are none.
580
per_file_parents, ie.revision = \
581
self.rev_store.get_parents_and_revision_for_entry(ie)
582
self.per_file_parents_for_commit[ie.file_id] = per_file_parents
583
self.inventory.add(ie)
584
except errors.DuplicateFileId:
585
# Directory already exists as a file or symlink
586
del self.inventory[ie.file_id]
588
self.inventory.add(ie)
590
def record_changed(self, path, ie, parent_id):
591
# HACK: no API for this (del+add does more than it needs to)
592
per_file_parents, ie.revision = \
593
self.rev_store.get_parents_and_revision_for_entry(ie)
594
self.per_file_parents_for_commit[ie.file_id] = per_file_parents
595
self.inventory._byid[ie.file_id] = ie
596
parent_ie = self.inventory._byid[parent_id]
597
parent_ie.children[ie.name] = ie
599
def record_delete(self, path, ie):
600
self.inventory.remove_recursive_id(ie.file_id)
602
def record_rename(self, old_path, new_path, file_id, ie):
603
# For a rename, the revision-id is always the new one so
604
# no need to change/set it here
605
ie.revision = self.revision_id
606
per_file_parents, _ = \
607
self.rev_store.get_parents_and_revision_for_entry(ie)
608
self.per_file_parents_for_commit[file_id] = per_file_parents
609
new_basename, new_parent_id = self._ensure_directory(new_path,
611
self.inventory.rename(file_id, new_parent_id, new_basename)
613
def modify_handler(self, filecmd):
614
if filecmd.dataref is not None:
615
data = self.cache_mgr.fetch_blob(filecmd.dataref)
618
self.debug("modifying %s", filecmd.path)
619
(kind, is_executable) = mode_to_kind(filecmd.mode)
620
self._modify_item(self._decode_path(filecmd.path), kind,
621
is_executable, data, self.inventory)
623
def delete_handler(self, filecmd):
624
self.debug("deleting %s", filecmd.path)
625
self._delete_item(self._decode_path(filecmd.path), self.inventory)
627
def copy_handler(self, filecmd):
628
src_path = self._decode_path(filecmd.src_path)
629
dest_path = self._decode_path(filecmd.dest_path)
630
self.debug("copying %s to %s", src_path, dest_path)
631
self._copy_item(src_path, dest_path, self.inventory)
633
def rename_handler(self, filecmd):
634
old_path = self._decode_path(filecmd.old_path)
635
new_path = self._decode_path(filecmd.new_path)
636
self.debug("renaming %s to %s", old_path, new_path)
637
self._rename_item(old_path, new_path, self.inventory)
639
def deleteall_handler(self, filecmd):
640
self.debug("deleting all files (and also all directories)")
641
self._delete_all_items(self.inventory)
644
class InventoryDeltaCommitHandler(GenericCommitHandler):
645
"""A CommitHandler that builds Inventories by applying a delta."""
647
def pre_process_files(self):
648
super(InventoryDeltaCommitHandler, self).pre_process_files()
649
self._dirs_that_might_become_empty = set()
651
# A given file-id can only appear once so we accumulate
652
# the entries in a dict then build the actual delta at the end
653
self._delta_entries_by_fileid = {}
654
if len(self.parents) == 0 or not self.rev_store.expects_rich_root():
659
# Need to explicitly add the root entry for the first revision
660
# and for non rich-root inventories
661
root_id = inventory.ROOT_ID
662
root_ie = inventory.InventoryDirectory(root_id, u'', None)
663
root_ie.revision = self.revision_id
664
self._add_entry((old_path, '', root_id, root_ie))
666
def post_process_files(self):
667
"""Save the revision."""
668
delta = self._get_final_delta()
669
inv = self.rev_store.load_using_delta(self.revision,
670
self.basis_inventory, delta, None,
672
self._get_per_file_parents,
673
self._get_inventories)
674
self.cache_mgr.inventories[self.revision_id] = inv
675
#print "committed %s" % self.revision_id
677
def _get_final_delta(self):
678
"""Generate the final delta.
680
Smart post-processing of changes, e.g. pruning of directories
681
that would become empty, goes here.
683
delta = list(self._delta_entries_by_fileid.values())
684
if self.prune_empty_dirs and self._dirs_that_might_become_empty:
685
candidates = self._dirs_that_might_become_empty
688
parent_dirs_that_might_become_empty = set()
689
for path, file_id in self._empty_after_delta(delta, candidates):
690
newly_added = self._new_file_ids.get(path)
692
never_born.add(newly_added)
694
delta.append((path, None, file_id, None))
695
parent_dir = osutils.dirname(path)
697
parent_dirs_that_might_become_empty.add(parent_dir)
698
candidates = parent_dirs_that_might_become_empty
699
# Clean up entries that got deleted before they were ever added
701
delta = [de for de in delta if de[2] not in never_born]
704
def _empty_after_delta(self, delta, candidates):
705
#self.mutter("delta so far is:\n%s" % "\n".join([str(de) for de in delta]))
706
#self.mutter("candidates for deletion are:\n%s" % "\n".join([c for c in candidates]))
707
new_inv = self._get_proposed_inventory(delta)
709
for dir in candidates:
710
file_id = new_inv.path2id(dir)
713
ie = new_inv[file_id]
714
if ie.kind != 'directory':
716
if len(ie.children) == 0:
717
result.append((dir, file_id))
719
self.note("pruning empty directory %s" % (dir,))
722
def _get_proposed_inventory(self, delta):
723
if len(self.parents):
724
# new_inv = self.basis_inventory._get_mutable_inventory()
725
# Note that this will create unreferenced chk pages if we end up
726
# deleting entries, because this 'test' inventory won't end up
727
# used. However, it is cheaper than having to create a full copy of
728
# the inventory for every commit.
729
new_inv = self.basis_inventory.create_by_apply_delta(delta,
730
'not-a-valid-revision-id:')
732
new_inv = inventory.Inventory(revision_id=self.revision_id)
733
# This is set in the delta so remove it to prevent a duplicate
734
del new_inv[inventory.ROOT_ID]
736
new_inv.apply_delta(delta)
737
except errors.InconsistentDelta:
738
self.mutter("INCONSISTENT DELTA IS:\n%s" % "\n".join([str(de) for de in delta]))
742
def _add_entry(self, entry):
743
# We need to combine the data if multiple entries have the same file-id.
744
# For example, a rename followed by a modification looks like:
746
# (x, y, f, e) & (y, y, f, g) => (x, y, f, g)
748
# Likewise, a modification followed by a rename looks like:
750
# (x, x, f, e) & (x, y, f, g) => (x, y, f, g)
752
# Here's a rename followed by a delete and a modification followed by
755
# (x, y, f, e) & (y, None, f, None) => (x, None, f, None)
756
# (x, x, f, e) & (x, None, f, None) => (x, None, f, None)
758
# In summary, we use the original old-path, new new-path and new ie
759
# when combining entries.
764
existing = self._delta_entries_by_fileid.get(file_id, None)
765
if existing is not None:
766
old_path = existing[0]
767
entry = (old_path, new_path, file_id, ie)
768
if new_path is None and old_path is None:
769
# This is a delete cancelling a previous add
770
del self._delta_entries_by_fileid[file_id]
771
parent_dir = osutils.dirname(existing[1])
772
self.mutter("cancelling add of %s with parent %s" % (existing[1], parent_dir))
774
self._dirs_that_might_become_empty.add(parent_dir)
777
self._delta_entries_by_fileid[file_id] = entry
779
# Collect parent directories that might become empty
782
parent_dir = osutils.dirname(old_path)
783
# note: no need to check the root
785
self._dirs_that_might_become_empty.add(parent_dir)
786
elif old_path is not None and old_path != new_path:
788
old_parent_dir = osutils.dirname(old_path)
789
new_parent_dir = osutils.dirname(new_path)
790
if old_parent_dir and old_parent_dir != new_parent_dir:
791
self._dirs_that_might_become_empty.add(old_parent_dir)
793
# Calculate the per-file parents, if not already done
794
if file_id in self.per_file_parents_for_commit:
798
# If this is a merge, the file was most likely added already.
799
# The per-file parent(s) must therefore be calculated and
800
# we can't assume there are none.
801
per_file_parents, ie.revision = \
802
self.rev_store.get_parents_and_revision_for_entry(ie)
803
self.per_file_parents_for_commit[file_id] = per_file_parents
804
elif new_path is None:
807
elif old_path != new_path:
809
per_file_parents, _ = \
810
self.rev_store.get_parents_and_revision_for_entry(ie)
811
self.per_file_parents_for_commit[file_id] = per_file_parents
814
per_file_parents, ie.revision = \
815
self.rev_store.get_parents_and_revision_for_entry(ie)
816
self.per_file_parents_for_commit[file_id] = per_file_parents
818
def record_new(self, path, ie):
819
self._add_entry((None, path, ie.file_id, ie))
821
def record_changed(self, path, ie, parent_id=None):
822
self._add_entry((path, path, ie.file_id, ie))
823
self._modified_file_ids[path] = ie.file_id
825
def record_delete(self, path, ie):
826
self._add_entry((path, None, ie.file_id, None))
827
self._paths_deleted_this_commit.add(path)
828
if ie.kind == 'directory':
830
del self.directory_entries[path]
833
for child_relpath, entry in \
834
self.basis_inventory.iter_entries_by_dir(from_dir=ie):
835
child_path = osutils.pathjoin(path, child_relpath)
836
self._add_entry((child_path, None, entry.file_id, None))
837
self._paths_deleted_this_commit.add(child_path)
838
if entry.kind == 'directory':
840
del self.directory_entries[child_path]
844
def record_rename(self, old_path, new_path, file_id, old_ie):
845
new_ie = old_ie.copy()
846
new_basename, new_parent_id = self._ensure_directory(new_path,
847
self.basis_inventory)
848
new_ie.name = new_basename
849
new_ie.parent_id = new_parent_id
850
new_ie.revision = self.revision_id
851
self._add_entry((old_path, new_path, file_id, new_ie))
852
self._modified_file_ids[new_path] = file_id
853
self._paths_deleted_this_commit.discard(new_path)
854
if new_ie.kind == 'directory':
855
self.directory_entries[new_path] = new_ie
857
def _rename_pending_change(self, old_path, new_path, file_id):
858
"""Instead of adding/modifying old-path, add new-path instead."""
859
# note: delta entries look like (old, new, file-id, ie)
860
old_ie = self._delta_entries_by_fileid[file_id][3]
862
# Delete the old path. Note that this might trigger implicit
863
# deletion of newly created parents that could now become empty.
864
self.record_delete(old_path, old_ie)
866
# Update the dictionaries used for tracking new file-ids
867
if old_path in self._new_file_ids:
868
del self._new_file_ids[old_path]
870
del self._modified_file_ids[old_path]
871
self._new_file_ids[new_path] = file_id
873
# Create the new InventoryEntry
875
basename, parent_id = self._ensure_directory(new_path,
876
self.basis_inventory)
877
ie = inventory.make_entry(kind, basename, parent_id, file_id)
878
ie.revision = self.revision_id
880
ie.executable = old_ie.executable
881
ie.text_sha1 = old_ie.text_sha1
882
ie.text_size = old_ie.text_size
883
elif kind == 'symlink':
884
ie.symlink_target = old_ie.symlink_target
887
self.record_new(new_path, ie)
889
def modify_handler(self, filecmd):
890
(kind, executable) = mode_to_kind(filecmd.mode)
891
if filecmd.dataref is not None:
892
if kind == "directory":
894
elif kind == "tree-reference":
895
data = filecmd.dataref
897
data = self.cache_mgr.fetch_blob(filecmd.dataref)
900
self.debug("modifying %s", filecmd.path)
901
decoded_path = self._decode_path(filecmd.path)
902
self._modify_item(decoded_path, kind,
903
executable, data, self.basis_inventory)
905
def delete_handler(self, filecmd):
906
self.debug("deleting %s", filecmd.path)
908
self._decode_path(filecmd.path), self.basis_inventory)
910
def copy_handler(self, filecmd):
911
src_path = self._decode_path(filecmd.src_path)
912
dest_path = self._decode_path(filecmd.dest_path)
913
self.debug("copying %s to %s", src_path, dest_path)
914
self._copy_item(src_path, dest_path, self.basis_inventory)
916
def rename_handler(self, filecmd):
917
old_path = self._decode_path(filecmd.old_path)
918
new_path = self._decode_path(filecmd.new_path)
919
self.debug("renaming %s to %s", old_path, new_path)
920
self._rename_item(old_path, new_path, self.basis_inventory)
922
def deleteall_handler(self, filecmd):
923
self.debug("deleting all files (and also all directories)")
924
self._delete_all_items(self.basis_inventory)