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
18
19
# The newly committed revision is going to have a shape corresponding
19
20
# to that of the working tree. Files that are not in the
49
50
# TODO: Change the parameter 'rev_id' to 'revision_id' to be consistent with
50
51
# the rest of the code; add a deprecation of the old name.
59
from bzrlib.branch import Branch
60
from bzrlib.cleanup import OperationWithCleanups
62
from bzrlib.errors import (BzrError, PointlessCommit,
60
from brzlib.branch import Branch
61
from brzlib.cleanup import OperationWithCleanups
63
from brzlib.errors import (BzrError, PointlessCommit,
66
from bzrlib.osutils import (get_user_encoding,
67
from brzlib.osutils import (get_user_encoding,
68
69
minimum_path_selection,
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
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
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 ('added', 'modified'):
117
if path == '' and change in (gettext('added'), gettext('modified')):
119
119
self._note("%s %s", change, path)
128
128
"to started.", DeprecationWarning,
131
self._note('Committing%s', location)
131
self._note(gettext('Committing%s'), location)
133
133
def completed(self, revno, rev_id):
134
self._note('Committed revision %d.', revno)
134
self._note(gettext('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('deleted %s', path)
143
self._note(gettext('deleted %s'), path)
145
145
def missing(self, path):
146
self._note('missing %s', path)
146
self._note(gettext('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
192
229
message_callback=None,
193
230
recursive='down',
195
possible_master_transports=None):
232
possible_master_transports=None,
196
234
"""Commit working copy as a new revision.
198
236
:param message: the commit message (it or message_callback is required)
225
263
:param exclude: None or a list of relative paths to exclude from the
226
264
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.
229
269
operation = OperationWithCleanups(self._commit)
230
270
self.revprops = revprops or {}
231
271
# XXX: Can be set on __init__ or passed in - this is a bit ugly.
232
self.config = config or self.config
272
self.config_stack = config or self.config_stack
233
273
return operation.run(
235
275
timestamp=timestamp,
246
286
message_callback=message_callback,
247
287
recursive=recursive,
249
possible_master_transports=possible_master_transports)
289
possible_master_transports=possible_master_transports,
251
292
def _commit(self, operation, message, timestamp, timezone, committer,
252
293
specific_files, rev_id, allow_pointless, strict, verbose,
253
294
working_tree, local, reporter, message_callback, recursive,
254
exclude, possible_master_transports):
295
exclude, possible_master_transports, lossy):
255
296
mutter('preparing to commit')
257
298
if working_tree is None:
289
330
minimum_path_selection(specific_files))
291
332
self.specific_files = None
293
334
self.allow_pointless = allow_pointless
294
335
self.message_callback = message_callback
295
336
self.timestamp = timestamp
309
350
not self.branch.repository._format.supports_tree_reference and
310
351
(self.branch.repository._format.fast_deltas or
311
352
len(self.parents) < 2))
312
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
353
self.pb = ui.ui_factory.nested_progress_bar()
313
354
operation.add_cleanup(self.pb.finished)
314
355
self.basis_revid = self.work_tree.last_revision()
315
356
self.basis_tree = self.work_tree.basis_tree()
323
364
self._check_bound_branch(operation, possible_master_transports)
325
366
# Check that the working tree is up to date
326
old_revno, new_revno = self._check_out_of_date_tree()
367
old_revno, old_revid, new_revno = self._check_out_of_date_tree()
328
369
# Complete configuration setup
329
370
if reporter is not None:
330
371
self.reporter = reporter
331
372
elif self.reporter is None:
332
373
self.reporter = self._select_reporter()
333
if self.config is None:
334
self.config = self.branch.get_config()
374
if self.config_stack is None:
375
self.config_stack = self.work_tree.get_config_stack()
336
377
self._set_specific_file_ids()
362
405
# Collect the changes
363
406
self._set_progress_stage("Collecting changes", counter=True)
364
408
self.builder = self.branch.get_commit_builder(self.parents,
365
self.config, timestamp, timezone, committer, self.revprops, rev_id)
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")
368
422
self.builder.will_record_deletes()
395
449
except Exception, e:
396
450
mutter("aborting commit write group because of exception:")
397
451
trace.log_exception_quietly()
398
note("aborting commit write group: %r" % (e,))
399
452
self.builder.abort()
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)
455
self._update_branches(old_revno, old_revid, new_revno)
416
457
# Make the working tree be up to date with the branch. This
417
458
# includes automatic changes scheduled to be made to the tree, such
424
465
self._process_post_hooks(old_revno, new_revno)
425
466
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))) )
427
514
def _select_reporter(self):
428
515
"""Select the CommitReporter to use."""
436
523
# A merge with no effect on files
437
524
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()
448
526
if self.builder.any_changes():
450
528
raise PointlessCommit()
495
573
def _check_out_of_date_tree(self):
496
574
"""Check that the working tree is up to date.
498
:return: old_revision_number,new_revision_number tuple
576
:return: old_revision_number, old_revision_id, new_revision_number
501
580
first_tree_parent = self.work_tree.get_parent_ids()[0]
504
583
# this is so that we still consider the master branch
505
584
# - in a checkout scenario the tree may have no
506
585
# parents but the branch may do.
507
first_tree_parent = bzrlib.revision.NULL_REVISION
586
first_tree_parent = brzlib.revision.NULL_REVISION
508
587
old_revno, master_last = self.master_branch.last_revision_info()
509
588
if master_last != first_tree_parent:
510
if master_last != bzrlib.revision.NULL_REVISION:
589
if master_last != brzlib.revision.NULL_REVISION:
511
590
raise errors.OutOfDateTree(self.work_tree)
512
591
if self.branch.repository.has_revision(first_tree_parent):
513
592
new_revno = old_revno + 1
515
594
# ghost parents never appear in revision history.
517
return old_revno,new_revno
596
return old_revno, master_last, new_revno
519
598
def _process_pre_hooks(self, old_revno, new_revno):
520
599
"""Process any registered pre commit hooks."""
526
605
# Process the post commit hooks, if any
527
606
self._set_progress_stage("Running post_commit hooks")
528
607
# old style commit hooks - should be deprecated ? (obsoleted in
530
if self.config.post_commit() is not None:
531
hooks = self.config.post_commit().split(' ')
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(' ')
532
612
# this would be nicer with twisted.python.reflect.namedAny
533
613
for hook in hooks:
534
614
result = eval(hook + '(branch, rev_id)',
535
615
{'branch':self.branch,
537
617
'rev_id':self.rev_id})
538
618
# process new style post commit hooks
539
619
self._process_hooks("post_commit", old_revno, new_revno)
556
636
old_revid = self.parents[0]
558
old_revid = bzrlib.revision.NULL_REVISION
638
old_revid = brzlib.revision.NULL_REVISION
560
640
if hook_name == "pre_commit":
561
641
future_tree = self.builder.revision_tree()
587
667
# entries and the order is preserved when doing this.
588
668
if self.use_record_iter_changes:
590
self.basis_inv = self.basis_tree.inventory
670
self.basis_inv = self.basis_tree.root_inventory
591
671
self.parent_invs = [self.basis_inv]
592
672
for revision in self.parents[1:]:
593
673
if self.branch.repository.has_revision(revision):
646
726
# Reset the new path (None) and new versioned flag (False)
647
727
change = (change[0], (change[1][0], None), change[2],
648
728
(change[3][0], False)) + change[4:]
729
new_path = change[1][1]
649
731
elif kind == 'tree-reference':
650
732
if self.recursive == 'down':
651
733
self._commit_nested_tree(change[0], change[1][1])
655
737
if new_path is None:
656
738
reporter.deleted(old_path)
657
739
elif old_path is None:
658
reporter.snapshot_change('added', new_path)
740
reporter.snapshot_change(gettext('added'), new_path)
659
741
elif old_path != new_path:
660
reporter.renamed('renamed', old_path, new_path)
742
reporter.renamed(gettext('renamed'), old_path, new_path)
663
745
self.work_tree.branch.repository._format.rich_root_data):
664
746
# Don't report on changes to '' in non rich root
666
reporter.snapshot_change('modified', new_path)
748
reporter.snapshot_change(gettext('modified'), new_path)
667
749
self._next_progress_entry()
668
750
# Unversion IDs that were found to be deleted
669
751
self.deleted_ids = deleted_ids
675
757
if self.specific_files or self.exclude:
676
758
specific_files = self.specific_files or []
677
759
for path, old_ie in self.basis_inv.iter_entries():
678
if old_ie.file_id in self.builder.new_inventory:
760
if self.builder.new_inventory.has_id(old_ie.file_id):
679
761
# already added - skip.
681
763
if (is_inside_any(specific_files, path)
752
834
deleted_paths = {}
753
835
# XXX: Note that entries may have the wrong kind because the entry does
754
836
# not reflect the status on disk.
755
work_inv = self.work_tree.inventory
756
837
# NB: entries will include entries within the excluded ids/paths
757
838
# because iter_entries_by_dir has no 'exclude' facility today.
758
entries = work_inv.iter_entries_by_dir(
839
entries = self.work_tree.iter_entries_by_dir(
759
840
specific_file_ids=self.specific_file_ids, yield_parents=True)
760
841
for path, existing_ie in entries:
761
842
file_id = existing_ie.file_id
892
973
self.reporter.renamed(change, old_path, path)
893
974
self._next_progress_entry()
895
if change == 'unchanged':
976
if change == gettext('unchanged'):
897
978
self.reporter.snapshot_change(change, path)
898
979
self._next_progress_entry()
915
996
def _emit_progress(self):
916
997
if self.pb_entries_count is not None:
917
text = "%s [%d] - Stage" % (self.pb_stage_name,
998
text = gettext("{0} [{1}] - Stage").format(self.pb_stage_name,
918
999
self.pb_entries_count)
920
text = "%s - Stage" % (self.pb_stage_name, )
1001
text = gettext("%s - Stage") % (self.pb_stage_name, )
921
1002
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
923
1004
def _set_specific_file_ids(self):