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
"""CommitHandlers that build and save revisions & their inventories."""
28
from bzrlib.plugins.fastimport import commands, helpers, processor
31
_serializer_handles_escaping = hasattr(serializer.Serializer,
32
'squashes_xml_invalid_characters')
35
def copy_inventory(inv):
36
# This currently breaks revision-id matching
37
#if hasattr(inv, "_get_mutable_inventory"):
38
# # TODO: Make this a public API on inventory
39
# return inv._get_mutable_inventory()
41
# TODO: Shallow copy - deep inventory copying is expensive
45
class GenericCommitHandler(processor.CommitHandler):
46
"""Base class for Bazaar CommitHandlers."""
48
def __init__(self, command, cache_mgr, rev_store, verbose=False,
49
prune_empty_dirs=True):
50
super(GenericCommitHandler, self).__init__(command)
51
self.cache_mgr = cache_mgr
52
self.rev_store = rev_store
53
self.verbose = verbose
54
self.branch_ref = command.ref
55
self.prune_empty_dirs = prune_empty_dirs
56
# This tracks path->file-id for things we're creating this commit.
57
# If the same path is created multiple times, we need to warn the
58
# user and add it just once.
59
# If a path is added then renamed or copied, we need to handle that.
60
self._new_file_ids = {}
61
# This tracks path->file-id for things we're modifying this commit.
62
# If a path is modified then renamed or copied, we need the make
63
# sure we grab the new content.
64
self._modified_file_ids = {}
65
# This tracks the paths for things we're deleting this commit.
66
# If the same path is added or the destination of a rename say,
67
# then a fresh file-id is required.
68
self._paths_deleted_this_commit = set()
70
def pre_process_files(self):
71
"""Prepare for committing."""
72
self.revision_id = self.gen_revision_id()
73
# cache of texts for this commit, indexed by file-id
74
self.lines_for_commit = {}
75
#if self.rev_store.expects_rich_root():
76
self.lines_for_commit[inventory.ROOT_ID] = []
78
# Track the heads and get the real parent list
79
parents = self.cache_mgr.track_heads(self.command)
81
# Convert the parent commit-ids to bzr revision-ids
83
self.parents = [self.cache_mgr.revision_ids[p]
87
self.debug("%s id: %s, parents: %s", self.command.id,
88
self.revision_id, str(self.parents))
90
# Tell the RevisionStore we're starting a new commit
91
self.revision = self.build_revision()
92
self.parent_invs = [self.get_inventory(p) for p in self.parents]
93
self.rev_store.start_new_revision(self.revision, self.parents,
96
# cache of per-file parents for this commit, indexed by file-id
97
self.per_file_parents_for_commit = {}
98
if self.rev_store.expects_rich_root():
99
self.per_file_parents_for_commit[inventory.ROOT_ID] = ()
101
# Keep the basis inventory. This needs to be treated as read-only.
102
if len(self.parents) == 0:
103
self.basis_inventory = self._init_inventory()
105
self.basis_inventory = self.get_inventory(self.parents[0])
106
if hasattr(self.basis_inventory, "root_id"):
107
self.inventory_root_id = self.basis_inventory.root_id
109
self.inventory_root_id = self.basis_inventory.root.file_id
111
# directory-path -> inventory-entry for current inventory
112
self.directory_entries = {}
114
def _init_inventory(self):
115
return self.rev_store.init_inventory(self.revision_id)
117
def get_inventory(self, revision_id):
118
"""Get the inventory for a revision id."""
120
inv = self.cache_mgr.inventories[revision_id]
123
self.mutter("get_inventory cache miss for %s", revision_id)
124
# Not cached so reconstruct from the RevisionStore
125
inv = self.rev_store.get_inventory(revision_id)
126
self.cache_mgr.inventories[revision_id] = inv
129
def _get_lines(self, file_id):
130
"""Get the lines for a file-id."""
131
return self.lines_for_commit[file_id]
133
def _get_per_file_parents(self, file_id):
134
"""Get the lines for a file-id."""
135
return self.per_file_parents_for_commit[file_id]
137
def _get_inventories(self, revision_ids):
138
"""Get the inventories for revision-ids.
140
This is a callback used by the RepositoryStore to
141
speed up inventory reconstruction.
145
# If an inventory is in the cache, we assume it was
146
# successfully loaded into the revision store
147
for revision_id in revision_ids:
149
inv = self.cache_mgr.inventories[revision_id]
150
present.append(revision_id)
153
self.note("get_inventories cache miss for %s", revision_id)
154
# Not cached so reconstruct from the revision store
156
inv = self.get_inventory(revision_id)
157
present.append(revision_id)
159
inv = self._init_inventory()
160
self.cache_mgr.inventories[revision_id] = inv
161
inventories.append(inv)
162
return present, inventories
164
def bzr_file_id_and_new(self, path):
165
"""Get a Bazaar file identifier and new flag for a path.
167
:return: file_id, is_new where
168
is_new = True if the file_id is newly created
170
if path not in self._paths_deleted_this_commit:
171
# Try file-ids renamed in this commit
172
id = self._modified_file_ids.get(path)
176
# Try the basis inventory
177
id = self.basis_inventory.path2id(path)
181
# Try the other inventories
182
if len(self.parents) > 1:
183
for inv in self.parent_invs[1:]:
184
id = self.basis_inventory.path2id(path)
188
# Doesn't exist yet so create it
189
id = generate_ids.gen_file_id(path)
190
self.debug("Generated new file id %s for '%s' in revision-id '%s'",
191
id, path, self.revision_id)
192
self._new_file_ids[path] = id
195
def bzr_file_id(self, path):
196
"""Get a Bazaar file identifier for a path."""
197
return self.bzr_file_id_and_new(path)[0]
199
def _format_name_email(self, name, email):
200
"""Format name & email as a string."""
202
return "%s <%s>" % (name, email)
206
def gen_revision_id(self):
207
"""Generate a revision id.
209
Subclasses may override this to produce deterministic ids say.
211
committer = self.command.committer
212
# Perhaps 'who' being the person running the import is ok? If so,
213
# it might be a bit quicker and give slightly better compression?
214
who = self._format_name_email(committer[0], committer[1])
215
timestamp = committer[2]
216
return generate_ids.gen_revision_id(who, timestamp)
218
def build_revision(self):
219
rev_props = self._legal_revision_properties(self.command.properties)
220
self._save_author_info(rev_props)
221
committer = self.command.committer
222
who = self._format_name_email(committer[0], committer[1])
223
message = self.command.message
224
if not _serializer_handles_escaping:
225
# We need to assume the bad ol' days
226
message = helpers.escape_commit_message(message)
227
return revision.Revision(
228
timestamp=committer[2],
229
timezone=committer[3],
232
revision_id=self.revision_id,
233
properties=rev_props,
234
parent_ids=self.parents)
236
def _legal_revision_properties(self, props):
237
"""Clean-up any revision properties we can't handle."""
238
# For now, we just check for None because that's not allowed in 2.0rc1
240
if props is not None:
241
for name, value in props.items():
244
"converting None to empty string for property %s"
251
def _save_author_info(self, rev_props):
252
author = self.command.author
255
if self.command.more_authors:
256
authors = [author] + self.command.more_authors
257
author_ids = [self._format_name_email(a[0], a[1]) for a in authors]
258
elif author != self.command.committer:
259
author_ids = [self._format_name_email(author[0], author[1])]
262
# If we reach here, there are authors worth storing
263
rev_props['authors'] = "\n".join(author_ids)
265
def _modify_item(self, path, kind, is_executable, data, inv):
266
"""Add to or change an item in the inventory."""
267
# If we've already added this, warn the user that we're ignoring it.
268
# In the future, it might be nice to double check that the new data
269
# is the same as the old but, frankly, exporters should be fixed
270
# not to produce bad data streams in the first place ...
271
existing = self._new_file_ids.get(path)
273
# We don't warn about directories because it's fine for them
274
# to be created already by a previous rename
275
if kind != 'directory':
276
self.warning("%s already added in this commit - ignoring" %
280
# Create the new InventoryEntry
281
basename, parent_id = self._ensure_directory(path, inv)
282
file_id = self.bzr_file_id(path)
283
ie = inventory.make_entry(kind, basename, parent_id, file_id)
284
ie.revision = self.revision_id
286
ie.executable = is_executable
287
lines = osutils.split_lines(data)
288
ie.text_sha1 = osutils.sha_strings(lines)
289
ie.text_size = sum(map(len, lines))
290
self.lines_for_commit[file_id] = lines
291
elif kind == 'directory':
292
self.directory_entries[path] = ie
293
# There are no lines stored for a directory so
294
# make sure the cache used by get_lines knows that
295
self.lines_for_commit[file_id] = []
296
elif kind == 'symlink':
297
ie.symlink_target = data.encode('utf8')
298
# There are no lines stored for a symlink so
299
# make sure the cache used by get_lines knows that
300
self.lines_for_commit[file_id] = []
302
self.warning("Cannot import items of kind '%s' yet - ignoring '%s'"
307
old_ie = inv[file_id]
308
if old_ie.kind == 'directory':
309
self.record_delete(path, old_ie)
310
self.record_changed(path, ie, parent_id)
313
self.record_new(path, ie)
315
print "failed to add path '%s' with entry '%s' in command %s" \
316
% (path, ie, self.command.id)
317
print "parent's children are:\n%r\n" % (ie.parent_id.children,)
320
def _ensure_directory(self, path, inv):
321
"""Ensure that the containing directory exists for 'path'"""
322
dirname, basename = osutils.split(path)
324
# the root node doesn't get updated
325
return basename, self.inventory_root_id
327
ie = self._get_directory_entry(inv, dirname)
329
# We will create this entry, since it doesn't exist
332
return basename, ie.file_id
334
# No directory existed, we will just create one, first, make sure
336
dir_basename, parent_id = self._ensure_directory(dirname, inv)
337
dir_file_id = self.bzr_file_id(dirname)
338
ie = inventory.entry_factory['directory'](dir_file_id,
339
dir_basename, parent_id)
340
ie.revision = self.revision_id
341
self.directory_entries[dirname] = ie
342
# There are no lines stored for a directory so
343
# make sure the cache used by get_lines knows that
344
self.lines_for_commit[dir_file_id] = []
346
# It's possible that a file or symlink with that file-id
347
# already exists. If it does, we need to delete it.
348
if dir_file_id in inv:
349
self.record_delete(dirname, ie)
350
self.record_new(dirname, ie)
351
return basename, ie.file_id
353
def _get_directory_entry(self, inv, dirname):
354
"""Get the inventory entry for a directory.
356
Raises KeyError if dirname is not a directory in inv.
358
result = self.directory_entries.get(dirname)
360
if dirname in self._paths_deleted_this_commit:
363
file_id = inv.path2id(dirname)
364
except errors.NoSuchId:
365
# In a CHKInventory, this is raised if there's no root yet
369
result = inv[file_id]
370
# dirname must be a directory for us to return it
371
if result.kind == 'directory':
372
self.directory_entries[dirname] = result
377
def _delete_item(self, path, inv):
378
newly_added = self._new_file_ids.get(path)
380
# We've only just added this path earlier in this commit.
381
file_id = newly_added
382
# note: delta entries look like (old, new, file-id, ie)
383
ie = self._delta_entries_by_fileid[file_id][3]
385
file_id = inv.path2id(path)
387
self.mutter("ignoring delete of %s as not in inventory", path)
391
except errors.NoSuchId:
392
self.mutter("ignoring delete of %s as not in inventory", path)
394
self.record_delete(path, ie)
396
def _copy_item(self, src_path, dest_path, inv):
397
newly_changed = self._new_file_ids.get(src_path) or \
398
self._modified_file_ids.get(src_path)
400
# We've only just added/changed this path earlier in this commit.
401
file_id = newly_changed
402
# note: delta entries look like (old, new, file-id, ie)
403
ie = self._delta_entries_by_fileid[file_id][3]
405
file_id = inv.path2id(src_path)
407
self.warning("ignoring copy of %s to %s - source does not exist",
414
content = ''.join(self.lines_for_commit[file_id])
416
content = self.rev_store.get_file_text(self.parents[0], file_id)
417
self._modify_item(dest_path, kind, ie.executable, content, inv)
418
elif kind == 'symlink':
419
self._modify_item(dest_path, kind, False, ie.symlink_target, inv)
421
self.warning("ignoring copy of %s %s - feature not yet supported",
424
def _rename_item(self, old_path, new_path, inv):
425
existing = self._new_file_ids.get(old_path) or \
426
self._modified_file_ids.get(old_path)
428
# We've only just added/modified this path earlier in this commit.
429
# Change the add/modify of old_path to an add of new_path
430
self._rename_pending_change(old_path, new_path, existing)
433
file_id = inv.path2id(old_path)
436
"ignoring rename of %s to %s - old path does not exist" %
437
(old_path, new_path))
441
new_file_id = inv.path2id(new_path)
442
if new_file_id is not None:
443
self.record_delete(new_path, inv[new_file_id])
444
self.record_rename(old_path, new_path, file_id, ie)
446
# The revision-id for this entry will be/has been updated and
447
# that means the loader then needs to know what the "new" text is.
448
# We therefore must go back to the revision store to get it.
449
lines = self.rev_store.get_file_lines(rev_id, file_id)
450
self.lines_for_commit[file_id] = lines
452
def _delete_all_items(self, inv):
453
for name, root_item in inv.root.children.iteritems():
454
inv.remove_recursive_id(root_item.file_id)
456
def _warn_unless_in_merges(self, fileid, path):
457
if len(self.parents) <= 1:
459
for parent in self.parents[1:]:
460
if fileid in self.get_inventory(parent):
462
self.warning("ignoring delete of %s as not in parent inventories", path)
465
class InventoryCommitHandler(GenericCommitHandler):
466
"""A CommitHandler that builds and saves Inventory objects."""
468
def pre_process_files(self):
469
super(InventoryCommitHandler, self).pre_process_files()
471
# Seed the inventory from the previous one. Note that
472
# the parent class version of pre_process_files() has
473
# already set the right basis_inventory for this branch
474
# but we need to copy it in order to mutate it safely
475
# without corrupting the cached inventory value.
476
if len(self.parents) == 0:
477
self.inventory = self.basis_inventory
479
self.inventory = copy_inventory(self.basis_inventory)
480
self.inventory_root = self.inventory.root
482
# directory-path -> inventory-entry for current inventory
483
self.directory_entries = dict(self.inventory.directories())
485
# Initialise the inventory revision info as required
486
if self.rev_store.expects_rich_root():
487
self.inventory.revision_id = self.revision_id
489
# In this revision store, root entries have no knit or weave.
490
# When serializing out to disk and back in, root.revision is
491
# always the new revision_id.
492
self.inventory.root.revision = self.revision_id
494
def post_process_files(self):
495
"""Save the revision."""
496
self.cache_mgr.inventories[self.revision_id] = self.inventory
497
self.rev_store.load(self.revision, self.inventory, None,
498
lambda file_id: self._get_lines(file_id),
499
lambda file_id: self._get_per_file_parents(file_id),
500
lambda revision_ids: self._get_inventories(revision_ids))
502
def record_new(self, path, ie):
504
# If this is a merge, the file was most likely added already.
505
# The per-file parent(s) must therefore be calculated and
506
# we can't assume there are none.
507
per_file_parents, ie.revision = \
508
self.rev_store.get_parents_and_revision_for_entry(ie)
509
self.per_file_parents_for_commit[ie.file_id] = per_file_parents
510
self.inventory.add(ie)
511
except errors.DuplicateFileId:
512
# Directory already exists as a file or symlink
513
del self.inventory[ie.file_id]
515
self.inventory.add(ie)
517
def record_changed(self, path, ie, parent_id):
518
# HACK: no API for this (del+add does more than it needs to)
519
per_file_parents, ie.revision = \
520
self.rev_store.get_parents_and_revision_for_entry(ie)
521
self.per_file_parents_for_commit[ie.file_id] = per_file_parents
522
self.inventory._byid[ie.file_id] = ie
523
parent_ie = self.inventory._byid[parent_id]
524
parent_ie.children[ie.name] = ie
526
def record_delete(self, path, ie):
527
self.inventory.remove_recursive_id(ie.file_id)
529
def record_rename(self, old_path, new_path, file_id, ie):
530
# For a rename, the revision-id is always the new one so
531
# no need to change/set it here
532
ie.revision = self.revision_id
533
per_file_parents, _ = \
534
self.rev_store.get_parents_and_revision_for_entry(ie)
535
self.per_file_parents_for_commit[file_id] = per_file_parents
536
new_basename, new_parent_id = self._ensure_directory(new_path,
538
self.inventory.rename(file_id, new_parent_id, new_basename)
540
def modify_handler(self, filecmd):
541
if filecmd.dataref is not None:
542
data = self.cache_mgr.fetch_blob(filecmd.dataref)
545
self.debug("modifying %s", filecmd.path)
546
self._modify_item(filecmd.path, filecmd.kind,
547
filecmd.is_executable, data, self.inventory)
549
def delete_handler(self, filecmd):
550
self.debug("deleting %s", filecmd.path)
551
self._delete_item(filecmd.path, self.inventory)
553
def copy_handler(self, filecmd):
554
src_path = filecmd.src_path
555
dest_path = filecmd.dest_path
556
self.debug("copying %s to %s", src_path, dest_path)
557
self._copy_item(src_path, dest_path, self.inventory)
559
def rename_handler(self, filecmd):
560
old_path = filecmd.old_path
561
new_path = filecmd.new_path
562
self.debug("renaming %s to %s", old_path, new_path)
563
self._rename_item(old_path, new_path, self.inventory)
565
def deleteall_handler(self, filecmd):
566
self.debug("deleting all files (and also all directories)")
567
self._delete_all_items(self.inventory)
570
class InventoryDeltaCommitHandler(GenericCommitHandler):
571
"""A CommitHandler that builds Inventories by applying a delta."""
573
def pre_process_files(self):
574
super(InventoryDeltaCommitHandler, self).pre_process_files()
575
self._dirs_that_might_become_empty = set()
577
# A given file-id can only appear once so we accumulate
578
# the entries in a dict then build the actual delta at the end
579
self._delta_entries_by_fileid = {}
580
if len(self.parents) == 0 or not self.rev_store.expects_rich_root():
585
# Need to explicitly add the root entry for the first revision
586
# and for non rich-root inventories
587
root_id = inventory.ROOT_ID
588
root_ie = inventory.InventoryDirectory(root_id, u'', None)
589
root_ie.revision = self.revision_id
590
self._add_entry((old_path, '', root_id, root_ie))
592
def post_process_files(self):
593
"""Save the revision."""
594
delta = self._get_final_delta()
595
inv = self.rev_store.load_using_delta(self.revision,
596
self.basis_inventory, delta, None,
597
lambda file_id: self._get_lines(file_id),
598
lambda file_id: self._get_per_file_parents(file_id),
599
lambda revision_ids: self._get_inventories(revision_ids))
600
self.cache_mgr.inventories[self.revision_id] = inv
601
#print "committed %s" % self.revision_id
603
def _get_final_delta(self):
604
"""Generate the final delta.
606
Smart post-processing of changes, e.g. pruning of directories
607
that would become empty, goes here.
609
delta = list(self._delta_entries_by_fileid.values())
610
if self.prune_empty_dirs and self._dirs_that_might_become_empty:
611
candidates = self._dirs_that_might_become_empty
614
parent_dirs_that_might_become_empty = set()
615
for path, file_id in self._empty_after_delta(delta, candidates):
616
newly_added = self._new_file_ids.get(path)
618
never_born.add(newly_added)
620
delta.append((path, None, file_id, None))
621
parent_dir = osutils.dirname(path)
623
parent_dirs_that_might_become_empty.add(parent_dir)
624
candidates = parent_dirs_that_might_become_empty
625
# Clean up entries that got deleted before they were ever added
627
delta = [de for de in delta if de[2] not in never_born]
630
def _empty_after_delta(self, delta, candidates):
631
#self.mutter("delta so far is:\n%s" % "\n".join([str(de) for de in delta]))
632
#self.mutter("candidates for deletion are:\n%s" % "\n".join([c for c in candidates]))
633
new_inv = self._get_proposed_inventory(delta)
635
for dir in candidates:
636
file_id = new_inv.path2id(dir)
639
ie = new_inv[file_id]
640
if ie.kind != 'directory':
642
if len(ie.children) == 0:
643
result.append((dir, file_id))
645
self.note("pruning empty directory %s" % (dir,))
648
def _get_proposed_inventory(self, delta):
649
if len(self.parents):
650
new_inv = self.basis_inventory._get_mutable_inventory()
652
new_inv = inventory.Inventory(revision_id=self.revision_id)
653
# This is set in the delta so remove it to prevent a duplicate
654
del new_inv[inventory.ROOT_ID]
656
new_inv.apply_delta(delta)
657
except errors.InconsistentDelta:
658
self.mutter("INCONSISTENT DELTA IS:\n%s" % "\n".join([str(de) for de in delta]))
662
def _add_entry(self, entry):
663
# We need to combine the data if multiple entries have the same file-id.
664
# For example, a rename followed by a modification looks like:
666
# (x, y, f, e) & (y, y, f, g) => (x, y, f, g)
668
# Likewise, a modification followed by a rename looks like:
670
# (x, x, f, e) & (x, y, f, g) => (x, y, f, g)
672
# Here's a rename followed by a delete and a modification followed by
675
# (x, y, f, e) & (y, None, f, None) => (x, None, f, None)
676
# (x, x, f, e) & (x, None, f, None) => (x, None, f, None)
678
# In summary, we use the original old-path, new new-path and new ie
679
# when combining entries.
684
existing = self._delta_entries_by_fileid.get(file_id, None)
685
if existing is not None:
686
old_path = existing[0]
687
entry = (old_path, new_path, file_id, ie)
688
if new_path is None and old_path is None:
689
# This is a delete cancelling a previous add
690
del self._delta_entries_by_fileid[file_id]
691
parent_dir = osutils.dirname(existing[1])
692
self.mutter("cancelling add of %s with parent %s" % (existing[1], parent_dir))
694
self._dirs_that_might_become_empty.add(parent_dir)
697
self._delta_entries_by_fileid[file_id] = entry
699
# Collect parent directories that might become empty
702
parent_dir = osutils.dirname(old_path)
703
# note: no need to check the root
705
self._dirs_that_might_become_empty.add(parent_dir)
706
elif old_path is not None and old_path != new_path:
708
old_parent_dir = osutils.dirname(old_path)
709
new_parent_dir = osutils.dirname(new_path)
710
if old_parent_dir and old_parent_dir != new_parent_dir:
711
self._dirs_that_might_become_empty.add(old_parent_dir)
713
# Calculate the per-file parents, if not already done
714
if file_id in self.per_file_parents_for_commit:
718
# If this is a merge, the file was most likely added already.
719
# The per-file parent(s) must therefore be calculated and
720
# we can't assume there are none.
721
per_file_parents, ie.revision = \
722
self.rev_store.get_parents_and_revision_for_entry(ie)
723
self.per_file_parents_for_commit[file_id] = per_file_parents
724
elif new_path is None:
727
elif old_path != new_path:
729
per_file_parents, _ = \
730
self.rev_store.get_parents_and_revision_for_entry(ie)
731
self.per_file_parents_for_commit[file_id] = per_file_parents
734
per_file_parents, ie.revision = \
735
self.rev_store.get_parents_and_revision_for_entry(ie)
736
self.per_file_parents_for_commit[file_id] = per_file_parents
738
def record_new(self, path, ie):
739
self._add_entry((None, path, ie.file_id, ie))
741
def record_changed(self, path, ie, parent_id=None):
742
self._add_entry((path, path, ie.file_id, ie))
743
self._modified_file_ids[path] = ie.file_id
745
def record_delete(self, path, ie):
746
self._add_entry((path, None, ie.file_id, None))
747
self._paths_deleted_this_commit.add(path)
748
if ie.kind == 'directory':
750
del self.directory_entries[path]
753
for child_relpath, entry in \
754
self.basis_inventory.iter_entries_by_dir(from_dir=ie):
755
child_path = osutils.pathjoin(path, child_relpath)
756
self._add_entry((child_path, None, entry.file_id, None))
757
self._paths_deleted_this_commit.add(child_path)
758
if entry.kind == 'directory':
760
del self.directory_entries[child_path]
764
def record_rename(self, old_path, new_path, file_id, old_ie):
765
new_ie = old_ie.copy()
766
new_basename, new_parent_id = self._ensure_directory(new_path,
767
self.basis_inventory)
768
new_ie.name = new_basename
769
new_ie.parent_id = new_parent_id
770
new_ie.revision = self.revision_id
771
self._add_entry((old_path, new_path, file_id, new_ie))
772
self._modified_file_ids[new_path] = file_id
773
self._paths_deleted_this_commit.discard(new_path)
774
if new_ie.kind == 'directory':
775
self.directory_entries[new_path] = new_ie
777
def _rename_pending_change(self, old_path, new_path, file_id):
778
"""Instead of adding/modifying old-path, add new-path instead."""
779
# note: delta entries look like (old, new, file-id, ie)
780
old_ie = self._delta_entries_by_fileid[file_id][3]
782
# Delete the old path. Note that this might trigger implicit
783
# deletion of newly created parents that could now become empty.
784
self.record_delete(old_path, old_ie)
786
# Update the dictionaries used for tracking new file-ids
787
if old_path in self._new_file_ids:
788
del self._new_file_ids[old_path]
790
del self._modified_file_ids[old_path]
791
self._new_file_ids[new_path] = file_id
793
# Create the new InventoryEntry
795
basename, parent_id = self._ensure_directory(new_path,
796
self.basis_inventory)
797
ie = inventory.make_entry(kind, basename, parent_id, file_id)
798
ie.revision = self.revision_id
800
ie.executable = old_ie.executable
801
ie.text_sha1 = old_ie.text_sha1
802
ie.text_size = old_ie.text_size
803
elif kind == 'symlink':
804
ie.symlink_target = old_ie.symlink_target
807
self.record_new(new_path, ie)
809
def modify_handler(self, filecmd):
810
if filecmd.dataref is not None:
811
if filecmd.kind == commands.DIRECTORY_KIND:
813
elif filecmd.kind == commands.TREE_REFERENCE_KIND:
814
data = filecmd.dataref
816
data = self.cache_mgr.fetch_blob(filecmd.dataref)
819
self.debug("modifying %s", filecmd.path)
820
self._modify_item(filecmd.path, filecmd.kind,
821
filecmd.is_executable, data, self.basis_inventory)
823
def delete_handler(self, filecmd):
824
self.debug("deleting %s", filecmd.path)
825
self._delete_item(filecmd.path, self.basis_inventory)
827
def copy_handler(self, filecmd):
828
src_path = filecmd.src_path
829
dest_path = filecmd.dest_path
830
self.debug("copying %s to %s", src_path, dest_path)
831
self._copy_item(src_path, dest_path, self.basis_inventory)
833
def rename_handler(self, filecmd):
834
old_path = filecmd.old_path
835
new_path = filecmd.new_path
836
self.debug("renaming %s to %s", old_path, new_path)
837
self._rename_item(old_path, new_path, self.basis_inventory)
839
def deleteall_handler(self, filecmd):
840
self.debug("deleting all files (and also all directories)")
841
# I'm not 100% sure this will work in the delta case.
842
# But clearing out the basis inventory so that everything
843
# is added sounds ok in theory ...
844
# We grab a copy as the basis is likely to be cached and
845
# we don't want to destroy the cached version
846
self.basis_inventory = copy_inventory(self.basis_inventory)
847
self._delete_all_items(self.basis_inventory)