1
# Copyright (C) 2005-2011 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
19
# The newly committed revision is going to have a shape corresponding
20
# to that of the working tree. Files that are not in the
21
# working tree and that were in the predecessor are reported as
22
# removed --- this can include files that were either removed from the
23
# inventory or deleted in the working tree. If they were only
24
# deleted from disk, they are removed from the working inventory.
26
# We then consider the remaining entries, which will be in the new
27
# version. Directory entries are simply copied across. File entries
28
# must be checked to see if a new version of the file should be
29
# recorded. For each parent revision tree, we check to see what
30
# version of the file was present. If the file was present in at
31
# least one tree, and if it was the same version in all the trees,
32
# then we can just refer to that version. Otherwise, a new version
33
# representing the merger of the file versions must be added.
35
# TODO: Update hashcache before and after - or does the WorkingTree
38
# TODO: Rather than mashing together the ancestry and storing it back,
39
# perhaps the weave should have single method which does it all in one
40
# go, avoiding a lot of redundant work.
42
# TODO: Perhaps give a warning if one of the revisions marked as
43
# merged is already in the ancestry, and then don't record it as a
46
# TODO: If the file is newly merged but unchanged from the version it
47
# merges from, then it should still be reported as newly added
48
# relative to the basis revision.
50
# TODO: Change the parameter 'rev_id' to 'revision_id' to be consistent with
51
# the rest of the code; add a deprecation of the old name.
59
from .branch import Branch
60
from .cleanup import OperationWithCleanups
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?
116
class NullCommitReporter(object):
117
"""I report on progress of a commit."""
119
def started(self, revno, revid, location):
122
def snapshot_change(self, change, path):
125
def completed(self, revno, rev_id):
128
def deleted(self, path):
131
def missing(self, path):
134
def renamed(self, change, old_path, new_path):
137
def is_verbose(self):
141
class ReportCommitToLog(NullCommitReporter):
143
def _note(self, format, *args):
146
Subclasses may choose to override this method.
150
def snapshot_change(self, change, path):
151
if path == '' and change in (gettext('added'), gettext('modified')):
153
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'))
160
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)
172
def deleted(self, path):
173
self._note(gettext('deleted %s'), path)
175
def missing(self, path):
176
self._note(gettext('missing %s'), path)
178
def renamed(self, change, old_path, new_path):
179
self._note('%s %s => %s', change, old_path, new_path)
181
def is_verbose(self):
185
class Commit(object):
186
"""Task of committing a new revision.
188
This is a MethodObject: it accumulates state as the commit is
189
prepared, and then it is discarded. It doesn't represent
190
historical revisions, just the act of recording a new one.
193
Modified to hold a list of files that have been deleted from
194
the working directory; these should be removed from the
201
"""Create a Commit object.
203
:param reporter: the default reporter to use or None to decide later
205
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)
239
allow_pointless=True,
247
message_callback=None,
250
possible_master_transports=None,
252
"""Commit working copy as a new revision.
254
:param message: the commit message (it or message_callback is required)
255
:param message_callback: A callback: message =
256
message_callback(cmt_obj)
258
:param timestamp: if not None, seconds-since-epoch for a
259
postdated/predated commit.
261
:param specific_files: If not None, commit only those files. An empty
262
list means 'commit no files'.
264
:param rev_id: If set, use this as the new revision id.
265
Useful for test or import commands that need to tightly
266
control what revisions are assigned. If you duplicate
267
a revision id that exists elsewhere it is your own fault.
268
If null (default), a time/random revision id is generated.
270
:param allow_pointless: If true (default), commit even if nothing
271
has changed and no merges are recorded.
273
:param strict: If true, don't allow a commit if the working tree
274
contains unknown files.
276
:param revprops: Properties for new revision
277
:param local: Perform a local only commit.
278
:param reporter: the reporter to use or None for the default
279
:param verbose: if True and the reporter is not None, report everything
280
:param recursive: If set to 'down', commit in any subtrees that have
281
pending changes of any sort during this commit.
282
:param exclude: None or a list of relative paths to exclude from the
283
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
operation = OperationWithCleanups(self._commit)
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
return operation.run(
297
specific_files=specific_files,
299
allow_pointless=allow_pointless,
302
working_tree=working_tree,
305
message_callback=message_callback,
308
possible_master_transports=possible_master_transports,
311
def _commit(self, operation, message, timestamp, timezone, committer,
312
specific_files, rev_id, allow_pointless, strict, verbose,
313
working_tree, local, reporter, message_callback, recursive,
314
exclude, possible_master_transports, lossy):
315
mutter('preparing to commit')
317
if working_tree is None:
318
raise BzrError("working_tree must be passed into commit().")
320
self.work_tree = working_tree
321
self.branch = self.work_tree.branch
322
if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
323
if not self.branch.repository.supports_rich_root():
324
raise errors.RootNotRich()
325
if message_callback is None:
326
if message is not None:
327
if isinstance(message, bytes):
328
message = message.decode(get_user_encoding())
330
def message_callback(x):
333
raise BzrError("The message or message_callback keyword"
334
" parameter is required for commit().")
336
self.bound_branch = None
337
self.any_entries_deleted = False
338
if exclude is not None:
339
self.exclude = sorted(
340
minimum_path_selection(exclude))
344
self.master_branch = None
345
self.recursive = recursive
347
# self.specific_files is None to indicate no filter, or any iterable to
348
# indicate a filter - [] means no files at all, as per iter_changes.
349
if specific_files is not None:
350
self.specific_files = sorted(
351
minimum_path_selection(specific_files))
353
self.specific_files = None
355
self.allow_pointless = allow_pointless
356
self.message_callback = message_callback
357
self.timestamp = timestamp
358
self.timezone = timezone
359
self.committer = committer
361
self.verbose = verbose
363
self.work_tree.lock_write()
364
operation.add_cleanup(self.work_tree.unlock)
365
self.parents = self.work_tree.get_parent_ids()
366
self.pb = ui.ui_factory.nested_progress_bar()
367
operation.add_cleanup(self.pb.finished)
368
self.basis_revid = self.work_tree.last_revision()
369
self.basis_tree = self.work_tree.basis_tree()
370
self.basis_tree.lock_read()
371
operation.add_cleanup(self.basis_tree.unlock)
372
# Cannot commit with conflicts present.
373
if len(self.work_tree.conflicts()) > 0:
374
raise ConflictsInTree
376
# Setup the bound branch variables as needed.
377
self._check_bound_branch(operation, possible_master_transports)
379
if self.config_stack is None:
380
self.config_stack = self.work_tree.get_config_stack()
382
# Check that the working tree is up to date
383
old_revno, old_revid, new_revno = self._check_out_of_date_tree()
385
# Complete configuration setup
386
if reporter is not None:
387
self.reporter = reporter
388
elif self.reporter is None:
389
self.reporter = self._select_reporter()
391
# Setup the progress bar. As the number of files that need to be
392
# committed in unknown, progress is reported as stages.
393
# We keep track of entries separately though and include that
394
# information in the progress bar during the relevant stages.
395
self.pb_stage_name = ""
396
self.pb_stage_count = 0
397
self.pb_stage_total = 5
398
if self.bound_branch:
399
# 2 extra stages: "Uploading data to master branch" and "Merging
400
# tags to master branch"
401
self.pb_stage_total += 2
402
self.pb.show_pct = False
403
self.pb.show_spinner = False
404
self.pb.show_eta = False
405
self.pb.show_count = True
406
self.pb.show_bar = True
408
# After a merge, a selected file commit is not supported.
409
# See 'bzr help merge' for an explanation as to why.
410
if len(self.parents) > 1 and self.specific_files is not None:
411
raise CannotCommitSelectedFileMerge(self.specific_files)
412
# Excludes are a form of selected file commit.
413
if len(self.parents) > 1 and self.exclude:
414
raise CannotCommitSelectedFileMerge(self.exclude)
416
# Collect the changes
417
self._set_progress_stage("Collecting changes", counter=True)
419
self.builder = self.branch.get_commit_builder(
420
self.parents, self.config_stack, timestamp, timezone, committer,
421
self.revprops, rev_id, lossy=lossy)
423
if self.builder.updates_branch and self.bound_branch:
425
raise AssertionError(
426
"bound branches not supported for commit builders "
427
"that update the branch")
430
# find the location being committed to
431
if self.bound_branch:
432
master_location = self.master_branch.base
434
master_location = self.branch.base
436
# report the start of the commit
437
self.reporter.started(new_revno, self.rev_id, master_location)
439
self._update_builder_with_changes()
440
self._check_pointless()
442
# TODO: Now the new inventory is known, check for conflicts.
443
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
444
# weave lines, because nothing should be recorded until it is known
445
# that commit will succeed.
446
self._set_progress_stage("Saving data locally")
447
self.builder.finish_inventory()
449
# Prompt the user for a commit message if none provided
450
message = message_callback(self)
451
self.message = message
453
# Add revision data to the local branch
454
self.rev_id = self.builder.commit(self.message)
457
mutter("aborting commit write group because of exception:")
458
trace.log_exception_quietly()
462
self._update_branches(old_revno, old_revid, new_revno)
464
# Make the working tree be up to date with the branch. This
465
# includes automatic changes scheduled to be made to the tree, such
466
# as updating its basis and unversioning paths that were missing.
467
self.work_tree.unversion(self.deleted_paths)
468
self._set_progress_stage("Updating the working tree")
469
self.work_tree.update_basis_by_delta(self.rev_id,
470
self.builder.get_basis_delta())
471
self.reporter.completed(new_revno, self.rev_id)
472
self._process_post_hooks(old_revno, new_revno)
475
def _update_branches(self, old_revno, old_revid, new_revno):
476
"""Update the master and local branch to the new revision.
478
This will try to make sure that the master branch is updated
479
before the local branch.
481
:param old_revno: Revision number of master branch before the
483
:param old_revid: Tip of master branch before the commit
484
:param new_revno: Revision number of the new commit
486
if not self.builder.updates_branch:
487
self._process_pre_hooks(old_revno, new_revno)
489
# Upload revision data to the master.
490
# this will propagate merged revisions too if needed.
491
if self.bound_branch:
492
self._set_progress_stage("Uploading data to master branch")
493
# 'commit' to the master first so a timeout here causes the
494
# local branch to be out of date
495
(new_revno, self.rev_id) = self.master_branch.import_last_revision_info_and_tags(
496
self.branch, new_revno, self.rev_id, lossy=self._lossy)
498
self.branch.fetch(self.master_branch, self.rev_id)
500
# and now do the commit locally.
501
if new_revno is None:
502
# Keep existing behaviour around ghosts
504
self.branch.set_last_revision_info(new_revno, self.rev_id)
507
self._process_pre_hooks(old_revno, new_revno)
508
except BaseException:
509
# The commit builder will already have updated the branch,
511
self.branch.set_last_revision_info(old_revno, old_revid)
514
# Merge local tags to remote
515
if self.bound_branch:
516
self._set_progress_stage("Merging tags to master branch")
517
tag_updates, tag_conflicts = self.branch.tags.merge_to(
518
self.master_branch.tags)
520
warning_lines = [' ' + name for name, _, _ in tag_conflicts]
521
note(gettext("Conflicting tags in bound branch:\n{0}".format(
522
"\n".join(warning_lines))))
524
def _select_reporter(self):
525
"""Select the CommitReporter to use."""
527
return NullCommitReporter()
528
return ReportCommitToLog()
530
def _check_pointless(self):
531
if self.allow_pointless:
533
# A merge with no effect on files
534
if len(self.parents) > 1:
536
if self.builder.any_changes():
538
raise PointlessCommit()
540
def _check_bound_branch(self, operation, possible_master_transports=None):
541
"""Check to see if the local branch is bound.
543
If it is bound, then most of the commit will actually be
544
done using the remote branch as the target branch.
545
Only at the end will the local branch be updated.
547
if self.local and not self.branch.get_bound_location():
548
raise errors.LocalRequiresBoundBranch()
551
self.master_branch = self.branch.get_master_branch(
552
possible_master_transports)
554
if not self.master_branch:
555
# make this branch the reference branch for out of date checks.
556
self.master_branch = self.branch
559
# If the master branch is bound, we must fail
560
master_bound_location = self.master_branch.get_bound_location()
561
if master_bound_location:
562
raise errors.CommitToDoubleBoundBranch(
563
self.branch, self.master_branch, master_bound_location)
565
# TODO: jam 20051230 We could automatically push local
566
# commits to the remote branch if they would fit.
567
# But for now, just require remote to be identical
570
# Make sure the local branch is identical to the master
571
master_revid = self.master_branch.last_revision()
572
local_revid = self.branch.last_revision()
573
if local_revid != master_revid:
574
raise errors.BoundBranchOutOfDate(self.branch,
577
# Now things are ready to change the master branch
579
self.bound_branch = self.branch
580
self.master_branch.lock_write()
581
operation.add_cleanup(self.master_branch.unlock)
583
def _check_out_of_date_tree(self):
584
"""Check that the working tree is up to date.
586
:return: old_revision_number, old_revision_id, new_revision_number
590
first_tree_parent = self.work_tree.get_parent_ids()[0]
592
# if there are no parents, treat our parent as 'None'
593
# this is so that we still consider the master branch
594
# - in a checkout scenario the tree may have no
595
# parents but the branch may do.
596
first_tree_parent = breezy.revision.NULL_REVISION
597
if (self.master_branch._format.stores_revno() or
598
self.config_stack.get('calculate_revnos')):
600
old_revno, master_last = self.master_branch.last_revision_info()
601
except errors.UnsupportedOperation:
602
master_last = self.master_branch.last_revision()
603
old_revno = self.branch.revision_id_to_revno(master_last)
605
master_last = self.master_branch.last_revision()
607
if master_last != first_tree_parent:
608
if master_last != breezy.revision.NULL_REVISION:
609
raise errors.OutOfDateTree(self.work_tree)
610
if (old_revno is not None and
611
self.branch.repository.has_revision(first_tree_parent)):
612
new_revno = old_revno + 1
614
# ghost parents never appear in revision history.
616
return old_revno, master_last, new_revno
618
def _process_pre_hooks(self, old_revno, new_revno):
619
"""Process any registered pre commit hooks."""
620
self._set_progress_stage("Running pre_commit hooks")
621
self._process_hooks("pre_commit", old_revno, new_revno)
623
def _process_post_hooks(self, old_revno, new_revno):
624
"""Process any registered post commit hooks."""
625
# Process the post commit hooks, if any
626
self._set_progress_stage("Running post_commit hooks")
627
# old style commit hooks - should be deprecated ? (obsoleted in
628
# 0.15^H^H^H^H 2.5.0)
629
post_commit = self.config_stack.get('post_commit')
630
if post_commit is not None:
631
hooks = post_commit.split(' ')
632
# this would be nicer with twisted.python.reflect.namedAny
634
result = eval(hook + '(branch, rev_id)',
635
{'branch': self.branch,
637
'rev_id': self.rev_id})
638
# process new style post commit hooks
639
self._process_hooks("post_commit", old_revno, new_revno)
641
def _process_hooks(self, hook_name, old_revno, new_revno):
642
if not Branch.hooks[hook_name]:
645
# new style commit hooks:
646
if not self.bound_branch:
647
hook_master = self.branch
650
hook_master = self.master_branch
651
hook_local = self.branch
652
# With bound branches, when the master is behind the local branch,
653
# the 'old_revno' and old_revid values here are incorrect.
654
# XXX: FIXME ^. RBC 20060206
656
old_revid = self.parents[0]
658
old_revid = breezy.revision.NULL_REVISION
660
if hook_name == "pre_commit":
661
future_tree = self.builder.revision_tree()
662
tree_delta = future_tree.changes_from(self.basis_tree,
665
for hook in Branch.hooks[hook_name]:
666
# show the running hook in the progress bar. As hooks may
667
# end up doing nothing (e.g. because they are not configured by
668
# the user) this is still showing progress, not showing overall
669
# actions - its up to each plugin to show a UI if it want's to
670
# (such as 'Emailing diff to foo@example.com').
671
self.pb_stage_name = "Running %s hooks [%s]" % \
672
(hook_name, Branch.hooks.get_hook_name(hook))
673
self._emit_progress()
674
if 'hooks' in debug.debug_flags:
675
mutter("Invoking commit hook: %r", hook)
676
if hook_name == "post_commit":
677
hook(hook_local, hook_master, old_revno, old_revid, new_revno,
679
elif hook_name == "pre_commit":
680
hook(hook_local, hook_master,
681
old_revno, old_revid, new_revno, self.rev_id,
682
tree_delta, future_tree)
684
def _update_builder_with_changes(self):
685
"""Update the commit builder with the data about what has changed.
687
specific_files = self.specific_files
688
mutter("Selecting files for commit with filter %r", specific_files)
691
iter_changes = self.work_tree.iter_changes(
692
self.basis_tree, specific_files=specific_files)
694
iter_changes = filter_excluded(iter_changes, self.exclude)
695
iter_changes = self._filter_iter_changes(iter_changes)
696
for path, fs_hash in self.builder.record_iter_changes(
697
self.work_tree, self.basis_revid, iter_changes):
698
self.work_tree._observed_sha1(path, fs_hash)
700
def _filter_iter_changes(self, iter_changes):
701
"""Process iter_changes.
703
This method reports on the changes in iter_changes to the user, and
704
converts 'missing' entries in the iter_changes iterator to 'deleted'
705
entries. 'missing' entries have their
707
:param iter_changes: An iter_changes to process.
708
:return: A generator of changes.
710
reporter = self.reporter
711
report_changes = reporter.is_verbose()
713
for change in iter_changes:
715
old_path = change.path[0]
716
new_path = change.path[1]
717
versioned = change.versioned[1]
718
kind = change.kind[1]
719
versioned = change.versioned[1]
720
if kind is None and versioned:
723
reporter.missing(new_path)
724
if change.kind[0] == 'symlink' and not self.work_tree.supports_symlinks():
725
trace.warning('Ignoring "%s" as symlinks are not '
726
'supported on this filesystem.' % (change.path[0],))
728
deleted_paths.append(change[1][1])
729
# Reset the new path (None) and new versioned flag (False)
730
change = change.discard_new()
731
new_path = change.path[1]
733
elif kind == 'tree-reference':
734
if self.recursive == 'down':
735
self._commit_nested_tree(change[1][1])
736
if change.versioned[0] or change.versioned[1]:
740
reporter.deleted(old_path)
741
elif old_path is None:
742
reporter.snapshot_change(gettext('added'), new_path)
743
elif old_path != new_path:
744
reporter.renamed(gettext('renamed'),
748
or self.work_tree.branch.repository._format.rich_root_data):
749
# Don't report on changes to '' in non rich root
751
reporter.snapshot_change(
752
gettext('modified'), new_path)
753
self._next_progress_entry()
754
# Unversion files that were found to be deleted
755
self.deleted_paths = deleted_paths
757
def _check_strict(self):
758
# XXX: when we use iter_changes this would likely be faster if
759
# iter_changes would check for us (even in the presence of
762
# raise an exception as soon as we find a single unknown.
763
for unknown in self.work_tree.unknowns():
764
raise StrictCommitFailed()
766
def _commit_nested_tree(self, path):
767
"Commit a nested tree."
768
sub_tree = self.work_tree.get_nested_tree(path)
769
# FIXME: be more comprehensive here:
770
# this works when both trees are in --trees repository,
771
# but when both are bound to a different repository,
772
# it fails; a better way of approaching this is to
773
# finally implement the explicit-caches approach design
774
# a while back - RBC 20070306.
775
if sub_tree.branch.repository.has_same_location(
776
self.work_tree.branch.repository):
777
sub_tree.branch.repository = \
778
self.work_tree.branch.repository
780
return sub_tree.commit(message=None, revprops=self.revprops,
781
recursive=self.recursive,
782
message_callback=self.message_callback,
783
timestamp=self.timestamp,
784
timezone=self.timezone,
785
committer=self.committer,
786
allow_pointless=self.allow_pointless,
787
strict=self.strict, verbose=self.verbose,
788
local=self.local, reporter=self.reporter)
789
except PointlessCommit:
790
return self.work_tree.get_reference_revision(path)
792
def _set_progress_stage(self, name, counter=False):
793
"""Set the progress stage and emit an update to the progress bar."""
794
self.pb_stage_name = name
795
self.pb_stage_count += 1
797
self.pb_entries_count = 0
799
self.pb_entries_count = None
800
self._emit_progress()
802
def _next_progress_entry(self):
803
"""Emit an update to the progress bar and increment the entry count."""
804
self.pb_entries_count += 1
805
self._emit_progress()
807
def _emit_progress(self):
808
if self.pb_entries_count is not None:
809
text = gettext("{0} [{1}] - Stage").format(self.pb_stage_name,
810
self.pb_entries_count)
812
text = gettext("%s - Stage") % (self.pb_stage_name, )
813
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)