fix timezones in darcs-fast-export, take 2
[bzr-fastimport/rorcz.git] / bzr_commit_handler.py
blob31dd578060bdf42f6ba35d944bf3bb67297c5314
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."""
20 from bzrlib import (
21 errors,
22 generate_ids,
23 inventory,
24 osutils,
25 revision,
26 serializer,
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
42 return inv.copy()
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
82 if parents:
83 self.parents = [self.cache_mgr.revision_ids[p]
84 for p in parents]
85 else:
86 self.parents = []
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,
94 self.parent_invs)
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()
104 else:
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
108 else:
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."""
119 try:
120 inv = self.cache_mgr.inventories[revision_id]
121 except KeyError:
122 if self.verbose:
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
127 return 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.
143 present = []
144 inventories = []
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:
148 try:
149 inv = self.cache_mgr.inventories[revision_id]
150 present.append(revision_id)
151 except KeyError:
152 if self.verbose:
153 self.note("get_inventories cache miss for %s", revision_id)
154 # Not cached so reconstruct from the revision store
155 try:
156 inv = self.get_inventory(revision_id)
157 present.append(revision_id)
158 except:
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)
173 if id is not None:
174 return id, False
176 # Try the basis inventory
177 id = self.basis_inventory.path2id(path)
178 if id is not None:
179 return id, False
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)
185 if id is not None:
186 return id, False
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
194 return id, True
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."""
202 if email:
203 return "%s <%s>" % (name, email)
204 else:
205 return name
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],
231 committer=who,
232 message=message,
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
240 result = {}
241 if props is not None:
242 for name, value in props.items():
243 if value is None:
244 self.warning(
245 "converting None to empty string for property %s"
246 % (name,))
247 result[name] = ''
248 else:
249 result[name] = value
250 return result
252 def _save_author_info(self, rev_props):
253 author = self.command.author
254 if author is None:
255 return
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])]
261 else:
262 return
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)
273 if existing:
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" %
278 (path,))
279 return
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
286 if kind == 'file':
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] = []
302 else:
303 self.warning("Cannot import items of kind '%s' yet - ignoring '%s'"
304 % (kind, path))
305 return
306 # Record it
307 if file_id in inv:
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)
312 else:
313 try:
314 self.record_new(path, ie)
315 except:
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,)
319 raise
321 def _ensure_directory(self, path, inv):
322 """Ensure that the containing directory exists for 'path'"""
323 dirname, basename = osutils.split(path)
324 if dirname == '':
325 # the root node doesn't get updated
326 return basename, self.inventory_root_id
327 try:
328 ie = self._get_directory_entry(inv, dirname)
329 except KeyError:
330 # We will create this entry, since it doesn't exist
331 pass
332 else:
333 return basename, ie.file_id
335 # No directory existed, we will just create one, first, make sure
336 # the parent exists
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)
360 if result is None:
361 if dirname in self._paths_deleted_this_commit:
362 raise KeyError
363 try:
364 file_id = inv.path2id(dirname)
365 except errors.NoSuchId:
366 # In a CHKInventory, this is raised if there's no root yet
367 raise KeyError
368 if file_id is None:
369 raise KeyError
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
374 else:
375 raise KeyError
376 return result
378 def _delete_item(self, path, inv):
379 newly_added = self._new_file_ids.get(path)
380 if newly_added:
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]
385 else:
386 file_id = inv.path2id(path)
387 if file_id is None:
388 self.mutter("ignoring delete of %s as not in inventory", path)
389 return
390 try:
391 ie = inv[file_id]
392 except errors.NoSuchId:
393 self.mutter("ignoring delete of %s as not in inventory", path)
394 return
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)
400 if newly_changed:
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]
405 else:
406 file_id = inv.path2id(src_path)
407 if file_id is None:
408 self.warning("ignoring copy of %s to %s - source does not exist",
409 src_path, dest_path)
410 return
411 ie = inv[file_id]
412 kind = ie.kind
413 if kind == 'file':
414 if newly_changed:
415 content = ''.join(self.lines_for_commit[file_id])
416 else:
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)
421 else:
422 self.warning("ignoring copy of %s %s - feature not yet supported",
423 kind, path)
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)
428 if existing:
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)
432 return
434 file_id = inv.path2id(old_path)
435 if file_id is None:
436 self.warning(
437 "ignoring rename of %s to %s - old path does not exist" %
438 (old_path, new_path))
439 return
440 ie = inv[file_id]
441 rev_id = ie.revision
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:
459 return
460 for parent in self.parents[1:]:
461 if fileid in self.get_inventory(parent):
462 return
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
479 else:
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
489 else:
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):
504 try:
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]
515 # Try again
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,
538 self.inventory)
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)
544 else:
545 data = filecmd.data
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():
582 if self.parents:
583 old_path = ''
584 else:
585 old_path = None
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
613 while candidates:
614 never_born = set()
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)
618 if newly_added:
619 never_born.add(newly_added)
620 else:
621 delta.append((path, None, file_id, None))
622 parent_dir = osutils.dirname(path)
623 if parent_dir:
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
627 if never_born:
628 delta = [de for de in delta if de[2] not in never_born]
629 return delta
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)
635 result = []
636 for dir in candidates:
637 file_id = new_inv.path2id(dir)
638 if file_id is None:
639 continue
640 ie = new_inv[file_id]
641 if ie.kind != 'directory':
642 continue
643 if len(ie.children) == 0:
644 result.append((dir, file_id))
645 if self.verbose:
646 self.note("pruning empty directory %s" % (dir,))
647 return result
649 def _get_proposed_inventory(self, delta):
650 if len(self.parents):
651 new_inv = self.basis_inventory._get_mutable_inventory()
652 else:
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]
656 try:
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]))
660 raise
661 return new_inv
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
674 # a delete:
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.
681 old_path = entry[0]
682 new_path = entry[1]
683 file_id = entry[2]
684 ie = entry[3]
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))
694 if parent_dir:
695 self._dirs_that_might_become_empty.add(parent_dir)
696 return
697 else:
698 self._delta_entries_by_fileid[file_id] = entry
700 # Collect parent directories that might become empty
701 if new_path is None:
702 # delete
703 parent_dir = osutils.dirname(old_path)
704 # note: no need to check the root
705 if parent_dir:
706 self._dirs_that_might_become_empty.add(parent_dir)
707 elif old_path is not None and old_path != new_path:
708 # rename
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:
716 return
717 if old_path is None:
718 # add
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:
726 # delete
727 pass
728 elif old_path != new_path:
729 # rename
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
733 else:
734 # modify
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':
750 try:
751 del self.directory_entries[path]
752 except KeyError:
753 pass
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':
760 try:
761 del self.directory_entries[child_path]
762 except KeyError:
763 pass
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]
790 else:
791 del self._modified_file_ids[old_path]
792 self._new_file_ids[new_path] = file_id
794 # Create the new InventoryEntry
795 kind = old_ie.kind
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
800 if kind == 'file':
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
807 # Record it
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:
813 data = None
814 elif filecmd.kind == commands.TREE_REFERENCE_KIND:
815 data = filecmd.dataref
816 else:
817 data = self.cache_mgr.fetch_blob(filecmd.dataref)
818 else:
819 data = filecmd.data
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)