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 dirname
, basename
= osutils
.split(path
)
190 id = generate_ids
.gen_file_id(basename
)
191 self
.debug("Generated new file id %s for '%s' in revision-id '%s'",
192 id, path
, self
.revision_id
)
193 self
._new
_file
_ids
[path
] = id
196 def bzr_file_id(self
, path
):
197 """Get a Bazaar file identifier for a path."""
198 return self
.bzr_file_id_and_new(path
)[0]
200 def _format_name_email(self
, name
, email
):
201 """Format name & email as a string."""
203 return "%s <%s>" % (name
, email
)
207 def gen_revision_id(self
):
208 """Generate a revision id.
210 Subclasses may override this to produce deterministic ids say.
212 committer
= self
.command
.committer
213 # Perhaps 'who' being the person running the import is ok? If so,
214 # it might be a bit quicker and give slightly better compression?
215 who
= self
._format
_name
_email
(committer
[0], committer
[1])
216 timestamp
= committer
[2]
217 return generate_ids
.gen_revision_id(who
, timestamp
)
219 def build_revision(self
):
220 rev_props
= self
._legal
_revision
_properties
(self
.command
.properties
)
221 self
._save
_author
_info
(rev_props
)
222 committer
= self
.command
.committer
223 who
= self
._format
_name
_email
(committer
[0], committer
[1])
224 message
= self
.command
.message
225 if not _serializer_handles_escaping
:
226 # We need to assume the bad ol' days
227 message
= helpers
.escape_commit_message(message
)
228 return revision
.Revision(
229 timestamp
=committer
[2],
230 timezone
=committer
[3],
233 revision_id
=self
.revision_id
,
234 properties
=rev_props
,
235 parent_ids
=self
.parents
)
237 def _legal_revision_properties(self
, props
):
238 """Clean-up any revision properties we can't handle."""
239 # For now, we just check for None because that's not allowed in 2.0rc1
241 if props
is not None:
242 for name
, value
in props
.items():
245 "converting None to empty string for property %s"
252 def _save_author_info(self
, rev_props
):
253 author
= self
.command
.author
256 if self
.command
.more_authors
:
257 authors
= [author
] + self
.command
.more_authors
258 author_ids
= [self
._format
_name
_email
(a
[0], a
[1]) for a
in authors
]
259 elif author
!= self
.command
.committer
:
260 author_ids
= [self
._format
_name
_email
(author
[0], author
[1])]
263 # If we reach here, there are authors worth storing
264 rev_props
['authors'] = "\n".join(author_ids
)
266 def _modify_item(self
, path
, kind
, is_executable
, data
, inv
):
267 """Add to or change an item in the inventory."""
268 # If we've already added this, warn the user that we're ignoring it.
269 # In the future, it might be nice to double check that the new data
270 # is the same as the old but, frankly, exporters should be fixed
271 # not to produce bad data streams in the first place ...
272 existing
= self
._new
_file
_ids
.get(path
)
274 # We don't warn about directories because it's fine for them
275 # to be created already by a previous rename
276 if kind
!= 'directory':
277 self
.warning("%s already added in this commit - ignoring" %
281 # Create the new InventoryEntry
282 basename
, parent_id
= self
._ensure
_directory
(path
, inv
)
283 file_id
= self
.bzr_file_id(path
)
284 ie
= inventory
.make_entry(kind
, basename
, parent_id
, file_id
)
285 ie
.revision
= self
.revision_id
287 ie
.executable
= is_executable
288 lines
= osutils
.split_lines(data
)
289 ie
.text_sha1
= osutils
.sha_strings(lines
)
290 ie
.text_size
= sum(map(len, lines
))
291 self
.lines_for_commit
[file_id
] = lines
292 elif kind
== 'directory':
293 self
.directory_entries
[path
] = ie
294 # There are no lines stored for a directory so
295 # make sure the cache used by get_lines knows that
296 self
.lines_for_commit
[file_id
] = []
297 elif kind
== 'symlink':
298 ie
.symlink_target
= data
.encode('utf8')
299 # There are no lines stored for a symlink so
300 # make sure the cache used by get_lines knows that
301 self
.lines_for_commit
[file_id
] = []
303 self
.warning("Cannot import items of kind '%s' yet - ignoring '%s'"
308 old_ie
= inv
[file_id
]
309 if old_ie
.kind
== 'directory':
310 self
.record_delete(path
, old_ie
)
311 self
.record_changed(path
, ie
, parent_id
)
314 self
.record_new(path
, ie
)
316 print "failed to add path '%s' with entry '%s' in command %s" \
317 % (path
, ie
, self
.command
.id)
318 print "parent's children are:\n%r\n" % (ie
.parent_id
.children
,)
321 def _ensure_directory(self
, path
, inv
):
322 """Ensure that the containing directory exists for 'path'"""
323 dirname
, basename
= osutils
.split(path
)
325 # the root node doesn't get updated
326 return basename
, self
.inventory_root_id
328 ie
= self
._get
_directory
_entry
(inv
, dirname
)
330 # We will create this entry, since it doesn't exist
333 return basename
, ie
.file_id
335 # No directory existed, we will just create one, first, make sure
337 dir_basename
, parent_id
= self
._ensure
_directory
(dirname
, inv
)
338 dir_file_id
= self
.bzr_file_id(dirname
)
339 ie
= inventory
.entry_factory
['directory'](dir_file_id
,
340 dir_basename
, parent_id
)
341 ie
.revision
= self
.revision_id
342 self
.directory_entries
[dirname
] = ie
343 # There are no lines stored for a directory so
344 # make sure the cache used by get_lines knows that
345 self
.lines_for_commit
[dir_file_id
] = []
347 # It's possible that a file or symlink with that file-id
348 # already exists. If it does, we need to delete it.
349 if dir_file_id
in inv
:
350 self
.record_delete(dirname
, ie
)
351 self
.record_new(dirname
, ie
)
352 return basename
, ie
.file_id
354 def _get_directory_entry(self
, inv
, dirname
):
355 """Get the inventory entry for a directory.
357 Raises KeyError if dirname is not a directory in inv.
359 result
= self
.directory_entries
.get(dirname
)
361 if dirname
in self
._paths
_deleted
_this
_commit
:
364 file_id
= inv
.path2id(dirname
)
365 except errors
.NoSuchId
:
366 # In a CHKInventory, this is raised if there's no root yet
370 result
= inv
[file_id
]
371 # dirname must be a directory for us to return it
372 if result
.kind
== 'directory':
373 self
.directory_entries
[dirname
] = result
378 def _delete_item(self
, path
, inv
):
379 newly_added
= self
._new
_file
_ids
.get(path
)
381 # We've only just added this path earlier in this commit.
382 file_id
= newly_added
383 # note: delta entries look like (old, new, file-id, ie)
384 ie
= self
._delta
_entries
_by
_fileid
[file_id
][3]
386 file_id
= inv
.path2id(path
)
388 self
.mutter("ignoring delete of %s as not in inventory", path
)
392 except errors
.NoSuchId
:
393 self
.mutter("ignoring delete of %s as not in inventory", path
)
395 self
.record_delete(path
, ie
)
397 def _copy_item(self
, src_path
, dest_path
, inv
):
398 newly_changed
= self
._new
_file
_ids
.get(src_path
) or \
399 self
._modified
_file
_ids
.get(src_path
)
401 # We've only just added/changed this path earlier in this commit.
402 file_id
= newly_changed
403 # note: delta entries look like (old, new, file-id, ie)
404 ie
= self
._delta
_entries
_by
_fileid
[file_id
][3]
406 file_id
= inv
.path2id(src_path
)
408 self
.warning("ignoring copy of %s to %s - source does not exist",
415 content
= ''.join(self
.lines_for_commit
[file_id
])
417 content
= self
.rev_store
.get_file_text(self
.parents
[0], file_id
)
418 self
._modify
_item
(dest_path
, kind
, ie
.executable
, content
, inv
)
419 elif kind
== 'symlink':
420 self
._modify
_item
(dest_path
, kind
, False, ie
.symlink_target
, inv
)
422 self
.warning("ignoring copy of %s %s - feature not yet supported",
425 def _rename_item(self
, old_path
, new_path
, inv
):
426 existing
= self
._new
_file
_ids
.get(old_path
) or \
427 self
._modified
_file
_ids
.get(old_path
)
429 # We've only just added/modified this path earlier in this commit.
430 # Change the add/modify of old_path to an add of new_path
431 self
._rename
_pending
_change
(old_path
, new_path
, existing
)
434 file_id
= inv
.path2id(old_path
)
437 "ignoring rename of %s to %s - old path does not exist" %
438 (old_path
, new_path
))
442 new_file_id
= inv
.path2id(new_path
)
443 if new_file_id
is not None:
444 self
.record_delete(new_path
, inv
[new_file_id
])
445 self
.record_rename(old_path
, new_path
, file_id
, ie
)
447 # The revision-id for this entry will be/has been updated and
448 # that means the loader then needs to know what the "new" text is.
449 # We therefore must go back to the revision store to get it.
450 lines
= self
.rev_store
.get_file_lines(rev_id
, file_id
)
451 self
.lines_for_commit
[file_id
] = lines
453 def _delete_all_items(self
, inv
):
454 for name
, root_item
in inv
.root
.children
.iteritems():
455 inv
.remove_recursive_id(root_item
.file_id
)
457 def _warn_unless_in_merges(self
, fileid
, path
):
458 if len(self
.parents
) <= 1:
460 for parent
in self
.parents
[1:]:
461 if fileid
in self
.get_inventory(parent
):
463 self
.warning("ignoring delete of %s as not in parent inventories", path
)
466 class InventoryCommitHandler(GenericCommitHandler
):
467 """A CommitHandler that builds and saves Inventory objects."""
469 def pre_process_files(self
):
470 super(InventoryCommitHandler
, self
).pre_process_files()
472 # Seed the inventory from the previous one. Note that
473 # the parent class version of pre_process_files() has
474 # already set the right basis_inventory for this branch
475 # but we need to copy it in order to mutate it safely
476 # without corrupting the cached inventory value.
477 if len(self
.parents
) == 0:
478 self
.inventory
= self
.basis_inventory
480 self
.inventory
= copy_inventory(self
.basis_inventory
)
481 self
.inventory_root
= self
.inventory
.root
483 # directory-path -> inventory-entry for current inventory
484 self
.directory_entries
= dict(self
.inventory
.directories())
486 # Initialise the inventory revision info as required
487 if self
.rev_store
.expects_rich_root():
488 self
.inventory
.revision_id
= self
.revision_id
490 # In this revision store, root entries have no knit or weave.
491 # When serializing out to disk and back in, root.revision is
492 # always the new revision_id.
493 self
.inventory
.root
.revision
= self
.revision_id
495 def post_process_files(self
):
496 """Save the revision."""
497 self
.cache_mgr
.inventories
[self
.revision_id
] = self
.inventory
498 self
.rev_store
.load(self
.revision
, self
.inventory
, None,
499 lambda file_id
: self
._get
_lines
(file_id
),
500 lambda file_id
: self
._get
_per
_file
_parents
(file_id
),
501 lambda revision_ids
: self
._get
_inventories
(revision_ids
))
503 def record_new(self
, path
, ie
):
505 # If this is a merge, the file was most likely added already.
506 # The per-file parent(s) must therefore be calculated and
507 # we can't assume there are none.
508 per_file_parents
, ie
.revision
= \
509 self
.rev_store
.get_parents_and_revision_for_entry(ie
)
510 self
.per_file_parents_for_commit
[ie
.file_id
] = per_file_parents
511 self
.inventory
.add(ie
)
512 except errors
.DuplicateFileId
:
513 # Directory already exists as a file or symlink
514 del self
.inventory
[ie
.file_id
]
516 self
.inventory
.add(ie
)
518 def record_changed(self
, path
, ie
, parent_id
):
519 # HACK: no API for this (del+add does more than it needs to)
520 per_file_parents
, ie
.revision
= \
521 self
.rev_store
.get_parents_and_revision_for_entry(ie
)
522 self
.per_file_parents_for_commit
[ie
.file_id
] = per_file_parents
523 self
.inventory
._byid
[ie
.file_id
] = ie
524 parent_ie
= self
.inventory
._byid
[parent_id
]
525 parent_ie
.children
[ie
.name
] = ie
527 def record_delete(self
, path
, ie
):
528 self
.inventory
.remove_recursive_id(ie
.file_id
)
530 def record_rename(self
, old_path
, new_path
, file_id
, ie
):
531 # For a rename, the revision-id is always the new one so
532 # no need to change/set it here
533 ie
.revision
= self
.revision_id
534 per_file_parents
, _
= \
535 self
.rev_store
.get_parents_and_revision_for_entry(ie
)
536 self
.per_file_parents_for_commit
[file_id
] = per_file_parents
537 new_basename
, new_parent_id
= self
._ensure
_directory
(new_path
,
539 self
.inventory
.rename(file_id
, new_parent_id
, new_basename
)
541 def modify_handler(self
, filecmd
):
542 if filecmd
.dataref
is not None:
543 data
= self
.cache_mgr
.fetch_blob(filecmd
.dataref
)
546 self
.debug("modifying %s", filecmd
.path
)
547 self
._modify
_item
(filecmd
.path
, filecmd
.kind
,
548 filecmd
.is_executable
, data
, self
.inventory
)
550 def delete_handler(self
, filecmd
):
551 self
.debug("deleting %s", filecmd
.path
)
552 self
._delete
_item
(filecmd
.path
, self
.inventory
)
554 def copy_handler(self
, filecmd
):
555 src_path
= filecmd
.src_path
556 dest_path
= filecmd
.dest_path
557 self
.debug("copying %s to %s", src_path
, dest_path
)
558 self
._copy
_item
(src_path
, dest_path
, self
.inventory
)
560 def rename_handler(self
, filecmd
):
561 old_path
= filecmd
.old_path
562 new_path
= filecmd
.new_path
563 self
.debug("renaming %s to %s", old_path
, new_path
)
564 self
._rename
_item
(old_path
, new_path
, self
.inventory
)
566 def deleteall_handler(self
, filecmd
):
567 self
.debug("deleting all files (and also all directories)")
568 self
._delete
_all
_items
(self
.inventory
)
571 class InventoryDeltaCommitHandler(GenericCommitHandler
):
572 """A CommitHandler that builds Inventories by applying a delta."""
574 def pre_process_files(self
):
575 super(InventoryDeltaCommitHandler
, self
).pre_process_files()
576 self
._dirs
_that
_might
_become
_empty
= set()
578 # A given file-id can only appear once so we accumulate
579 # the entries in a dict then build the actual delta at the end
580 self
._delta
_entries
_by
_fileid
= {}
581 if len(self
.parents
) == 0 or not self
.rev_store
.expects_rich_root():
586 # Need to explicitly add the root entry for the first revision
587 # and for non rich-root inventories
588 root_id
= inventory
.ROOT_ID
589 root_ie
= inventory
.InventoryDirectory(root_id
, u
'', None)
590 root_ie
.revision
= self
.revision_id
591 self
._add
_entry
((old_path
, '', root_id
, root_ie
))
593 def post_process_files(self
):
594 """Save the revision."""
595 delta
= self
._get
_final
_delta
()
596 inv
= self
.rev_store
.load_using_delta(self
.revision
,
597 self
.basis_inventory
, delta
, None,
598 lambda file_id
: self
._get
_lines
(file_id
),
599 lambda file_id
: self
._get
_per
_file
_parents
(file_id
),
600 lambda revision_ids
: self
._get
_inventories
(revision_ids
))
601 self
.cache_mgr
.inventories
[self
.revision_id
] = inv
602 #print "committed %s" % self.revision_id
604 def _get_final_delta(self
):
605 """Generate the final delta.
607 Smart post-processing of changes, e.g. pruning of directories
608 that would become empty, goes here.
610 delta
= list(self
._delta
_entries
_by
_fileid
.values())
611 if self
.prune_empty_dirs
and self
._dirs
_that
_might
_become
_empty
:
612 candidates
= self
._dirs
_that
_might
_become
_empty
615 parent_dirs_that_might_become_empty
= set()
616 for path
, file_id
in self
._empty
_after
_delta
(delta
, candidates
):
617 newly_added
= self
._new
_file
_ids
.get(path
)
619 never_born
.add(newly_added
)
621 delta
.append((path
, None, file_id
, None))
622 parent_dir
= osutils
.dirname(path
)
624 parent_dirs_that_might_become_empty
.add(parent_dir
)
625 candidates
= parent_dirs_that_might_become_empty
626 # Clean up entries that got deleted before they were ever added
628 delta
= [de
for de
in delta
if de
[2] not in never_born
]
631 def _empty_after_delta(self
, delta
, candidates
):
632 #self.mutter("delta so far is:\n%s" % "\n".join([str(de) for de in delta]))
633 #self.mutter("candidates for deletion are:\n%s" % "\n".join([c for c in candidates]))
634 new_inv
= self
._get
_proposed
_inventory
(delta
)
636 for dir in candidates
:
637 file_id
= new_inv
.path2id(dir)
640 ie
= new_inv
[file_id
]
641 if ie
.kind
!= 'directory':
643 if len(ie
.children
) == 0:
644 result
.append((dir, file_id
))
646 self
.note("pruning empty directory %s" % (dir,))
649 def _get_proposed_inventory(self
, delta
):
650 if len(self
.parents
):
651 new_inv
= self
.basis_inventory
._get
_mutable
_inventory
()
653 new_inv
= inventory
.Inventory(revision_id
=self
.revision_id
)
654 # This is set in the delta so remove it to prevent a duplicate
655 del new_inv
[inventory
.ROOT_ID
]
657 new_inv
.apply_delta(delta
)
658 except errors
.InconsistentDelta
:
659 self
.mutter("INCONSISTENT DELTA IS:\n%s" % "\n".join([str(de
) for de
in delta
]))
663 def _add_entry(self
, entry
):
664 # We need to combine the data if multiple entries have the same file-id.
665 # For example, a rename followed by a modification looks like:
667 # (x, y, f, e) & (y, y, f, g) => (x, y, f, g)
669 # Likewise, a modification followed by a rename looks like:
671 # (x, x, f, e) & (x, y, f, g) => (x, y, f, g)
673 # Here's a rename followed by a delete and a modification followed by
676 # (x, y, f, e) & (y, None, f, None) => (x, None, f, None)
677 # (x, x, f, e) & (x, None, f, None) => (x, None, f, None)
679 # In summary, we use the original old-path, new new-path and new ie
680 # when combining entries.
685 existing
= self
._delta
_entries
_by
_fileid
.get(file_id
, None)
686 if existing
is not None:
687 old_path
= existing
[0]
688 entry
= (old_path
, new_path
, file_id
, ie
)
689 if new_path
is None and old_path
is None:
690 # This is a delete cancelling a previous add
691 del self
._delta
_entries
_by
_fileid
[file_id
]
692 parent_dir
= osutils
.dirname(existing
[1])
693 self
.mutter("cancelling add of %s with parent %s" % (existing
[1], parent_dir
))
695 self
._dirs
_that
_might
_become
_empty
.add(parent_dir
)
698 self
._delta
_entries
_by
_fileid
[file_id
] = entry
700 # Collect parent directories that might become empty
703 parent_dir
= osutils
.dirname(old_path
)
704 # note: no need to check the root
706 self
._dirs
_that
_might
_become
_empty
.add(parent_dir
)
707 elif old_path
is not None and old_path
!= new_path
:
709 old_parent_dir
= osutils
.dirname(old_path
)
710 new_parent_dir
= osutils
.dirname(new_path
)
711 if old_parent_dir
and old_parent_dir
!= new_parent_dir
:
712 self
._dirs
_that
_might
_become
_empty
.add(old_parent_dir
)
714 # Calculate the per-file parents, if not already done
715 if file_id
in self
.per_file_parents_for_commit
:
719 # If this is a merge, the file was most likely added already.
720 # The per-file parent(s) must therefore be calculated and
721 # we can't assume there are none.
722 per_file_parents
, ie
.revision
= \
723 self
.rev_store
.get_parents_and_revision_for_entry(ie
)
724 self
.per_file_parents_for_commit
[file_id
] = per_file_parents
725 elif new_path
is None:
728 elif old_path
!= new_path
:
730 per_file_parents
, _
= \
731 self
.rev_store
.get_parents_and_revision_for_entry(ie
)
732 self
.per_file_parents_for_commit
[file_id
] = per_file_parents
735 per_file_parents
, ie
.revision
= \
736 self
.rev_store
.get_parents_and_revision_for_entry(ie
)
737 self
.per_file_parents_for_commit
[file_id
] = per_file_parents
739 def record_new(self
, path
, ie
):
740 self
._add
_entry
((None, path
, ie
.file_id
, ie
))
742 def record_changed(self
, path
, ie
, parent_id
=None):
743 self
._add
_entry
((path
, path
, ie
.file_id
, ie
))
744 self
._modified
_file
_ids
[path
] = ie
.file_id
746 def record_delete(self
, path
, ie
):
747 self
._add
_entry
((path
, None, ie
.file_id
, None))
748 self
._paths
_deleted
_this
_commit
.add(path
)
749 if ie
.kind
== 'directory':
751 del self
.directory_entries
[path
]
754 for child_relpath
, entry
in \
755 self
.basis_inventory
.iter_entries_by_dir(from_dir
=ie
):
756 child_path
= osutils
.pathjoin(path
, child_relpath
)
757 self
._add
_entry
((child_path
, None, entry
.file_id
, None))
758 self
._paths
_deleted
_this
_commit
.add(child_path
)
759 if entry
.kind
== 'directory':
761 del self
.directory_entries
[child_path
]
765 def record_rename(self
, old_path
, new_path
, file_id
, old_ie
):
766 new_ie
= old_ie
.copy()
767 new_basename
, new_parent_id
= self
._ensure
_directory
(new_path
,
768 self
.basis_inventory
)
769 new_ie
.name
= new_basename
770 new_ie
.parent_id
= new_parent_id
771 new_ie
.revision
= self
.revision_id
772 self
._add
_entry
((old_path
, new_path
, file_id
, new_ie
))
773 self
._modified
_file
_ids
[new_path
] = file_id
774 self
._paths
_deleted
_this
_commit
.discard(new_path
)
775 if new_ie
.kind
== 'directory':
776 self
.directory_entries
[new_path
] = new_ie
778 def _rename_pending_change(self
, old_path
, new_path
, file_id
):
779 """Instead of adding/modifying old-path, add new-path instead."""
780 # note: delta entries look like (old, new, file-id, ie)
781 old_ie
= self
._delta
_entries
_by
_fileid
[file_id
][3]
783 # Delete the old path. Note that this might trigger implicit
784 # deletion of newly created parents that could now become empty.
785 self
.record_delete(old_path
, old_ie
)
787 # Update the dictionaries used for tracking new file-ids
788 if old_path
in self
._new
_file
_ids
:
789 del self
._new
_file
_ids
[old_path
]
791 del self
._modified
_file
_ids
[old_path
]
792 self
._new
_file
_ids
[new_path
] = file_id
794 # Create the new InventoryEntry
796 basename
, parent_id
= self
._ensure
_directory
(new_path
,
797 self
.basis_inventory
)
798 ie
= inventory
.make_entry(kind
, basename
, parent_id
, file_id
)
799 ie
.revision
= self
.revision_id
801 ie
.executable
= old_ie
.executable
802 ie
.text_sha1
= old_ie
.text_sha1
803 ie
.text_size
= old_ie
.text_size
804 elif kind
== 'symlink':
805 ie
.symlink_target
= old_ie
.symlink_target
808 self
.record_new(new_path
, ie
)
810 def modify_handler(self
, filecmd
):
811 if filecmd
.dataref
is not None:
812 if filecmd
.kind
== commands
.DIRECTORY_KIND
:
814 elif filecmd
.kind
== commands
.TREE_REFERENCE_KIND
:
815 data
= filecmd
.dataref
817 data
= self
.cache_mgr
.fetch_blob(filecmd
.dataref
)
820 self
.debug("modifying %s", filecmd
.path
)
821 self
._modify
_item
(filecmd
.path
, filecmd
.kind
,
822 filecmd
.is_executable
, data
, self
.basis_inventory
)
824 def delete_handler(self
, filecmd
):
825 self
.debug("deleting %s", filecmd
.path
)
826 self
._delete
_item
(filecmd
.path
, self
.basis_inventory
)
828 def copy_handler(self
, filecmd
):
829 src_path
= filecmd
.src_path
830 dest_path
= filecmd
.dest_path
831 self
.debug("copying %s to %s", src_path
, dest_path
)
832 self
._copy
_item
(src_path
, dest_path
, self
.basis_inventory
)
834 def rename_handler(self
, filecmd
):
835 old_path
= filecmd
.old_path
836 new_path
= filecmd
.new_path
837 self
.debug("renaming %s to %s", old_path
, new_path
)
838 self
._rename
_item
(old_path
, new_path
, self
.basis_inventory
)
840 def deleteall_handler(self
, filecmd
):
841 self
.debug("deleting all files (and also all directories)")
842 # I'm not 100% sure this will work in the delta case.
843 # But clearing out the basis inventory so that everything
844 # is added sounds ok in theory ...
845 # We grab a copy as the basis is likely to be cached and
846 # we don't want to destroy the cached version
847 self
.basis_inventory
= copy_inventory(self
.basis_inventory
)
848 self
._delete
_all
_items
(self
.basis_inventory
)