14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
19
18
# The newly committed revision is going to have a shape corresponding
20
19
# to that of the working tree. Files that are not in the
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.
60
from brzlib.branch import Branch
61
from brzlib.cleanup import OperationWithCleanups
63
from brzlib.errors import (BzrError, PointlessCommit,
59
from bzrlib.branch import Branch
60
from bzrlib.cleanup import OperationWithCleanups
62
from bzrlib.errors import (BzrError, PointlessCommit,
67
from brzlib.osutils import (get_user_encoding,
66
from bzrlib.osutils import (get_user_encoding,
69
68
minimum_path_selection,
72
from brzlib.trace import mutter, note, is_quiet
73
from brzlib.inventory import Inventory, InventoryEntry, make_entry
74
from brzlib import symbol_versioning
75
from brzlib.urlutils import unescape_for_display
76
from brzlib.i18n import gettext
71
from bzrlib.trace import mutter, note, is_quiet
72
from bzrlib.inventory import Inventory, InventoryEntry, make_entry
73
from bzrlib import symbol_versioning
74
from bzrlib.urlutils import unescape_for_display
78
78
class NullCommitReporter(object):
79
79
"""I report on progress of a commit."""
114
114
note(format, *args)
116
116
def snapshot_change(self, change, path):
117
if path == '' and change in (gettext('added'), gettext('modified')):
117
if path == '' and change in ('added', 'modified'):
119
119
self._note("%s %s", change, path)
128
128
"to started.", DeprecationWarning,
131
self._note(gettext('Committing%s'), location)
131
self._note('Committing%s', location)
133
133
def completed(self, revno, rev_id):
134
self._note(gettext('Committed revision %d.'), revno)
134
self._note('Committed revision %d.', revno)
135
135
# self._note goes to the console too; so while we want to log the
136
136
# rev_id, we can't trivially only log it. (See bug 526425). Long
137
137
# term we should rearrange the reporting structure, but for now
140
140
mutter('Committed revid %s as revno %d.', rev_id, revno)
142
142
def deleted(self, path):
143
self._note(gettext('deleted %s'), path)
143
self._note('deleted %s', path)
145
145
def missing(self, path):
146
self._note(gettext('missing %s'), path)
146
self._note('missing %s', path)
148
148
def renamed(self, change, old_path, new_path):
149
149
self._note('%s %s => %s', change, old_path, new_path)
167
167
def __init__(self,
170
170
"""Create a Commit object.
172
172
:param reporter: the default reporter to use or None to decide later
174
174
self.reporter = reporter
175
self.config_stack = config_stack
178
def update_revprops(revprops, branch, authors=None, author=None,
179
local=False, possible_master_transports=None):
182
if possible_master_transports is None:
183
possible_master_transports = []
184
if not 'branch-nick' in revprops:
185
revprops['branch-nick'] = branch._get_nick(
187
possible_master_transports)
188
if authors is not None:
189
if author is not None:
190
raise AssertionError('Specifying both author and authors '
191
'is not allowed. Specify just authors instead')
192
if 'author' in revprops or 'authors' in revprops:
193
# XXX: maybe we should just accept one of them?
194
raise AssertionError('author property given twice')
196
for individual in authors:
197
if '\n' in individual:
198
raise AssertionError('\\n is not a valid character '
199
'in an author identity')
200
revprops['authors'] = '\n'.join(authors)
201
if author is not None:
202
symbol_versioning.warn('The parameter author was deprecated'
203
' in version 1.13. Use authors instead',
205
if 'author' in revprops or 'authors' in revprops:
206
# XXX: maybe we should just accept one of them?
207
raise AssertionError('author property given twice')
209
raise AssertionError('\\n is not a valid character '
210
'in an author identity')
211
revprops['authors'] = author
229
192
message_callback=None,
230
193
recursive='down',
232
possible_master_transports=None,
195
possible_master_transports=None):
234
196
"""Commit working copy as a new revision.
236
198
:param message: the commit message (it or message_callback is required)
263
225
:param exclude: None or a list of relative paths to exclude from the
264
226
commit. Pending changes to excluded files will be ignored by the
266
:param lossy: When committing to a foreign VCS, ignore any
267
data that can not be natively represented.
269
229
operation = OperationWithCleanups(self._commit)
270
230
self.revprops = revprops or {}
271
231
# XXX: Can be set on __init__ or passed in - this is a bit ugly.
272
self.config_stack = config or self.config_stack
232
self.config = config or self.config
273
233
return operation.run(
275
235
timestamp=timestamp,
286
246
message_callback=message_callback,
287
247
recursive=recursive,
289
possible_master_transports=possible_master_transports,
249
possible_master_transports=possible_master_transports)
292
251
def _commit(self, operation, message, timestamp, timezone, committer,
293
252
specific_files, rev_id, allow_pointless, strict, verbose,
294
253
working_tree, local, reporter, message_callback, recursive,
295
exclude, possible_master_transports, lossy):
254
exclude, possible_master_transports):
296
255
mutter('preparing to commit')
298
257
if working_tree is None:
330
289
minimum_path_selection(specific_files))
332
291
self.specific_files = None
334
293
self.allow_pointless = allow_pointless
335
294
self.message_callback = message_callback
336
295
self.timestamp = timestamp
350
309
not self.branch.repository._format.supports_tree_reference and
351
310
(self.branch.repository._format.fast_deltas or
352
311
len(self.parents) < 2))
353
self.pb = ui.ui_factory.nested_progress_bar()
312
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
354
313
operation.add_cleanup(self.pb.finished)
355
314
self.basis_revid = self.work_tree.last_revision()
356
315
self.basis_tree = self.work_tree.basis_tree()
364
323
self._check_bound_branch(operation, possible_master_transports)
366
325
# Check that the working tree is up to date
367
old_revno, old_revid, new_revno = self._check_out_of_date_tree()
326
old_revno, new_revno = self._check_out_of_date_tree()
369
328
# Complete configuration setup
370
329
if reporter is not None:
371
330
self.reporter = reporter
372
331
elif self.reporter is None:
373
332
self.reporter = self._select_reporter()
374
if self.config_stack is None:
375
self.config_stack = self.work_tree.get_config_stack()
333
if self.config is None:
334
self.config = self.branch.get_config()
377
336
self._set_specific_file_ids()
405
362
# Collect the changes
406
363
self._set_progress_stage("Collecting changes", counter=True)
408
364
self.builder = self.branch.get_commit_builder(self.parents,
409
self.config_stack, timestamp, timezone, committer, self.revprops,
411
if not self.builder.supports_record_entry_contents and self.exclude:
413
raise errors.ExcludesUnsupported(self.branch.repository)
415
if self.builder.updates_branch and self.bound_branch:
417
raise AssertionError(
418
"bound branches not supported for commit builders "
419
"that update the branch")
365
self.config, timestamp, timezone, committer, self.revprops, rev_id)
422
368
self.builder.will_record_deletes()
449
395
except Exception, e:
450
396
mutter("aborting commit write group because of exception:")
451
397
trace.log_exception_quietly()
398
note("aborting commit write group: %r" % (e,))
452
399
self.builder.abort()
455
self._update_branches(old_revno, old_revid, new_revno)
402
self._process_pre_hooks(old_revno, new_revno)
404
# Upload revision data to the master.
405
# this will propagate merged revisions too if needed.
406
if self.bound_branch:
407
self._set_progress_stage("Uploading data to master branch")
408
# 'commit' to the master first so a timeout here causes the
409
# local branch to be out of date
410
self.master_branch.import_last_revision_info(
411
self.branch.repository, new_revno, self.rev_id)
413
# and now do the commit locally.
414
self.branch.set_last_revision_info(new_revno, self.rev_id)
457
416
# Make the working tree be up to date with the branch. This
458
417
# includes automatic changes scheduled to be made to the tree, such
465
424
self._process_post_hooks(old_revno, new_revno)
466
425
return self.rev_id
468
def _update_branches(self, old_revno, old_revid, new_revno):
469
"""Update the master and local branch to the new revision.
471
This will try to make sure that the master branch is updated
472
before the local branch.
474
:param old_revno: Revision number of master branch before the
476
:param old_revid: Tip of master branch before the commit
477
:param new_revno: Revision number of the new commit
479
if not self.builder.updates_branch:
480
self._process_pre_hooks(old_revno, new_revno)
482
# Upload revision data to the master.
483
# this will propagate merged revisions too if needed.
484
if self.bound_branch:
485
self._set_progress_stage("Uploading data to master branch")
486
# 'commit' to the master first so a timeout here causes the
487
# local branch to be out of date
488
(new_revno, self.rev_id) = self.master_branch.import_last_revision_info_and_tags(
489
self.branch, new_revno, self.rev_id, lossy=self._lossy)
491
self.branch.fetch(self.master_branch, self.rev_id)
493
# and now do the commit locally.
494
self.branch.set_last_revision_info(new_revno, self.rev_id)
497
self._process_pre_hooks(old_revno, new_revno)
499
# The commit builder will already have updated the branch,
501
self.branch.set_last_revision_info(old_revno, old_revid)
504
# Merge local tags to remote
505
if self.bound_branch:
506
self._set_progress_stage("Merging tags to master branch")
507
tag_updates, tag_conflicts = self.branch.tags.merge_to(
508
self.master_branch.tags)
510
warning_lines = [' ' + name for name, _, _ in tag_conflicts]
511
note( gettext("Conflicting tags in bound branch:\n{0}".format(
512
"\n".join(warning_lines))) )
514
427
def _select_reporter(self):
515
428
"""Select the CommitReporter to use."""
523
436
# A merge with no effect on files
524
437
if len(self.parents) > 1:
439
# TODO: we could simplify this by using self.builder.basis_delta.
441
# The initial commit adds a root directory, but this in itself is not
442
# a worthwhile commit.
443
if (self.basis_revid == revision.NULL_REVISION and
444
((self.builder.new_inventory is not None and
445
len(self.builder.new_inventory) == 1) or
446
len(self.builder._basis_delta) == 1)):
447
raise PointlessCommit()
526
448
if self.builder.any_changes():
528
450
raise PointlessCommit()
573
495
def _check_out_of_date_tree(self):
574
496
"""Check that the working tree is up to date.
576
:return: old_revision_number, old_revision_id, new_revision_number
498
:return: old_revision_number,new_revision_number tuple
580
501
first_tree_parent = self.work_tree.get_parent_ids()[0]
583
504
# this is so that we still consider the master branch
584
505
# - in a checkout scenario the tree may have no
585
506
# parents but the branch may do.
586
first_tree_parent = brzlib.revision.NULL_REVISION
507
first_tree_parent = bzrlib.revision.NULL_REVISION
587
508
old_revno, master_last = self.master_branch.last_revision_info()
588
509
if master_last != first_tree_parent:
589
if master_last != brzlib.revision.NULL_REVISION:
510
if master_last != bzrlib.revision.NULL_REVISION:
590
511
raise errors.OutOfDateTree(self.work_tree)
591
512
if self.branch.repository.has_revision(first_tree_parent):
592
513
new_revno = old_revno + 1
594
515
# ghost parents never appear in revision history.
596
return old_revno, master_last, new_revno
517
return old_revno,new_revno
598
519
def _process_pre_hooks(self, old_revno, new_revno):
599
520
"""Process any registered pre commit hooks."""
605
526
# Process the post commit hooks, if any
606
527
self._set_progress_stage("Running post_commit hooks")
607
528
# old style commit hooks - should be deprecated ? (obsoleted in
608
# 0.15^H^H^H^H 2.5.0)
609
post_commit = self.config_stack.get('post_commit')
610
if post_commit is not None:
611
hooks = post_commit.split(' ')
530
if self.config.post_commit() is not None:
531
hooks = self.config.post_commit().split(' ')
612
532
# this would be nicer with twisted.python.reflect.namedAny
613
533
for hook in hooks:
614
534
result = eval(hook + '(branch, rev_id)',
615
535
{'branch':self.branch,
617
537
'rev_id':self.rev_id})
618
538
# process new style post commit hooks
619
539
self._process_hooks("post_commit", old_revno, new_revno)
636
556
old_revid = self.parents[0]
638
old_revid = brzlib.revision.NULL_REVISION
558
old_revid = bzrlib.revision.NULL_REVISION
640
560
if hook_name == "pre_commit":
641
561
future_tree = self.builder.revision_tree()
667
587
# entries and the order is preserved when doing this.
668
588
if self.use_record_iter_changes:
670
self.basis_inv = self.basis_tree.root_inventory
590
self.basis_inv = self.basis_tree.inventory
671
591
self.parent_invs = [self.basis_inv]
672
592
for revision in self.parents[1:]:
673
593
if self.branch.repository.has_revision(revision):
726
646
# Reset the new path (None) and new versioned flag (False)
727
647
change = (change[0], (change[1][0], None), change[2],
728
648
(change[3][0], False)) + change[4:]
729
new_path = change[1][1]
731
649
elif kind == 'tree-reference':
732
650
if self.recursive == 'down':
733
651
self._commit_nested_tree(change[0], change[1][1])
737
655
if new_path is None:
738
656
reporter.deleted(old_path)
739
657
elif old_path is None:
740
reporter.snapshot_change(gettext('added'), new_path)
658
reporter.snapshot_change('added', new_path)
741
659
elif old_path != new_path:
742
reporter.renamed(gettext('renamed'), old_path, new_path)
660
reporter.renamed('renamed', old_path, new_path)
745
663
self.work_tree.branch.repository._format.rich_root_data):
746
664
# Don't report on changes to '' in non rich root
748
reporter.snapshot_change(gettext('modified'), new_path)
666
reporter.snapshot_change('modified', new_path)
749
667
self._next_progress_entry()
750
668
# Unversion IDs that were found to be deleted
751
669
self.deleted_ids = deleted_ids
757
675
if self.specific_files or self.exclude:
758
676
specific_files = self.specific_files or []
759
677
for path, old_ie in self.basis_inv.iter_entries():
760
if self.builder.new_inventory.has_id(old_ie.file_id):
678
if old_ie.file_id in self.builder.new_inventory:
761
679
# already added - skip.
763
681
if (is_inside_any(specific_files, path)
834
752
deleted_paths = {}
835
753
# XXX: Note that entries may have the wrong kind because the entry does
836
754
# not reflect the status on disk.
755
work_inv = self.work_tree.inventory
837
756
# NB: entries will include entries within the excluded ids/paths
838
757
# because iter_entries_by_dir has no 'exclude' facility today.
839
entries = self.work_tree.iter_entries_by_dir(
758
entries = work_inv.iter_entries_by_dir(
840
759
specific_file_ids=self.specific_file_ids, yield_parents=True)
841
760
for path, existing_ie in entries:
842
761
file_id = existing_ie.file_id
973
892
self.reporter.renamed(change, old_path, path)
974
893
self._next_progress_entry()
976
if change == gettext('unchanged'):
895
if change == 'unchanged':
978
897
self.reporter.snapshot_change(change, path)
979
898
self._next_progress_entry()
996
915
def _emit_progress(self):
997
916
if self.pb_entries_count is not None:
998
text = gettext("{0} [{1}] - Stage").format(self.pb_stage_name,
917
text = "%s [%d] - Stage" % (self.pb_stage_name,
999
918
self.pb_entries_count)
1001
text = gettext("%s - Stage") % (self.pb_stage_name, )
920
text = "%s - Stage" % (self.pb_stage_name, )
1002
921
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
1004
923
def _set_specific_file_ids(self):