47
56
# merges from, then it should still be reported as newly added
48
57
# 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.
60
from .branch import Branch
61
from .cleanup import OperationWithCleanups
63
from .errors import (BzrError,
67
from .osutils import (get_user_encoding,
69
minimum_path_selection,
72
from .trace import mutter, note, is_quiet
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
59
# TODO: Do checks that the tree can be committed *before* running the
60
# editor; this should include checks for a pointless commit and for
61
# unknown or missing files.
63
# TODO: If commit fails, leave the message in a file somewhere.
72
from binascii import hexlify
73
from cStringIO import StringIO
75
from bzrlib.osutils import (local_time_offset,
76
rand_bytes, compact_date,
77
kind_marker, is_inside_any, quotefn,
78
sha_string, sha_strings, sha_file, isdir, isfile,
80
from bzrlib.branch import gen_file_id
82
from bzrlib.errors import (BzrError, PointlessCommit,
87
import bzrlib.gpg as gpg
88
from bzrlib.revision import Revision
89
from bzrlib.testament import Testament
90
from bzrlib.trace import mutter, note, warning
91
from bzrlib.xml5 import serializer_v5
92
from bzrlib.inventory import Inventory, ROOT_ID
93
from bzrlib.weave import Weave
94
from bzrlib.weavefile import read_weave, write_weave_v5
95
from bzrlib.atomicfile import AtomicFile
98
def commit(*args, **kwargs):
99
"""Commit a new revision to a branch.
101
Function-style interface for convenience of old callers.
103
New code should use the Commit class instead.
99
for change in iter_changes:
100
old_path = change[1][0]
101
new_path = change[1][1]
103
new_excluded = (new_path is not None and
104
is_inside_any(exclude, new_path))
106
old_excluded = (old_path is not None and
107
is_inside_any(exclude, old_path))
109
if old_excluded and new_excluded:
112
if old_excluded or new_excluded:
113
# TODO(jelmer): Perhaps raise an error here instead?
105
## XXX: Remove this in favor of Branch.commit?
106
Commit().commit(*args, **kwargs)
119
109
class NullCommitReporter(object):
120
110
"""I report on progress of a commit."""
122
def started(self, revno, revid, location):
125
112
def snapshot_change(self, change, path):
128
115
def completed(self, revno, rev_id):
131
def deleted(self, path):
118
def deleted(self, file_id):
121
def escaped(self, escape_count, message):
134
124
def missing(self, path):
137
def renamed(self, change, old_path, new_path):
140
def is_verbose(self):
144
127
class ReportCommitToLog(NullCommitReporter):
146
def _note(self, format, *args):
149
Subclasses may choose to override this method.
153
129
def snapshot_change(self, change, path):
154
if path == '' and change in (gettext('added'), gettext('modified')):
156
self._note("%s %s", change, path)
158
def started(self, revno, rev_id, location):
160
gettext('Committing to: %s'),
161
unescape_for_display(location, 'utf-8'))
130
note("%s %s", change, path)
163
132
def completed(self, revno, rev_id):
164
self._note(gettext('Committed revision %d.'), revno)
165
# self._note goes to the console too; so while we want to log the
166
# rev_id, we can't trivially only log it. (See bug 526425). Long
167
# term we should rearrange the reporting structure, but for now
168
# we just mutter seperately. We mutter the revid and revno together
169
# so that concurrent bzr invocations won't lead to confusion.
170
mutter('Committed revid %s as revno %d.', rev_id, revno)
133
note('committed r%d {%s}', revno, rev_id)
135
def deleted(self, file_id):
136
note('deleted %s', file_id)
172
def deleted(self, path):
173
self._note(gettext('deleted %s'), path)
138
def escaped(self, escape_count, message):
139
note("replaced %d control characters in message", escape_count)
175
141
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):
142
note('missing %s', path)
185
144
class Commit(object):
186
145
"""Task of committing a new revision.
238
175
allow_pointless=True,
246
message_callback=None,
249
possible_master_transports=None,
251
179
"""Commit working copy as a new revision.
253
:param message: the commit message (it or message_callback is required)
254
:param message_callback: A callback: message = message_callback(cmt_obj)
256
:param timestamp: if not None, seconds-since-epoch for a
257
postdated/predated commit.
259
:param specific_files: If not None, commit only those files. An empty
260
list means 'commit no files'.
262
:param rev_id: If set, use this as the new revision id.
181
timestamp -- if not None, seconds-since-epoch for a
182
postdated/predated commit.
184
specific_files -- If true, commit only those files.
186
rev_id -- If set, use this as the new revision id.
263
187
Useful for test or import commands that need to tightly
264
188
control what revisions are assigned. If you duplicate
265
189
a revision id that exists elsewhere it is your own fault.
266
190
If null (default), a time/random revision id is generated.
268
:param allow_pointless: If true (default), commit even if nothing
192
allow_pointless -- If true (default), commit even if nothing
269
193
has changed and no merges are recorded.
271
:param strict: If true, don't allow a commit if the working tree
195
strict -- If true, don't allow a commit if the working tree
272
196
contains unknown files.
274
:param revprops: Properties for new revision
275
:param local: Perform a local only commit.
276
:param reporter: the reporter to use or None for the default
277
:param verbose: if True and the reporter is not None, report everything
278
:param recursive: If set to 'down', commit in any subtrees that have
279
pending changes of any sort during this commit.
280
:param exclude: None or a list of relative paths to exclude from the
281
commit. Pending changes to excluded files will be ignored by the
283
:param lossy: When committing to a foreign VCS, ignore any
284
data that can not be natively represented.
198
revprops -- Properties for new revision
286
operation = OperationWithCleanups(self._commit)
287
self.revprops = revprops or {}
288
# XXX: Can be set on __init__ or passed in - this is a bit ugly.
289
self.config_stack = config or self.config_stack
290
return operation.run(
295
specific_files=specific_files,
297
allow_pointless=allow_pointless,
300
working_tree=working_tree,
303
message_callback=message_callback,
306
possible_master_transports=possible_master_transports,
309
def _commit(self, operation, message, timestamp, timezone, committer,
310
specific_files, rev_id, allow_pointless, strict, verbose,
311
working_tree, local, reporter, message_callback, recursive,
312
exclude, possible_master_transports, lossy):
313
200
mutter('preparing to commit')
315
if working_tree is None:
316
raise BzrError("working_tree must be passed into commit().")
318
self.work_tree = working_tree
319
self.branch = self.work_tree.branch
320
if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
321
if not self.branch.repository.supports_rich_root():
322
raise errors.RootNotRich()
323
if message_callback is None:
324
if message is not None:
325
if isinstance(message, bytes):
326
message = message.decode(get_user_encoding())
327
message_callback = lambda x: message
329
raise BzrError("The message or message_callback keyword"
330
" parameter is required for commit().")
332
self.bound_branch = None
333
self.any_entries_deleted = False
334
if exclude is not None:
335
self.exclude = sorted(
336
minimum_path_selection(exclude))
340
self.master_branch = None
341
self.recursive = recursive
343
# self.specific_files is None to indicate no filter, or any iterable to
344
# indicate a filter - [] means no files at all, as per iter_changes.
345
if specific_files is not None:
346
self.specific_files = sorted(
347
minimum_path_selection(specific_files))
349
self.specific_files = None
203
self.weave_store = branch.weave_store
205
self.specific_files = specific_files
351
206
self.allow_pointless = allow_pointless
352
self.message_callback = message_callback
353
self.timestamp = timestamp
354
self.timezone = timezone
355
self.committer = committer
357
self.verbose = verbose
359
self.work_tree.lock_write()
360
operation.add_cleanup(self.work_tree.unlock)
361
self.parents = self.work_tree.get_parent_ids()
362
self.pb = ui.ui_factory.nested_progress_bar()
363
operation.add_cleanup(self.pb.finished)
364
self.basis_revid = self.work_tree.last_revision()
365
self.basis_tree = self.work_tree.basis_tree()
366
self.basis_tree.lock_read()
367
operation.add_cleanup(self.basis_tree.unlock)
368
# Cannot commit with conflicts present.
369
if len(self.work_tree.conflicts()) > 0:
370
raise ConflictsInTree
372
# Setup the bound branch variables as needed.
373
self._check_bound_branch(operation, possible_master_transports)
375
# Check that the working tree is up to date
376
old_revno, old_revid, new_revno = self._check_out_of_date_tree()
378
# Complete configuration setup
379
if reporter is not None:
380
self.reporter = reporter
381
elif self.reporter is None:
382
self.reporter = self._select_reporter()
383
if self.config_stack is None:
384
self.config_stack = self.work_tree.get_config_stack()
386
# Setup the progress bar. As the number of files that need to be
387
# committed in unknown, progress is reported as stages.
388
# We keep track of entries separately though and include that
389
# information in the progress bar during the relevant stages.
390
self.pb_stage_name = ""
391
self.pb_stage_count = 0
392
self.pb_stage_total = 5
393
if self.bound_branch:
394
# 2 extra stages: "Uploading data to master branch" and "Merging
395
# tags to master branch"
396
self.pb_stage_total += 2
397
self.pb.show_pct = False
398
self.pb.show_spinner = False
399
self.pb.show_eta = False
400
self.pb.show_count = True
401
self.pb.show_bar = True
403
# After a merge, a selected file commit is not supported.
404
# See 'bzr help merge' for an explanation as to why.
405
if len(self.parents) > 1 and self.specific_files is not None:
406
raise CannotCommitSelectedFileMerge(self.specific_files)
407
# Excludes are a form of selected file commit.
408
if len(self.parents) > 1 and self.exclude:
409
raise CannotCommitSelectedFileMerge(self.exclude)
411
# Collect the changes
412
self._set_progress_stage("Collecting changes", counter=True)
414
self.builder = self.branch.get_commit_builder(self.parents,
415
self.config_stack, timestamp, timezone, committer, self.revprops,
418
if self.builder.updates_branch and self.bound_branch:
420
raise AssertionError(
421
"bound branches not supported for commit builders "
422
"that update the branch")
425
# find the location being committed to
426
if self.bound_branch:
427
master_location = self.master_branch.base
429
master_location = self.branch.base
431
# report the start of the commit
432
self.reporter.started(new_revno, self.rev_id, master_location)
434
self._update_builder_with_changes()
435
self._check_pointless()
437
# TODO: Now the new inventory is known, check for conflicts.
438
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
439
# weave lines, because nothing should be recorded until it is known
440
# that commit will succeed.
441
self._set_progress_stage("Saving data locally")
442
self.builder.finish_inventory()
444
# Prompt the user for a commit message if none provided
445
message = message_callback(self)
446
self.message = message
448
# Add revision data to the local branch
449
self.rev_id = self.builder.commit(self.message)
451
except Exception as e:
452
mutter("aborting commit write group because of exception:")
453
trace.log_exception_quietly()
457
self._update_branches(old_revno, old_revid, new_revno)
459
# Make the working tree be up to date with the branch. This
460
# includes automatic changes scheduled to be made to the tree, such
461
# as updating its basis and unversioning paths that were missing.
462
self.work_tree.unversion(self.deleted_paths)
463
self._set_progress_stage("Updating the working tree")
464
self.work_tree.update_basis_by_delta(self.rev_id,
465
self.builder.get_basis_delta())
466
self.reporter.completed(new_revno, self.rev_id)
467
self._process_post_hooks(old_revno, new_revno)
470
def _update_branches(self, old_revno, old_revid, new_revno):
471
"""Update the master and local branch to the new revision.
473
This will try to make sure that the master branch is updated
474
before the local branch.
476
:param old_revno: Revision number of master branch before the
478
:param old_revid: Tip of master branch before the commit
479
:param new_revno: Revision number of the new commit
481
if not self.builder.updates_branch:
482
self._process_pre_hooks(old_revno, new_revno)
484
# Upload revision data to the master.
485
# this will propagate merged revisions too if needed.
486
if self.bound_branch:
487
self._set_progress_stage("Uploading data to master branch")
488
# 'commit' to the master first so a timeout here causes the
489
# local branch to be out of date
490
(new_revno, self.rev_id) = self.master_branch.import_last_revision_info_and_tags(
491
self.branch, new_revno, self.rev_id, lossy=self._lossy)
493
self.branch.fetch(self.master_branch, self.rev_id)
495
# and now do the commit locally.
496
self.branch.set_last_revision_info(new_revno, self.rev_id)
499
self._process_pre_hooks(old_revno, new_revno)
501
# The commit builder will already have updated the branch,
503
self.branch.set_last_revision_info(old_revno, old_revid)
506
# Merge local tags to remote
507
if self.bound_branch:
508
self._set_progress_stage("Merging tags to master branch")
509
tag_updates, tag_conflicts = self.branch.tags.merge_to(
510
self.master_branch.tags)
512
warning_lines = [' ' + name for name, _, _ in tag_conflicts]
513
note( gettext("Conflicting tags in bound branch:\n{0}".format(
514
"\n".join(warning_lines))) )
516
def _select_reporter(self):
517
"""Select the CommitReporter to use."""
519
return NullCommitReporter()
520
return ReportCommitToLog()
522
def _check_pointless(self):
523
if self.allow_pointless:
525
# A merge with no effect on files
526
if len(self.parents) > 1:
528
if self.builder.any_changes():
530
raise PointlessCommit()
532
def _check_bound_branch(self, operation, possible_master_transports=None):
533
"""Check to see if the local branch is bound.
535
If it is bound, then most of the commit will actually be
536
done using the remote branch as the target branch.
537
Only at the end will the local branch be updated.
539
if self.local and not self.branch.get_bound_location():
540
raise errors.LocalRequiresBoundBranch()
543
self.master_branch = self.branch.get_master_branch(
544
possible_master_transports)
546
if not self.master_branch:
547
# make this branch the reference branch for out of date checks.
548
self.master_branch = self.branch
551
# If the master branch is bound, we must fail
552
master_bound_location = self.master_branch.get_bound_location()
553
if master_bound_location:
554
raise errors.CommitToDoubleBoundBranch(self.branch,
555
self.master_branch, master_bound_location)
557
# TODO: jam 20051230 We could automatically push local
558
# commits to the remote branch if they would fit.
559
# But for now, just require remote to be identical
562
# Make sure the local branch is identical to the master
563
master_info = self.master_branch.last_revision_info()
564
local_info = self.branch.last_revision_info()
565
if local_info != master_info:
566
raise errors.BoundBranchOutOfDate(self.branch,
569
# Now things are ready to change the master branch
571
self.bound_branch = self.branch
572
self.master_branch.lock_write()
573
operation.add_cleanup(self.master_branch.unlock)
575
def _check_out_of_date_tree(self):
576
"""Check that the working tree is up to date.
578
:return: old_revision_number, old_revision_id, new_revision_number
582
first_tree_parent = self.work_tree.get_parent_ids()[0]
584
# if there are no parents, treat our parent as 'None'
585
# this is so that we still consider the master branch
586
# - in a checkout scenario the tree may have no
587
# parents but the branch may do.
588
first_tree_parent = breezy.revision.NULL_REVISION
589
old_revno, master_last = self.master_branch.last_revision_info()
590
if master_last != first_tree_parent:
591
if master_last != breezy.revision.NULL_REVISION:
592
raise errors.OutOfDateTree(self.work_tree)
593
if self.branch.repository.has_revision(first_tree_parent):
594
new_revno = old_revno + 1
596
# ghost parents never appear in revision history.
598
return old_revno, master_last, new_revno
600
def _process_pre_hooks(self, old_revno, new_revno):
601
"""Process any registered pre commit hooks."""
602
self._set_progress_stage("Running pre_commit hooks")
603
self._process_hooks("pre_commit", old_revno, new_revno)
605
def _process_post_hooks(self, old_revno, new_revno):
606
"""Process any registered post commit hooks."""
607
# Process the post commit hooks, if any
608
self._set_progress_stage("Running post_commit hooks")
609
# old style commit hooks - should be deprecated ? (obsoleted in
610
# 0.15^H^H^H^H 2.5.0)
611
post_commit = self.config_stack.get('post_commit')
612
if post_commit is not None:
613
hooks = post_commit.split(' ')
614
# this would be nicer with twisted.python.reflect.namedAny
616
result = eval(hook + '(branch, rev_id)',
617
{'branch':self.branch,
619
'rev_id':self.rev_id})
620
# process new style post commit hooks
621
self._process_hooks("post_commit", old_revno, new_revno)
623
def _process_hooks(self, hook_name, old_revno, new_revno):
624
if not Branch.hooks[hook_name]:
627
# new style commit hooks:
628
if not self.bound_branch:
629
hook_master = self.branch
632
hook_master = self.master_branch
633
hook_local = self.branch
634
# With bound branches, when the master is behind the local branch,
635
# the 'old_revno' and old_revid values here are incorrect.
636
# XXX: FIXME ^. RBC 20060206
638
old_revid = self.parents[0]
640
old_revid = breezy.revision.NULL_REVISION
642
if hook_name == "pre_commit":
643
future_tree = self.builder.revision_tree()
644
tree_delta = future_tree.changes_from(self.basis_tree,
647
for hook in Branch.hooks[hook_name]:
648
# show the running hook in the progress bar. As hooks may
649
# end up doing nothing (e.g. because they are not configured by
650
# the user) this is still showing progress, not showing overall
651
# actions - its up to each plugin to show a UI if it want's to
652
# (such as 'Emailing diff to foo@example.com').
653
self.pb_stage_name = "Running %s hooks [%s]" % \
654
(hook_name, Branch.hooks.get_hook_name(hook))
655
self._emit_progress()
656
if 'hooks' in debug.debug_flags:
657
mutter("Invoking commit hook: %r", hook)
658
if hook_name == "post_commit":
659
hook(hook_local, hook_master, old_revno, old_revid, new_revno,
661
elif hook_name == "pre_commit":
662
hook(hook_local, hook_master,
663
old_revno, old_revid, new_revno, self.rev_id,
664
tree_delta, future_tree)
666
def _update_builder_with_changes(self):
667
"""Update the commit builder with the data about what has changed.
669
specific_files = self.specific_files
670
mutter("Selecting files for commit with filter %r", specific_files)
673
iter_changes = self.work_tree.iter_changes(self.basis_tree,
674
specific_files=specific_files)
676
iter_changes = filter_excluded(iter_changes, self.exclude)
677
iter_changes = self._filter_iter_changes(iter_changes)
678
for file_id, path, fs_hash in self.builder.record_iter_changes(
679
self.work_tree, self.basis_revid, iter_changes):
680
self.work_tree._observed_sha1(file_id, path, fs_hash)
682
def _filter_iter_changes(self, iter_changes):
683
"""Process iter_changes.
685
This method reports on the changes in iter_changes to the user, and
686
converts 'missing' entries in the iter_changes iterator to 'deleted'
687
entries. 'missing' entries have their
689
:param iter_changes: An iter_changes to process.
690
:return: A generator of changes.
692
reporter = self.reporter
693
report_changes = reporter.is_verbose()
695
for change in iter_changes:
697
old_path = change[1][0]
698
new_path = change[1][1]
699
versioned = change[3][1]
701
versioned = change[3][1]
702
if kind is None and versioned:
705
reporter.missing(new_path)
706
deleted_paths.append(change[1][1])
707
# Reset the new path (None) and new versioned flag (False)
708
change = (change[0], (change[1][0], None), change[2],
709
(change[3][0], False)) + change[4:]
710
new_path = change[1][1]
712
elif kind == 'tree-reference':
713
if self.recursive == 'down':
714
self._commit_nested_tree(change[0], change[1][1])
715
if change[3][0] or change[3][1]:
719
reporter.deleted(old_path)
720
elif old_path is None:
721
reporter.snapshot_change(gettext('added'), new_path)
722
elif old_path != new_path:
723
reporter.renamed(gettext('renamed'), old_path, new_path)
726
self.work_tree.branch.repository._format.rich_root_data):
727
# Don't report on changes to '' in non rich root
729
reporter.snapshot_change(gettext('modified'), new_path)
730
self._next_progress_entry()
731
# Unversion files that were found to be deleted
732
self.deleted_paths = deleted_paths
734
def _check_strict(self):
735
# XXX: when we use iter_changes this would likely be faster if
736
# iter_changes would check for us (even in the presence of
207
self.revprops = revprops
739
210
# raise an exception as soon as we find a single unknown.
740
for unknown in self.work_tree.unknowns():
211
for unknown in branch.unknowns():
741
212
raise StrictCommitFailed()
743
def _commit_nested_tree(self, file_id, path):
744
"Commit a nested tree."
745
sub_tree = self.work_tree.get_nested_tree(path, file_id)
746
# FIXME: be more comprehensive here:
747
# this works when both trees are in --trees repository,
748
# but when both are bound to a different repository,
749
# it fails; a better way of approaching this is to
750
# finally implement the explicit-caches approach design
751
# a while back - RBC 20070306.
752
if sub_tree.branch.repository.has_same_location(
753
self.work_tree.branch.repository):
754
sub_tree.branch.repository = \
755
self.work_tree.branch.repository
214
if timestamp is None:
215
self.timestamp = time.time()
217
self.timestamp = long(timestamp)
219
if self.config is None:
220
self.config = bzrlib.config.BranchConfig(self.branch)
223
self.rev_id = _gen_revision_id(self.config, self.timestamp)
227
if committer is None:
228
self.committer = self.config.username()
230
assert isinstance(committer, basestring), type(committer)
231
self.committer = committer
234
self.timezone = local_time_offset()
236
self.timezone = int(timezone)
238
assert isinstance(message, basestring), type(message)
239
self.message = message
240
self._escape_commit_message()
242
self.branch.lock_write()
757
return sub_tree.commit(message=None, revprops=self.revprops,
758
recursive=self.recursive,
759
message_callback=self.message_callback,
760
timestamp=self.timestamp, timezone=self.timezone,
761
committer=self.committer,
762
allow_pointless=self.allow_pointless,
763
strict=self.strict, verbose=self.verbose,
764
local=self.local, reporter=self.reporter)
765
except PointlessCommit:
766
return self.work_tree.get_reference_revision(path, file_id)
768
def _set_progress_stage(self, name, counter=False):
769
"""Set the progress stage and emit an update to the progress bar."""
770
self.pb_stage_name = name
771
self.pb_stage_count += 1
773
self.pb_entries_count = 0
775
self.pb_entries_count = None
776
self._emit_progress()
778
def _next_progress_entry(self):
779
"""Emit an update to the progress bar and increment the entry count."""
780
self.pb_entries_count += 1
781
self._emit_progress()
783
def _emit_progress(self):
784
if self.pb_entries_count is not None:
785
text = gettext("{0} [{1}] - Stage").format(self.pb_stage_name,
786
self.pb_entries_count)
788
text = gettext("%s - Stage") % (self.pb_stage_name, )
789
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
244
self.work_tree = self.branch.working_tree()
245
self.work_inv = self.work_tree.inventory
246
self.basis_tree = self.branch.basis_tree()
247
self.basis_inv = self.basis_tree.inventory
249
self._gather_parents()
250
if len(self.parents) > 1 and self.specific_files:
251
raise NotImplementedError('selected-file commit of merges is not supported yet')
252
self._check_parents_present()
254
self._remove_deleted()
255
self._populate_new_inv()
256
self._store_snapshot()
257
self._report_deletes()
259
if not (self.allow_pointless
260
or len(self.parents) > 1
261
or self.new_inv != self.basis_inv):
262
raise PointlessCommit()
264
if len(list(self.work_tree.iter_conflicts()))>0:
265
raise ConflictsInTree
267
self._record_inventory()
268
self._make_revision()
269
self.branch.append_revision(self.rev_id)
270
self.work_tree.set_pending_merges([])
271
self.reporter.completed(self.branch.revno()+1, self.rev_id)
272
if self.config.post_commit() is not None:
273
hooks = self.config.post_commit().split(' ')
274
# this would be nicer with twisted.python.reflect.namedAny
276
result = eval(hook + '(branch, rev_id)',
277
{'branch':self.branch,
279
'rev_id':self.rev_id})
283
def _record_inventory(self):
284
"""Store the inventory for the new revision."""
285
inv_text = serializer_v5.write_inventory_to_string(self.new_inv)
286
self.inv_sha1 = sha_string(inv_text)
287
s = self.branch.control_weaves
288
s.add_text('inventory', self.rev_id,
289
split_lines(inv_text), self.present_parents,
290
self.branch.get_transaction())
292
def _escape_commit_message(self):
293
"""Replace xml-incompatible control characters."""
294
# Python strings can include characters that can't be
295
# represented in well-formed XML; escape characters that
296
# aren't listed in the XML specification
297
# (http://www.w3.org/TR/REC-xml/#NT-Char).
298
if isinstance(self.message, unicode):
299
char_pattern = u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]'
301
# Use a regular 'str' as pattern to avoid having re.subn
302
# return 'unicode' results.
303
char_pattern = '[^x09\x0A\x0D\x20-\xFF]'
304
self.message, escape_count = re.subn(
306
lambda match: match.group(0).encode('unicode_escape'),
309
self.reporter.escaped(escape_count, self.message)
311
def _gather_parents(self):
312
"""Record the parents of a merge for merge detection."""
313
pending_merges = self.work_tree.pending_merges()
315
self.parent_invs = []
316
self.present_parents = []
317
precursor_id = self.branch.last_revision()
319
self.parents.append(precursor_id)
320
self.parents += pending_merges
321
for revision in self.parents:
322
if self.branch.has_revision(revision):
323
self.parent_invs.append(self.branch.get_inventory(revision))
324
self.present_parents.append(revision)
326
def _check_parents_present(self):
327
for parent_id in self.parents:
328
mutter('commit parent revision {%s}', parent_id)
329
if not self.branch.has_revision(parent_id):
330
if parent_id == self.branch.last_revision():
331
warning("parent is missing %r", parent_id)
332
raise HistoryMissing(self.branch, 'revision', parent_id)
334
mutter("commit will ghost revision %r", parent_id)
336
def _make_revision(self):
337
"""Record a new revision object for this commit."""
338
self.rev = Revision(timestamp=self.timestamp,
339
timezone=self.timezone,
340
committer=self.committer,
341
message=self.message,
342
inventory_sha1=self.inv_sha1,
343
revision_id=self.rev_id,
344
properties=self.revprops)
345
self.rev.parent_ids = self.parents
347
serializer_v5.write_revision(self.rev, rev_tmp)
349
if self.config.signature_needed():
350
plaintext = Testament(self.rev, self.new_inv).as_short_text()
351
self.branch.store_revision_signature(gpg.GPGStrategy(self.config),
352
plaintext, self.rev_id)
353
self.branch.revision_store.add(rev_tmp, self.rev_id)
354
mutter('new revision_id is {%s}', self.rev_id)
356
def _remove_deleted(self):
357
"""Remove deleted files from the working inventories.
359
This is done prior to taking the working inventory as the
360
basis for the new committed inventory.
362
This returns true if any files
363
*that existed in the basis inventory* were deleted.
364
Files that were added and deleted
365
in the working copy don't matter.
367
specific = self.specific_files
369
for path, ie in self.work_inv.iter_entries():
370
if specific and not is_inside_any(specific, path):
372
if not self.work_tree.has_filename(path):
373
self.reporter.missing(path)
374
deleted_ids.append((path, ie.file_id))
376
deleted_ids.sort(reverse=True)
377
for path, file_id in deleted_ids:
378
del self.work_inv[file_id]
379
self.work_tree._write_inventory(self.work_inv)
381
def _store_snapshot(self):
382
"""Pass over inventory and record a snapshot.
384
Entries get a new revision when they are modified in
385
any way, which includes a merge with a new set of
386
parents that have the same entry.
388
# XXX: Need to think more here about when the user has
389
# made a specific decision on a particular value -- c.f.
391
for path, ie in self.new_inv.iter_entries():
392
previous_entries = ie.find_previous_heads(
394
self.weave_store.get_weave_or_empty(ie.file_id,
395
self.branch.get_transaction()))
396
if ie.revision is None:
397
change = ie.snapshot(self.rev_id, path, previous_entries,
398
self.work_tree, self.weave_store,
399
self.branch.get_transaction())
402
self.reporter.snapshot_change(change, path)
404
def _populate_new_inv(self):
405
"""Build revision inventory.
407
This creates a new empty inventory. Depending on
408
which files are selected for commit, and what is present in the
409
current tree, the new inventory is populated. inventory entries
410
which are candidates for modification have their revision set to
411
None; inventory entries that are carried over untouched have their
412
revision set to their prior value.
414
mutter("Selecting files for commit with filter %s", self.specific_files)
415
self.new_inv = Inventory()
416
for path, new_ie in self.work_inv.iter_entries():
417
file_id = new_ie.file_id
418
mutter('check %s {%s}', path, new_ie.file_id)
419
if self.specific_files:
420
if not is_inside_any(self.specific_files, path):
421
mutter('%s not selected for commit', path)
422
self._carry_entry(file_id)
425
# this is selected, ensure its parents are too.
426
parent_id = new_ie.parent_id
427
while parent_id != ROOT_ID:
428
if not self.new_inv.has_id(parent_id):
429
ie = self._select_entry(self.work_inv[parent_id])
430
mutter('%s selected for commit because of %s',
431
self.new_inv.id2path(parent_id), path)
433
ie = self.new_inv[parent_id]
434
if ie.revision is not None:
436
mutter('%s selected for commit because of %s',
437
self.new_inv.id2path(parent_id), path)
438
parent_id = ie.parent_id
439
mutter('%s selected for commit', path)
440
self._select_entry(new_ie)
442
def _select_entry(self, new_ie):
443
"""Make new_ie be considered for committing."""
449
def _carry_entry(self, file_id):
450
"""Carry the file unchanged from the basis revision."""
451
if self.basis_inv.has_id(file_id):
452
self.new_inv.add(self.basis_inv[file_id].copy())
454
def _report_deletes(self):
455
for file_id in self.basis_inv:
456
if file_id not in self.new_inv:
457
self.reporter.deleted(self.basis_inv.id2path(file_id))
459
def _gen_revision_id(config, when):
460
"""Return new revision-id."""
461
s = '%s-%s-' % (config.user_email(), compact_date(when))
462
s += hexlify(rand_bytes(8))