50
49
# TODO: Change the parameter 'rev_id' to 'revision_id' to be consistent with
51
50
# the rest of the code; add a deprecation of the old name.
57
from cStringIO import StringIO
59
from .branch import Branch
60
from .cleanup import ExitStack
62
from .errors import (BzrError,
66
from .osutils import (get_user_encoding,
69
minimum_path_selection,
71
from .trace import mutter, note, is_quiet
72
from .tree import TreeChange
73
from .urlutils import unescape_for_display
74
from .i18n import gettext
77
class PointlessCommit(BzrError):
79
_fmt = "No changes to commit"
82
class CannotCommitSelectedFileMerge(BzrError):
84
_fmt = 'Selected-file commit of merges is not supported yet:'\
85
' files %(files_str)s'
87
def __init__(self, files):
88
files_str = ', '.join(files)
89
BzrError.__init__(self, files=files, files_str=files_str)
92
def filter_excluded(iter_changes, exclude):
93
"""Filter exclude filenames.
95
:param iter_changes: iter_changes function
96
:param exclude: List of paths to exclude
97
:return: iter_changes function
99
for change in iter_changes:
100
new_excluded = (change.path[1] is not None and
101
is_inside_any(exclude, change.path[1]))
103
old_excluded = (change.path[0] is not None and
104
is_inside_any(exclude, change.path[0]))
106
if old_excluded and new_excluded:
109
if old_excluded or new_excluded:
110
# TODO(jelmer): Perhaps raise an error here instead?
67
from bzrlib.branch import Branch
69
from bzrlib.errors import (BzrError, PointlessCommit,
73
from bzrlib.osutils import (get_user_encoding,
74
kind_marker, isdir,isfile, is_inside_any,
75
is_inside_or_parent_of_any,
76
minimum_path_selection,
77
quotefn, sha_file, split_lines,
80
from bzrlib.testament import Testament
81
from bzrlib.trace import mutter, note, warning, is_quiet
82
from bzrlib.inventory import Inventory, InventoryEntry, make_entry
83
from bzrlib import symbol_versioning
84
from bzrlib.symbol_versioning import (deprecated_passed,
87
from bzrlib.workingtree import WorkingTree
88
from bzrlib.urlutils import unescape_for_display
116
92
class NullCommitReporter(object):
117
93
"""I report on progress of a commit."""
119
def started(self, revno, revid, location):
95
def started(self, revno, revid, location=None):
97
symbol_versioning.warn("As of bzr 1.0 you must pass a location "
98
"to started.", DeprecationWarning,
122
102
def snapshot_change(self, change, path):
148
131
note(format, *args)
150
133
def snapshot_change(self, change, path):
151
if path == '' and change in (gettext('added'), gettext('modified')):
134
if path == '' and change in ('added', 'modified'):
153
136
self._note("%s %s", change, path)
155
def started(self, revno, rev_id, location):
157
gettext('Committing to: %s'),
158
unescape_for_display(location, 'utf-8'))
138
def started(self, revno, rev_id, location=None):
139
if location is not None:
140
location = ' to: ' + unescape_for_display(location, 'utf-8')
142
# When started was added, location was only made optional by
143
# accident. Matt Nordhoff 20071129
144
symbol_versioning.warn("As of bzr 1.0 you must pass a location "
145
"to started.", DeprecationWarning,
148
self._note('Committing%s', location)
160
150
def completed(self, revno, rev_id):
161
if revno is not None:
162
self._note(gettext('Committed revision %d.'), revno)
163
# self._note goes to the console too; so while we want to log the
164
# rev_id, we can't trivially only log it. (See bug 526425). Long
165
# term we should rearrange the reporting structure, but for now
166
# we just mutter seperately. We mutter the revid and revno together
167
# so that concurrent bzr invocations won't lead to confusion.
168
mutter('Committed revid %s as revno %d.', rev_id, revno)
170
self._note(gettext('Committed revid %s.'), rev_id)
151
self._note('Committed revision %d.', revno)
172
153
def deleted(self, path):
173
self._note(gettext('deleted %s'), path)
154
self._note('deleted %s', path)
156
def escaped(self, escape_count, message):
157
self._note("replaced %d control characters in message", escape_count)
175
159
def missing(self, path):
176
self._note(gettext('missing %s'), path)
160
self._note('missing %s', path)
178
162
def renamed(self, change, old_path, new_path):
179
163
self._note('%s %s => %s', change, old_path, new_path)
194
178
the working directory; these should be removed from the
195
179
working inventory.
198
181
def __init__(self,
201
184
"""Create a Commit object.
203
186
:param reporter: the default reporter to use or None to decide later
205
188
self.reporter = reporter
206
self.config_stack = config_stack
209
def update_revprops(revprops, branch, authors=None,
210
local=False, possible_master_transports=None):
213
if possible_master_transports is None:
214
possible_master_transports = []
215
if (u'branch-nick' not in revprops and
216
branch.repository._format.supports_storing_branch_nick):
217
revprops[u'branch-nick'] = branch._get_nick(
219
possible_master_transports)
220
if authors is not None:
221
if u'author' in revprops or u'authors' in revprops:
222
# XXX: maybe we should just accept one of them?
223
raise AssertionError('author property given twice')
225
for individual in authors:
226
if '\n' in individual:
227
raise AssertionError('\\n is not a valid character '
228
'in an author identity')
229
revprops[u'authors'] = '\n'.join(authors)
282
237
:param exclude: None or a list of relative paths to exclude from the
283
238
commit. Pending changes to excluded files will be ignored by the
285
:param lossy: When committing to a foreign VCS, ignore any
286
data that can not be natively represented.
288
with ExitStack() as stack:
289
self.revprops = revprops or {}
290
# XXX: Can be set on __init__ or passed in - this is a bit ugly.
291
self.config_stack = config or self.config_stack
292
mutter('preparing to commit')
294
if working_tree is None:
295
raise BzrError("working_tree must be passed into commit().")
297
self.work_tree = working_tree
298
self.branch = self.work_tree.branch
299
if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
300
if not self.branch.repository.supports_rich_root():
301
raise errors.RootNotRich()
302
if message_callback is None:
303
if message is not None:
304
if isinstance(message, bytes):
305
message = message.decode(get_user_encoding())
307
def message_callback(x):
310
raise BzrError("The message or message_callback keyword"
311
" parameter is required for commit().")
313
self.bound_branch = None
314
self.any_entries_deleted = False
315
if exclude is not None:
316
self.exclude = sorted(
317
minimum_path_selection(exclude))
321
self.master_branch = None
322
self.recursive = recursive
324
# self.specific_files is None to indicate no filter, or any iterable to
325
# indicate a filter - [] means no files at all, as per iter_changes.
326
if specific_files is not None:
327
self.specific_files = sorted(
328
minimum_path_selection(specific_files))
330
self.specific_files = None
332
self.allow_pointless = allow_pointless
333
self.message_callback = message_callback
334
self.timestamp = timestamp
335
self.timezone = timezone
336
self.committer = committer
338
self.verbose = verbose
340
stack.enter_context(self.work_tree.lock_write())
341
self.parents = self.work_tree.get_parent_ids()
342
self.pb = ui.ui_factory.nested_progress_bar()
343
stack.callback(self.pb.finished)
344
self.basis_revid = self.work_tree.last_revision()
345
self.basis_tree = self.work_tree.basis_tree()
346
stack.enter_context(self.basis_tree.lock_read())
241
mutter('preparing to commit')
243
if working_tree is None:
244
raise BzrError("working_tree must be passed into commit().")
246
self.work_tree = working_tree
247
self.branch = self.work_tree.branch
248
if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
249
if not self.branch.repository.supports_rich_root():
250
raise errors.RootNotRich()
251
if message_callback is None:
252
if message is not None:
253
if isinstance(message, str):
254
message = message.decode(get_user_encoding())
255
message_callback = lambda x: message
257
raise BzrError("The message or message_callback keyword"
258
" parameter is required for commit().")
260
self.bound_branch = None
261
self.any_entries_deleted = False
262
if exclude is not None:
263
self.exclude = sorted(
264
minimum_path_selection(exclude))
268
self.master_branch = None
269
self.master_locked = False
270
self.recursive = recursive
272
if specific_files is not None:
273
self.specific_files = sorted(
274
minimum_path_selection(specific_files))
276
self.specific_files = None
278
self.allow_pointless = allow_pointless
279
self.revprops = revprops
280
self.message_callback = message_callback
281
self.timestamp = timestamp
282
self.timezone = timezone
283
self.committer = committer
285
self.verbose = verbose
287
self.work_tree.lock_write()
288
self.parents = self.work_tree.get_parent_ids()
289
# We can use record_iter_changes IFF iter_changes is compatible with
290
# the command line parameters, and the repository has fast delta
291
# generation. See bug 347649.
292
self.use_record_iter_changes = (
293
not self.specific_files and
295
not self.branch.repository._format.supports_tree_reference and
296
(self.branch.repository._format.fast_deltas or
297
len(self.parents) < 2))
298
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
299
self.basis_revid = self.work_tree.last_revision()
300
self.basis_tree = self.work_tree.basis_tree()
301
self.basis_tree.lock_read()
347
303
# Cannot commit with conflicts present.
348
304
if len(self.work_tree.conflicts()) > 0:
349
305
raise ConflictsInTree
351
307
# Setup the bound branch variables as needed.
352
self._check_bound_branch(stack, possible_master_transports)
353
if self.config_stack is None:
354
self.config_stack = self.work_tree.get_config_stack()
308
self._check_bound_branch(possible_master_transports)
356
310
# Check that the working tree is up to date
357
old_revno, old_revid, new_revno = self._check_out_of_date_tree()
311
old_revno, new_revno = self._check_out_of_date_tree()
359
313
# Complete configuration setup
360
314
if reporter is not None:
361
315
self.reporter = reporter
362
316
elif self.reporter is None:
363
317
self.reporter = self._select_reporter()
318
if self.config is None:
319
self.config = self.branch.get_config()
321
self._set_specific_file_ids()
365
323
# Setup the progress bar. As the number of files that need to be
366
324
# committed in unknown, progress is reported as stages.
370
328
self.pb_stage_count = 0
371
329
self.pb_stage_total = 5
372
330
if self.bound_branch:
373
# 2 extra stages: "Uploading data to master branch" and "Merging
374
# tags to master branch"
375
self.pb_stage_total += 2
331
self.pb_stage_total += 1
376
332
self.pb.show_pct = False
377
333
self.pb.show_spinner = False
378
334
self.pb.show_eta = False
379
335
self.pb.show_count = True
380
336
self.pb.show_bar = True
338
self._gather_parents()
382
339
# After a merge, a selected file commit is not supported.
383
340
# See 'bzr help merge' for an explanation as to why.
384
if len(self.parents) > 1 and self.specific_files is not None:
385
raise CannotCommitSelectedFileMerge(self.specific_files)
341
if len(self.parents) > 1 and self.specific_files:
342
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
386
343
# Excludes are a form of selected file commit.
387
344
if len(self.parents) > 1 and self.exclude:
388
raise CannotCommitSelectedFileMerge(self.exclude)
345
raise errors.CannotCommitSelectedFileMerge(self.exclude)
390
347
# Collect the changes
391
348
self._set_progress_stage("Collecting changes", counter=True)
393
self.builder = self.branch.get_commit_builder(
394
self.parents, self.config_stack, timestamp, timezone, committer,
395
self.revprops, rev_id, lossy=lossy)
397
if self.builder.updates_branch and self.bound_branch:
399
raise AssertionError(
400
"bound branches not supported for commit builders "
401
"that update the branch")
349
self.builder = self.branch.get_commit_builder(self.parents,
350
self.config, timestamp, timezone, committer, revprops, rev_id)
353
self.builder.will_record_deletes()
404
354
# find the location being committed to
405
355
if self.bound_branch:
406
356
master_location = self.master_branch.base
423
373
# Prompt the user for a commit message if none provided
424
374
message = message_callback(self)
425
375
self.message = message
376
self._escape_commit_message()
427
378
# Add revision data to the local branch
428
379
self.rev_id = self.builder.commit(self.message)
431
382
mutter("aborting commit write group because of exception:")
432
383
trace.log_exception_quietly()
384
note("aborting commit write group: %r" % (e,))
433
385
self.builder.abort()
436
self._update_branches(old_revno, old_revid, new_revno)
438
# Make the working tree be up to date with the branch. This
439
# includes automatic changes scheduled to be made to the tree, such
440
# as updating its basis and unversioning paths that were missing.
441
self.work_tree.unversion(self.deleted_paths)
442
self._set_progress_stage("Updating the working tree")
443
self.work_tree.update_basis_by_delta(self.rev_id,
444
self.builder.get_basis_delta())
445
self.reporter.completed(new_revno, self.rev_id)
446
self._process_post_hooks(old_revno, new_revno)
449
def _update_branches(self, old_revno, old_revid, new_revno):
450
"""Update the master and local branch to the new revision.
452
This will try to make sure that the master branch is updated
453
before the local branch.
455
:param old_revno: Revision number of master branch before the
457
:param old_revid: Tip of master branch before the commit
458
:param new_revno: Revision number of the new commit
460
if not self.builder.updates_branch:
461
388
self._process_pre_hooks(old_revno, new_revno)
463
390
# Upload revision data to the master.
466
393
self._set_progress_stage("Uploading data to master branch")
467
394
# 'commit' to the master first so a timeout here causes the
468
395
# local branch to be out of date
469
(new_revno, self.rev_id) = self.master_branch.import_last_revision_info_and_tags(
470
self.branch, new_revno, self.rev_id, lossy=self._lossy)
472
self.branch.fetch(self.master_branch, self.rev_id)
396
self.master_branch.import_last_revision_info(
397
self.branch.repository, new_revno, self.rev_id)
474
399
# and now do the commit locally.
475
if new_revno is None:
476
# Keep existing behaviour around ghosts
478
400
self.branch.set_last_revision_info(new_revno, self.rev_id)
481
self._process_pre_hooks(old_revno, new_revno)
482
except BaseException:
483
# The commit builder will already have updated the branch,
485
self.branch.set_last_revision_info(old_revno, old_revid)
488
# Merge local tags to remote
489
if self.bound_branch:
490
self._set_progress_stage("Merging tags to master branch")
491
tag_updates, tag_conflicts = self.branch.tags.merge_to(
492
self.master_branch.tags)
494
warning_lines = [' ' + name for name, _, _ in tag_conflicts]
495
note(gettext("Conflicting tags in bound branch:\n{0}".format(
496
"\n".join(warning_lines))))
402
# Make the working tree up to date with the branch
403
self._set_progress_stage("Updating the working tree")
404
self.work_tree.update_basis_by_delta(self.rev_id,
405
self.builder.get_basis_delta())
406
self.reporter.completed(new_revno, self.rev_id)
407
self._process_post_hooks(old_revno, new_revno)
498
412
def _select_reporter(self):
499
413
"""Select the CommitReporter to use."""
544
467
# Make sure the local branch is identical to the master
545
master_revid = self.master_branch.last_revision()
546
local_revid = self.branch.last_revision()
547
if local_revid != master_revid:
468
master_info = self.master_branch.last_revision_info()
469
local_info = self.branch.last_revision_info()
470
if local_info != master_info:
548
471
raise errors.BoundBranchOutOfDate(self.branch,
551
474
# Now things are ready to change the master branch
552
475
# so grab the lock
553
476
self.bound_branch = self.branch
554
stack.enter_context(self.master_branch.lock_write())
477
self.master_branch.lock_write()
478
self.master_locked = True
556
480
def _check_out_of_date_tree(self):
557
481
"""Check that the working tree is up to date.
559
:return: old_revision_number, old_revision_id, new_revision_number
483
:return: old_revision_number,new_revision_number tuple
563
486
first_tree_parent = self.work_tree.get_parent_ids()[0]
566
489
# this is so that we still consider the master branch
567
490
# - in a checkout scenario the tree may have no
568
491
# parents but the branch may do.
569
first_tree_parent = breezy.revision.NULL_REVISION
570
if (self.master_branch._format.stores_revno() or
571
self.config_stack.get('calculate_revnos')):
573
old_revno, master_last = self.master_branch.last_revision_info()
574
except errors.UnsupportedOperation:
575
master_last = self.master_branch.last_revision()
576
old_revno = self.branch.revision_id_to_revno(master_last)
578
master_last = self.master_branch.last_revision()
492
first_tree_parent = bzrlib.revision.NULL_REVISION
493
old_revno, master_last = self.master_branch.last_revision_info()
580
494
if master_last != first_tree_parent:
581
if master_last != breezy.revision.NULL_REVISION:
495
if master_last != bzrlib.revision.NULL_REVISION:
582
496
raise errors.OutOfDateTree(self.work_tree)
583
if (old_revno is not None and
584
self.branch.repository.has_revision(first_tree_parent)):
497
if self.branch.repository.has_revision(first_tree_parent):
585
498
new_revno = old_revno + 1
587
500
# ghost parents never appear in revision history.
589
return old_revno, master_last, new_revno
502
return old_revno,new_revno
591
504
def _process_pre_hooks(self, old_revno, new_revno):
592
505
"""Process any registered pre commit hooks."""
654
566
old_revno, old_revid, new_revno, self.rev_id,
655
567
tree_delta, future_tree)
570
"""Cleanup any open locks, progress bars etc."""
571
cleanups = [self._cleanup_bound_branch,
572
self.basis_tree.unlock,
573
self.work_tree.unlock,
575
found_exception = None
576
for cleanup in cleanups:
579
# we want every cleanup to run no matter what.
580
# so we have a catchall here, but we will raise the
581
# last encountered exception up the stack: and
582
# typically this will be useful enough.
585
if found_exception is not None:
586
# don't do a plan raise, because the last exception may have been
587
# trashed, e is our sure-to-work exception even though it loses the
588
# full traceback. XXX: RBC 20060421 perhaps we could check the
589
# exc_info and if its the same one do a plain raise otherwise
590
# 'raise e' as we do now.
593
def _cleanup_bound_branch(self):
594
"""Executed at the end of a try/finally to cleanup a bound branch.
596
If the branch wasn't bound, this is a no-op.
597
If it was, it resents self.branch to the local branch, instead
600
if not self.bound_branch:
602
if self.master_locked:
603
self.master_branch.unlock()
605
def _escape_commit_message(self):
606
"""Replace xml-incompatible control characters."""
607
# FIXME: RBC 20060419 this should be done by the revision
608
# serialiser not by commit. Then we can also add an unescaper
609
# in the deserializer and start roundtripping revision messages
610
# precisely. See repository_implementations/test_repository.py
611
self.message, escape_count = xml_serializer.escape_invalid_chars(
614
self.reporter.escaped(escape_count, self.message)
616
def _gather_parents(self):
617
"""Record the parents of a merge for merge detection."""
618
# TODO: Make sure that this list doesn't contain duplicate
619
# entries and the order is preserved when doing this.
620
if self.use_record_iter_changes:
622
self.basis_inv = self.basis_tree.inventory
623
self.parent_invs = [self.basis_inv]
624
for revision in self.parents[1:]:
625
if self.branch.repository.has_revision(revision):
626
mutter('commit parent revision {%s}', revision)
627
inventory = self.branch.repository.get_inventory(revision)
628
self.parent_invs.append(inventory)
630
mutter('commit parent ghost revision {%s}', revision)
657
632
def _update_builder_with_changes(self):
658
633
"""Update the commit builder with the data about what has changed.
660
specific_files = self.specific_files
661
mutter("Selecting files for commit with filter %r", specific_files)
635
exclude = self.exclude
636
specific_files = self.specific_files or []
637
mutter("Selecting files for commit with filter %s", specific_files)
663
639
self._check_strict()
664
iter_changes = self.work_tree.iter_changes(
665
self.basis_tree, specific_files=specific_files)
667
iter_changes = filter_excluded(iter_changes, self.exclude)
668
iter_changes = self._filter_iter_changes(iter_changes)
669
for path, fs_hash in self.builder.record_iter_changes(
640
if self.use_record_iter_changes:
641
iter_changes = self.work_tree.iter_changes(self.basis_tree)
642
iter_changes = self._filter_iter_changes(iter_changes)
643
for file_id, path, fs_hash in self.builder.record_iter_changes(
670
644
self.work_tree, self.basis_revid, iter_changes):
671
self.work_tree._observed_sha1(path, fs_hash)
645
self.work_tree._observed_sha1(file_id, path, fs_hash)
647
# Build the new inventory
648
self._populate_from_inventory()
649
self._record_unselected()
650
self._report_and_accumulate_deletes()
673
652
def _filter_iter_changes(self, iter_changes):
674
653
"""Process iter_changes.
676
This method reports on the changes in iter_changes to the user, and
655
This method reports on the changes in iter_changes to the user, and
677
656
converts 'missing' entries in the iter_changes iterator to 'deleted'
678
657
entries. 'missing' entries have their
683
662
reporter = self.reporter
684
663
report_changes = reporter.is_verbose()
686
665
for change in iter_changes:
687
666
if report_changes:
688
old_path = change.path[0]
689
new_path = change.path[1]
690
versioned = change.versioned[1]
691
kind = change.kind[1]
692
versioned = change.versioned[1]
667
old_path = change[1][0]
668
new_path = change[1][1]
669
versioned = change[3][1]
671
versioned = change[3][1]
693
672
if kind is None and versioned:
695
674
if report_changes:
696
675
reporter.missing(new_path)
697
if change.kind[0] == 'symlink' and not self.work_tree.supports_symlinks():
698
trace.warning('Ignoring "%s" as symlinks are not '
699
'supported on this filesystem.' % (change.path[0],))
701
deleted_paths.append(change.path[1])
676
deleted_ids.append(change[0])
702
677
# Reset the new path (None) and new versioned flag (False)
703
change = change.discard_new()
704
new_path = change.path[1]
678
change = (change[0], (change[1][0], None), change[2],
679
(change[3][0], False)) + change[4:]
706
680
elif kind == 'tree-reference':
707
681
if self.recursive == 'down':
708
self._commit_nested_tree(change.path[1])
709
if change.versioned[0] or change.versioned[1]:
682
self._commit_nested_tree(change[0], change[1][1])
683
if change[3][0] or change[3][1]:
711
685
if report_changes:
712
686
if new_path is None:
713
687
reporter.deleted(old_path)
714
688
elif old_path is None:
715
reporter.snapshot_change(gettext('added'), new_path)
689
reporter.snapshot_change('added', new_path)
716
690
elif old_path != new_path:
717
reporter.renamed(gettext('renamed'),
691
reporter.renamed('renamed', old_path, new_path)
721
or self.work_tree.branch.repository._format.rich_root_data):
694
self.work_tree.branch.repository._format.rich_root_data):
722
695
# Don't report on changes to '' in non rich root
724
reporter.snapshot_change(
725
gettext('modified'), new_path)
697
reporter.snapshot_change('modified', new_path)
726
698
self._next_progress_entry()
727
# Unversion files that were found to be deleted
728
self.deleted_paths = deleted_paths
699
# Unversion IDs that were found to be deleted
700
self.work_tree.unversion(deleted_ids)
702
def _record_unselected(self):
703
# If specific files are selected, then all un-selected files must be
704
# recorded in their previous state. For more details, see
705
# https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
706
if self.specific_files or self.exclude:
707
specific_files = self.specific_files or []
708
for path, old_ie in self.basis_inv.iter_entries():
709
if old_ie.file_id in self.builder.new_inventory:
710
# already added - skip.
712
if (is_inside_any(specific_files, path)
713
and not is_inside_any(self.exclude, path)):
714
# was inside the selected path, and not excluded - if not
715
# present it has been deleted so skip.
717
# From here down it was either not selected, or was excluded:
718
# We preserve the entry unaltered.
720
# Note: specific file commits after a merge are currently
721
# prohibited. This test is for sanity/safety in case it's
722
# required after that changes.
723
if len(self.parents) > 1:
725
self.builder.record_entry_contents(ie, self.parent_invs, path,
726
self.basis_tree, None)
728
def _report_and_accumulate_deletes(self):
729
if (isinstance(self.basis_inv, Inventory)
730
and isinstance(self.builder.new_inventory, Inventory)):
731
# the older Inventory classes provide a _byid dict, and building a
732
# set from the keys of this dict is substantially faster than even
733
# getting a set of ids from the inventory
735
# <lifeless> set(dict) is roughly the same speed as
736
# set(iter(dict)) and both are significantly slower than
738
deleted_ids = set(self.basis_inv._byid.keys()) - \
739
set(self.builder.new_inventory._byid.keys())
741
deleted_ids = set(self.basis_inv) - set(self.builder.new_inventory)
743
self.any_entries_deleted = True
744
deleted = [(self.basis_tree.id2path(file_id), file_id)
745
for file_id in deleted_ids]
747
# XXX: this is not quite directory-order sorting
748
for path, file_id in deleted:
749
self.builder.record_delete(path, file_id)
750
self.reporter.deleted(path)
730
752
def _check_strict(self):
731
753
# XXX: when we use iter_changes this would likely be faster if
736
758
for unknown in self.work_tree.unknowns():
737
759
raise StrictCommitFailed()
739
def _commit_nested_tree(self, path):
761
def _populate_from_inventory(self):
762
"""Populate the CommitBuilder by walking the working tree inventory."""
763
# Build the revision inventory.
765
# This starts by creating a new empty inventory. Depending on
766
# which files are selected for commit, and what is present in the
767
# current tree, the new inventory is populated. inventory entries
768
# which are candidates for modification have their revision set to
769
# None; inventory entries that are carried over untouched have their
770
# revision set to their prior value.
772
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
773
# results to create a new inventory at the same time, which results
774
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
777
specific_files = self.specific_files
778
exclude = self.exclude
779
report_changes = self.reporter.is_verbose()
781
# A tree of paths that have been deleted. E.g. if foo/bar has been
782
# deleted, then we have {'foo':{'bar':{}}}
784
# XXX: Note that entries may have the wrong kind because the entry does
785
# not reflect the status on disk.
786
work_inv = self.work_tree.inventory
787
# NB: entries will include entries within the excluded ids/paths
788
# because iter_entries_by_dir has no 'exclude' facility today.
789
entries = work_inv.iter_entries_by_dir(
790
specific_file_ids=self.specific_file_ids, yield_parents=True)
791
for path, existing_ie in entries:
792
file_id = existing_ie.file_id
793
name = existing_ie.name
794
parent_id = existing_ie.parent_id
795
kind = existing_ie.kind
796
# Skip files that have been deleted from the working tree.
797
# The deleted path ids are also recorded so they can be explicitly
800
path_segments = splitpath(path)
801
deleted_dict = deleted_paths
802
for segment in path_segments:
803
deleted_dict = deleted_dict.get(segment, None)
805
# We either took a path not present in the dict
806
# (deleted_dict was None), or we've reached an empty
807
# child dir in the dict, so are now a sub-path.
811
if deleted_dict is not None:
812
# the path has a deleted parent, do not add it.
814
if exclude and is_inside_any(exclude, path):
815
# Skip excluded paths. Excluded paths are processed by
816
# _update_builder_with_changes.
818
content_summary = self.work_tree.path_content_summary(path)
819
# Note that when a filter of specific files is given, we must only
820
# skip/record deleted files matching that filter.
821
if not specific_files or is_inside_any(specific_files, path):
822
if content_summary[0] == 'missing':
823
if not deleted_paths:
824
# path won't have been split yet.
825
path_segments = splitpath(path)
826
deleted_dict = deleted_paths
827
for segment in path_segments:
828
deleted_dict = deleted_dict.setdefault(segment, {})
829
self.reporter.missing(path)
830
self._next_progress_entry()
831
deleted_ids.append(file_id)
833
# TODO: have the builder do the nested commit just-in-time IF and
835
if content_summary[0] == 'tree-reference':
836
# enforce repository nested tree policy.
837
if (not self.work_tree.supports_tree_reference() or
838
# repository does not support it either.
839
not self.branch.repository._format.supports_tree_reference):
840
content_summary = ('directory',) + content_summary[1:]
841
kind = content_summary[0]
842
# TODO: specific_files filtering before nested tree processing
843
if kind == 'tree-reference':
844
if self.recursive == 'down':
845
nested_revision_id = self._commit_nested_tree(
847
content_summary = content_summary[:3] + (
850
content_summary = content_summary[:3] + (
851
self.work_tree.get_reference_revision(file_id),)
853
# Record an entry for this item
854
# Note: I don't particularly want to have the existing_ie
855
# parameter but the test suite currently (28-Jun-07) breaks
856
# without it thanks to a unicode normalisation issue. :-(
857
definitely_changed = kind != existing_ie.kind
858
self._record_entry(path, file_id, specific_files, kind, name,
859
parent_id, definitely_changed, existing_ie, report_changes,
862
# Unversion IDs that were found to be deleted
863
self.work_tree.unversion(deleted_ids)
865
def _commit_nested_tree(self, file_id, path):
740
866
"Commit a nested tree."
741
sub_tree = self.work_tree.get_nested_tree(path)
867
sub_tree = self.work_tree.get_nested_tree(file_id, path)
742
868
# FIXME: be more comprehensive here:
743
869
# this works when both trees are in --trees repository,
744
870
# but when both are bound to a different repository,
746
872
# finally implement the explicit-caches approach design
747
873
# a while back - RBC 20070306.
748
874
if sub_tree.branch.repository.has_same_location(
749
self.work_tree.branch.repository):
875
self.work_tree.branch.repository):
750
876
sub_tree.branch.repository = \
751
877
self.work_tree.branch.repository
753
879
return sub_tree.commit(message=None, revprops=self.revprops,
754
recursive=self.recursive,
755
message_callback=self.message_callback,
756
timestamp=self.timestamp,
757
timezone=self.timezone,
758
committer=self.committer,
759
allow_pointless=self.allow_pointless,
760
strict=self.strict, verbose=self.verbose,
761
local=self.local, reporter=self.reporter)
762
except PointlessCommit:
763
return self.work_tree.get_reference_revision(path)
880
recursive=self.recursive,
881
message_callback=self.message_callback,
882
timestamp=self.timestamp, timezone=self.timezone,
883
committer=self.committer,
884
allow_pointless=self.allow_pointless,
885
strict=self.strict, verbose=self.verbose,
886
local=self.local, reporter=self.reporter)
887
except errors.PointlessCommit:
888
return self.work_tree.get_reference_revision(file_id)
890
def _record_entry(self, path, file_id, specific_files, kind, name,
891
parent_id, definitely_changed, existing_ie, report_changes,
893
"Record the new inventory entry for a path if any."
894
# mutter('check %s {%s}', path, file_id)
895
# mutter('%s selected for commit', path)
896
if definitely_changed or existing_ie is None:
897
ie = make_entry(kind, name, parent_id, file_id)
899
ie = existing_ie.copy()
901
# For carried over entries we don't care about the fs hash - the repo
902
# isn't generating a sha, so we're not saving computation time.
903
_, _, fs_hash = self.builder.record_entry_contents(
904
ie, self.parent_invs, path, self.work_tree, content_summary)
906
self._report_change(ie, path)
908
self.work_tree._observed_sha1(ie.file_id, path, fs_hash)
911
def _report_change(self, ie, path):
912
"""Report a change to the user.
914
The change that has occurred is described relative to the basis
917
if (self.basis_inv.has_id(ie.file_id)):
918
basis_ie = self.basis_inv[ie.file_id]
921
change = ie.describe_change(basis_ie, ie)
922
if change in (InventoryEntry.RENAMED,
923
InventoryEntry.MODIFIED_AND_RENAMED):
924
old_path = self.basis_inv.id2path(ie.file_id)
925
self.reporter.renamed(change, old_path, path)
926
self._next_progress_entry()
928
if change == 'unchanged':
930
self.reporter.snapshot_change(change, path)
931
self._next_progress_entry()
765
933
def _set_progress_stage(self, name, counter=False):
766
934
"""Set the progress stage and emit an update to the progress bar."""
780
948
def _emit_progress(self):
781
949
if self.pb_entries_count is not None:
782
text = gettext("{0} [{1}] - Stage").format(self.pb_stage_name,
783
self.pb_entries_count)
950
text = "%s [%d] - Stage" % (self.pb_stage_name,
951
self.pb_entries_count)
785
text = gettext("%s - Stage") % (self.pb_stage_name, )
953
text = "%s - Stage" % (self.pb_stage_name, )
786
954
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
956
def _set_specific_file_ids(self):
957
"""populate self.specific_file_ids if we will use it."""
958
if not self.use_record_iter_changes:
959
# If provided, ensure the specified files are versioned
960
if self.specific_files is not None:
961
# Note: This routine is being called because it raises
962
# PathNotVersionedError as a side effect of finding the IDs. We
963
# later use the ids we found as input to the working tree
964
# inventory iterator, so we only consider those ids rather than
965
# examining the whole tree again.
966
# XXX: Dont we have filter_unversioned to do this more
968
self.specific_file_ids = tree.find_ids_across_trees(
969
self.specific_files, [self.basis_tree, self.work_tree])
971
self.specific_file_ids = None