1
# Copyright (C) 2005 Canonical Ltd
1
# Copyright (C) 2005-2011 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
def commit(branch, message,
26
allow_pointless=True):
27
"""Commit working copy as a new revision.
29
The basic approach is to add all the file texts into the
30
store, then the inventory, then make a new revision pointing
31
to that inventory and store that.
33
This is not quite safe if the working copy changes during the
34
commit; for the moment that is simply not allowed. A better
35
approach is to make a temporary copy of the files before
36
computing their hashes, and then add those hashes in turn to
37
the inventory. This should mean at least that there are no
38
broken hash pointers. There is no way we can get a snapshot
39
of the whole directory at an instant. This would also have to
40
be robust against files disappearing, moving, etc. So the
41
whole thing is a bit hard.
43
This raises PointlessCommit if there are no changes, no new merges,
44
and allow_pointless is false.
46
timestamp -- if not None, seconds-since-epoch for a
47
postdated/predated commit.
50
If true, commit only those files.
53
If set, use this as the new revision id.
54
Useful for test or import commands that need to tightly
55
control what revisions are assigned. If you duplicate
56
a revision id that exists elsewhere it is your own fault.
57
If null (default), a time/random revision id is generated.
60
import time, tempfile, re
62
from bzrlib.osutils import local_time_offset, username
63
from bzrlib.branch import gen_file_id
64
from bzrlib.errors import BzrError, PointlessCommit
65
from bzrlib.revision import Revision, RevisionReference
66
from bzrlib.trace import mutter, note
67
from bzrlib.xml import serializer_v4
72
# First walk over the working inventory; and both update that
73
# and also build a new revision inventory. The revision
74
# inventory needs to hold the text-id, sha1 and size of the
75
# actual file versions committed in the revision. (These are
76
# not present in the working inventory.) We also need to
77
# detect missing/deleted files, and remove them from the
80
work_tree = branch.working_tree()
81
work_inv = work_tree.inventory
82
basis = branch.basis_tree()
83
basis_inv = basis.inventory
86
# note('looking for changes...')
87
# print 'looking for changes...'
88
# disabled; should be done at a higher level
91
pending_merges = branch.pending_merges()
93
missing_ids, new_inv, any_changes = \
94
_gather_commit(branch,
101
if not (any_changes or allow_pointless or pending_merges):
102
raise PointlessCommit()
104
for file_id in missing_ids:
105
# Any files that have been deleted are now removed from the
106
# working inventory. Files that were not selected for commit
107
# are left as they were in the working inventory and ommitted
108
# from the revision inventory.
110
# have to do this later so we don't mess up the iterator.
111
# since parents may be removed before their children we
114
# FIXME: There's probably a better way to do this; perhaps
115
# the workingtree should know how to filter itbranch.
116
if work_inv.has_id(file_id):
117
del work_inv[file_id]
120
rev_id = _gen_revision_id(branch, time.time())
123
inv_tmp = tempfile.TemporaryFile()
125
serializer_v4.write_inventory(new_inv, inv_tmp)
127
branch.inventory_store.add(inv_tmp, inv_id)
128
mutter('new inventory_id is {%s}' % inv_id)
130
# We could also just sha hash the inv_tmp file
131
# however, in the case that branch.inventory_store.add()
132
# ever actually does anything special
133
inv_sha1 = branch.get_inventory_sha1(inv_id)
135
branch._write_inventory(work_inv)
137
if timestamp == None:
138
timestamp = time.time()
140
if committer == None:
141
committer = username(branch)
144
timezone = local_time_offset()
146
mutter("building commit log message")
147
# Python strings can include characters that can't be
148
# represented in well-formed XML; escape characters that
149
# aren't listed in the XML specification
150
# (http://www.w3.org/TR/REC-xml/#NT-Char).
151
if isinstance(message, unicode):
152
char_pattern = u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]'
154
# Use a regular 'str' as pattern to avoid having re.subn
155
# return 'unicode' results.
156
char_pattern = '[^x09\x0A\x0D\x20-\xFF]'
157
message, escape_count = re.subn(
159
lambda match: match.group(0).encode('unicode_escape'),
162
note("replaced %d control characters in message", escape_count)
163
rev = Revision(timestamp=timestamp,
168
inventory_sha1=inv_sha1,
172
precursor_id = branch.last_patch()
174
precursor_sha1 = branch.get_revision_sha1(precursor_id)
175
rev.parents.append(RevisionReference(precursor_id, precursor_sha1))
176
for merge_rev in pending_merges:
177
rev.parents.append(RevisionReference(merge_rev))
179
rev_tmp = tempfile.TemporaryFile()
180
serializer_v4.write_revision(rev, rev_tmp)
182
branch.revision_store.add(rev_tmp, rev_id)
183
mutter("new revision_id is {%s}" % rev_id)
185
## XXX: Everything up to here can simply be orphaned if we abort
186
## the commit; it will leave junk files behind but that doesn't
189
## TODO: Read back the just-generated changeset, and make sure it
190
## applies and recreates the right state.
192
## TODO: Also calculate and store the inventory SHA1
193
mutter("committing patch r%d" % (branch.revno() + 1))
195
branch.append_revision(rev_id)
197
branch.set_pending_merges([])
200
# disabled; should go through logging
201
# note("commited r%d" % branch.revno())
202
# print ("commited r%d" % branch.revno())
209
def _gen_revision_id(branch, when):
210
"""Return new revision-id."""
211
from binascii import hexlify
212
from bzrlib.osutils import rand_bytes, compact_date, user_email
214
s = '%s-%s-' % (user_email(branch), compact_date(when))
215
s += hexlify(rand_bytes(8))
219
def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files,
221
"""Build inventory preparatory to commit.
223
Returns missing_ids, new_inv, any_changes.
225
This adds any changed files into the text store, and sets their
226
test-id, sha and size in the returned inventory appropriately.
229
Modified to hold a list of files that have been deleted from
230
the working directory; these should be removed from the
233
from bzrlib.inventory import Inventory
234
from bzrlib.osutils import isdir, isfile, sha_string, quotefn, \
235
local_time_offset, username, kind_marker, is_inside_any
237
from bzrlib.branch import gen_file_id
238
from bzrlib.errors import BzrError
239
from bzrlib.revision import Revision
240
from bzrlib.trace import mutter, note
243
inv = Inventory(work_inv.root.file_id)
246
for path, entry in work_inv.iter_entries():
247
## TODO: Check that the file kind has not changed from the previous
248
## revision of this file (if any).
250
p = branch.abspath(path)
251
file_id = entry.file_id
252
mutter('commit prep file %s, id %r ' % (p, file_id))
254
if specific_files and not is_inside_any(specific_files, path):
255
mutter(' skipping file excluded from commit')
256
if basis_inv.has_id(file_id):
257
# carry over with previous state
258
inv.add(basis_inv[file_id].copy())
260
# omit this from committed inventory
264
if not work_tree.has_id(file_id):
266
print('deleted %s%s' % (path, kind_marker(entry.kind)))
268
mutter(" file is missing, removing from inventory")
269
missing_ids.append(file_id)
272
# this is present in the new inventory; may be new, modified or
274
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
280
old_kind = old_ie.kind
281
if old_kind != entry.kind:
282
raise BzrError("entry %r changed kind from %r to %r"
283
% (file_id, old_kind, entry.kind))
285
if entry.kind == 'directory':
287
raise BzrError("%s is entered as directory but not a directory"
289
elif entry.kind == 'file':
291
raise BzrError("%s is entered as file but is not a file" % quotefn(p))
293
new_sha1 = work_tree.get_file_sha1(file_id)
296
and old_ie.text_sha1 == new_sha1):
297
## assert content == basis.get_file(file_id).read()
298
entry.text_id = old_ie.text_id
299
entry.text_sha1 = new_sha1
300
entry.text_size = old_ie.text_size
301
mutter(' unchanged from previous text_id {%s}' %
304
content = file(p, 'rb').read()
306
# calculate the sha again, just in case the file contents
307
# changed since we updated the cache
308
entry.text_sha1 = sha_string(content)
309
entry.text_size = len(content)
311
entry.text_id = gen_file_id(entry.name)
312
branch.text_store.add(content, entry.text_id)
313
mutter(' stored with text_id {%s}' % entry.text_id)
316
marked = path + kind_marker(entry.kind)
318
print 'added', marked
320
elif old_ie == entry:
322
elif (old_ie.name == entry.name
323
and old_ie.parent_id == entry.parent_id):
324
print 'modified', marked
327
print 'renamed', marked
329
elif old_ie != entry:
332
return missing_ids, inv, any_changes
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
# The newly committed revision is going to have a shape corresponding
18
# to that of the working tree. Files that are not in the
19
# working tree and that were in the predecessor are reported as
20
# removed --- this can include files that were either removed from the
21
# inventory or deleted in the working tree. If they were only
22
# deleted from disk, they are removed from the working inventory.
24
# We then consider the remaining entries, which will be in the new
25
# version. Directory entries are simply copied across. File entries
26
# must be checked to see if a new version of the file should be
27
# recorded. For each parent revision tree, we check to see what
28
# version of the file was present. If the file was present in at
29
# least one tree, and if it was the same version in all the trees,
30
# then we can just refer to that version. Otherwise, a new version
31
# representing the merger of the file versions must be added.
33
# TODO: Update hashcache before and after - or does the WorkingTree
36
# TODO: Rather than mashing together the ancestry and storing it back,
37
# perhaps the weave should have single method which does it all in one
38
# go, avoiding a lot of redundant work.
40
# TODO: Perhaps give a warning if one of the revisions marked as
41
# merged is already in the ancestry, and then don't record it as a
44
# TODO: If the file is newly merged but unchanged from the version it
45
# merges from, then it should still be reported as newly added
46
# relative to the basis revision.
48
# TODO: Change the parameter 'rev_id' to 'revision_id' to be consistent with
49
# the rest of the code; add a deprecation of the old name.
57
from .branch import Branch
58
from contextlib import ExitStack
60
from .errors import (BzrError,
64
from .osutils import (get_user_encoding,
67
minimum_path_selection,
69
from .trace import mutter, note, is_quiet
70
from .urlutils import unescape_for_display
71
from .i18n import gettext
74
class PointlessCommit(BzrError):
76
_fmt = "No changes to commit"
79
class CannotCommitSelectedFileMerge(BzrError):
81
_fmt = 'Selected-file commit of merges is not supported yet:'\
82
' files %(files_str)s'
84
def __init__(self, files):
85
files_str = ', '.join(files)
86
BzrError.__init__(self, files=files, files_str=files_str)
89
def filter_excluded(iter_changes, exclude):
90
"""Filter exclude filenames.
92
:param iter_changes: iter_changes function
93
:param exclude: List of paths to exclude
94
:return: iter_changes function
96
for change in iter_changes:
97
new_excluded = (change.path[1] is not None and
98
is_inside_any(exclude, change.path[1]))
100
old_excluded = (change.path[0] is not None and
101
is_inside_any(exclude, change.path[0]))
103
if old_excluded and new_excluded:
106
if old_excluded or new_excluded:
107
# TODO(jelmer): Perhaps raise an error here instead?
113
class NullCommitReporter(object):
114
"""I report on progress of a commit."""
116
def started(self, revno, revid, location):
119
def snapshot_change(self, change, path):
122
def completed(self, revno, rev_id):
125
def deleted(self, path):
128
def missing(self, path):
131
def renamed(self, change, old_path, new_path):
134
def is_verbose(self):
138
class ReportCommitToLog(NullCommitReporter):
140
def _note(self, format, *args):
143
Subclasses may choose to override this method.
147
def snapshot_change(self, change, path):
148
if path == '' and change in (gettext('added'), gettext('modified')):
150
self._note("%s %s", change, path)
152
def started(self, revno, rev_id, location):
154
gettext('Committing to: %s'),
155
unescape_for_display(location, 'utf-8'))
157
def completed(self, revno, rev_id):
158
if revno is not None:
159
self._note(gettext('Committed revision %d.'), revno)
160
# self._note goes to the console too; so while we want to log the
161
# rev_id, we can't trivially only log it. (See bug 526425). Long
162
# term we should rearrange the reporting structure, but for now
163
# we just mutter seperately. We mutter the revid and revno together
164
# so that concurrent bzr invocations won't lead to confusion.
165
mutter('Committed revid %s as revno %d.', rev_id, revno)
167
self._note(gettext('Committed revid %s.'), rev_id)
169
def deleted(self, path):
170
self._note(gettext('deleted %s'), path)
172
def missing(self, path):
173
self._note(gettext('missing %s'), path)
175
def renamed(self, change, old_path, new_path):
176
self._note('%s %s => %s', change, old_path, new_path)
178
def is_verbose(self):
182
class Commit(object):
183
"""Task of committing a new revision.
185
This is a MethodObject: it accumulates state as the commit is
186
prepared, and then it is discarded. It doesn't represent
187
historical revisions, just the act of recording a new one.
190
Modified to hold a list of files that have been deleted from
191
the working directory; these should be removed from the
198
"""Create a Commit object.
200
:param reporter: the default reporter to use or None to decide later
202
self.reporter = reporter
203
self.config_stack = config_stack
206
def update_revprops(revprops, branch, authors=None,
207
local=False, possible_master_transports=None):
210
if possible_master_transports is None:
211
possible_master_transports = []
212
if (u'branch-nick' not in revprops and
213
branch.repository._format.supports_storing_branch_nick):
214
revprops[u'branch-nick'] = branch._get_nick(
216
possible_master_transports)
217
if authors is not None:
218
if u'author' in revprops or u'authors' in revprops:
219
# XXX: maybe we should just accept one of them?
220
raise AssertionError('author property given twice')
222
for individual in authors:
223
if '\n' in individual:
224
raise AssertionError('\\n is not a valid character '
225
'in an author identity')
226
revprops[u'authors'] = '\n'.join(authors)
236
allow_pointless=True,
244
message_callback=None,
247
possible_master_transports=None,
249
"""Commit working copy as a new revision.
251
:param message: the commit message (it or message_callback is required)
252
:param message_callback: A callback: message =
253
message_callback(cmt_obj)
255
:param timestamp: if not None, seconds-since-epoch for a
256
postdated/predated commit.
258
:param specific_files: If not None, commit only those files. An empty
259
list means 'commit no files'.
261
:param rev_id: If set, use this as the new revision id.
262
Useful for test or import commands that need to tightly
263
control what revisions are assigned. If you duplicate
264
a revision id that exists elsewhere it is your own fault.
265
If null (default), a time/random revision id is generated.
267
:param allow_pointless: If true (default), commit even if nothing
268
has changed and no merges are recorded.
270
:param strict: If true, don't allow a commit if the working tree
271
contains unknown files.
273
:param revprops: Properties for new revision
274
:param local: Perform a local only commit.
275
:param reporter: the reporter to use or None for the default
276
:param verbose: if True and the reporter is not None, report everything
277
:param recursive: If set to 'down', commit in any subtrees that have
278
pending changes of any sort during this commit.
279
:param exclude: None or a list of relative paths to exclude from the
280
commit. Pending changes to excluded files will be ignored by the
282
:param lossy: When committing to a foreign VCS, ignore any
283
data that can not be natively represented.
285
with ExitStack() as stack:
286
self.revprops = revprops or {}
287
# XXX: Can be set on __init__ or passed in - this is a bit ugly.
288
self.config_stack = config or self.config_stack
289
mutter('preparing to commit')
291
if working_tree is None:
292
raise BzrError("working_tree must be passed into commit().")
294
self.work_tree = working_tree
295
self.branch = self.work_tree.branch
296
if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
297
if not self.branch.repository.supports_rich_root():
298
raise errors.RootNotRich()
299
if message_callback is None:
300
if message is not None:
301
if isinstance(message, bytes):
302
message = message.decode(get_user_encoding())
304
def message_callback(x):
307
raise BzrError("The message or message_callback keyword"
308
" parameter is required for commit().")
310
self.bound_branch = None
311
self.any_entries_deleted = False
312
if exclude is not None:
313
self.exclude = sorted(
314
minimum_path_selection(exclude))
318
self.master_branch = None
319
self.recursive = recursive
321
# self.specific_files is None to indicate no filter, or any iterable to
322
# indicate a filter - [] means no files at all, as per iter_changes.
323
if specific_files is not None:
324
self.specific_files = sorted(
325
minimum_path_selection(specific_files))
327
self.specific_files = None
329
self.allow_pointless = allow_pointless
330
self.message_callback = message_callback
331
self.timestamp = timestamp
332
self.timezone = timezone
333
self.committer = committer
335
self.verbose = verbose
337
stack.enter_context(self.work_tree.lock_write())
338
self.parents = self.work_tree.get_parent_ids()
339
self.pb = ui.ui_factory.nested_progress_bar()
340
stack.callback(self.pb.finished)
341
self.basis_revid = self.work_tree.last_revision()
342
self.basis_tree = self.work_tree.basis_tree()
343
stack.enter_context(self.basis_tree.lock_read())
344
# Cannot commit with conflicts present.
345
if len(self.work_tree.conflicts()) > 0:
346
raise ConflictsInTree
348
# Setup the bound branch variables as needed.
349
self._check_bound_branch(stack, possible_master_transports)
350
if self.config_stack is None:
351
self.config_stack = self.work_tree.get_config_stack()
353
# Check that the working tree is up to date
354
old_revno, old_revid, new_revno = self._check_out_of_date_tree()
356
# Complete configuration setup
357
if reporter is not None:
358
self.reporter = reporter
359
elif self.reporter is None:
360
self.reporter = self._select_reporter()
362
# Setup the progress bar. As the number of files that need to be
363
# committed in unknown, progress is reported as stages.
364
# We keep track of entries separately though and include that
365
# information in the progress bar during the relevant stages.
366
self.pb_stage_name = ""
367
self.pb_stage_count = 0
368
self.pb_stage_total = 5
369
if self.bound_branch:
370
# 2 extra stages: "Uploading data to master branch" and "Merging
371
# tags to master branch"
372
self.pb_stage_total += 2
373
self.pb.show_pct = False
374
self.pb.show_spinner = False
375
self.pb.show_eta = False
376
self.pb.show_count = True
377
self.pb.show_bar = True
379
# After a merge, a selected file commit is not supported.
380
# See 'bzr help merge' for an explanation as to why.
381
if len(self.parents) > 1 and self.specific_files is not None:
382
raise CannotCommitSelectedFileMerge(self.specific_files)
383
# Excludes are a form of selected file commit.
384
if len(self.parents) > 1 and self.exclude:
385
raise CannotCommitSelectedFileMerge(self.exclude)
387
# Collect the changes
388
self._set_progress_stage("Collecting changes", counter=True)
390
self.builder = self.branch.get_commit_builder(
391
self.parents, self.config_stack, timestamp, timezone, committer,
392
self.revprops, rev_id, lossy=lossy)
394
if self.builder.updates_branch and self.bound_branch:
396
raise AssertionError(
397
"bound branches not supported for commit builders "
398
"that update the branch")
401
# find the location being committed to
402
if self.bound_branch:
403
master_location = self.master_branch.base
405
master_location = self.branch.base
407
# report the start of the commit
408
self.reporter.started(new_revno, self.rev_id, master_location)
410
self._update_builder_with_changes()
411
self._check_pointless()
413
# TODO: Now the new inventory is known, check for conflicts.
414
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
415
# weave lines, because nothing should be recorded until it is known
416
# that commit will succeed.
417
self._set_progress_stage("Saving data locally")
418
self.builder.finish_inventory()
420
# Prompt the user for a commit message if none provided
421
message = message_callback(self)
422
self.message = message
424
# Add revision data to the local branch
425
self.rev_id = self.builder.commit(self.message)
428
mutter("aborting commit write group because of exception:")
429
trace.log_exception_quietly()
433
self._update_branches(old_revno, old_revid, new_revno)
435
# Make the working tree be up to date with the branch. This
436
# includes automatic changes scheduled to be made to the tree, such
437
# as updating its basis and unversioning paths that were missing.
438
self.work_tree.unversion(self.deleted_paths)
439
self._set_progress_stage("Updating the working tree")
440
self.work_tree.update_basis_by_delta(
441
self.rev_id, self.builder.get_basis_delta())
442
self.reporter.completed(new_revno, self.rev_id)
443
self._process_post_hooks(old_revno, new_revno)
446
def _update_branches(self, old_revno, old_revid, new_revno):
447
"""Update the master and local branch to the new revision.
449
This will try to make sure that the master branch is updated
450
before the local branch.
452
:param old_revno: Revision number of master branch before the
454
:param old_revid: Tip of master branch before the commit
455
:param new_revno: Revision number of the new commit
457
if not self.builder.updates_branch:
458
self._process_pre_hooks(old_revno, new_revno)
460
# Upload revision data to the master.
461
# this will propagate merged revisions too if needed.
462
if self.bound_branch:
463
self._set_progress_stage("Uploading data to master branch")
464
# 'commit' to the master first so a timeout here causes the
465
# local branch to be out of date
466
(new_revno, self.rev_id) = self.master_branch.import_last_revision_info_and_tags(
467
self.branch, new_revno, self.rev_id, lossy=self._lossy)
469
self.branch.fetch(self.master_branch, self.rev_id)
471
# and now do the commit locally.
472
if new_revno is None:
473
# Keep existing behaviour around ghosts
475
self.branch.set_last_revision_info(new_revno, self.rev_id)
478
self._process_pre_hooks(old_revno, new_revno)
479
except BaseException:
480
# The commit builder will already have updated the branch,
482
self.branch.set_last_revision_info(old_revno, old_revid)
485
# Merge local tags to remote
486
if self.bound_branch:
487
self._set_progress_stage("Merging tags to master branch")
488
tag_updates, tag_conflicts = self.branch.tags.merge_to(
489
self.master_branch.tags)
491
warning_lines = [' ' + name for name, _, _ in tag_conflicts]
492
note(gettext("Conflicting tags in bound branch:\n{0}".format(
493
"\n".join(warning_lines))))
495
def _select_reporter(self):
496
"""Select the CommitReporter to use."""
498
return NullCommitReporter()
499
return ReportCommitToLog()
501
def _check_pointless(self):
502
if self.allow_pointless:
504
# A merge with no effect on files
505
if len(self.parents) > 1:
507
if self.builder.any_changes():
509
raise PointlessCommit()
511
def _check_bound_branch(self, stack, possible_master_transports=None):
512
"""Check to see if the local branch is bound.
514
If it is bound, then most of the commit will actually be
515
done using the remote branch as the target branch.
516
Only at the end will the local branch be updated.
518
if self.local and not self.branch.get_bound_location():
519
raise errors.LocalRequiresBoundBranch()
522
self.master_branch = self.branch.get_master_branch(
523
possible_master_transports)
525
if not self.master_branch:
526
# make this branch the reference branch for out of date checks.
527
self.master_branch = self.branch
530
# If the master branch is bound, we must fail
531
master_bound_location = self.master_branch.get_bound_location()
532
if master_bound_location:
533
raise errors.CommitToDoubleBoundBranch(
534
self.branch, self.master_branch, master_bound_location)
536
# TODO: jam 20051230 We could automatically push local
537
# commits to the remote branch if they would fit.
538
# But for now, just require remote to be identical
541
# Make sure the local branch is identical to the master
542
master_revid = self.master_branch.last_revision()
543
local_revid = self.branch.last_revision()
544
if local_revid != master_revid:
545
raise errors.BoundBranchOutOfDate(self.branch,
548
# Now things are ready to change the master branch
550
self.bound_branch = self.branch
551
stack.enter_context(self.master_branch.lock_write())
553
def _check_out_of_date_tree(self):
554
"""Check that the working tree is up to date.
556
:return: old_revision_number, old_revision_id, new_revision_number
560
first_tree_parent = self.work_tree.get_parent_ids()[0]
562
# if there are no parents, treat our parent as 'None'
563
# this is so that we still consider the master branch
564
# - in a checkout scenario the tree may have no
565
# parents but the branch may do.
566
first_tree_parent = breezy.revision.NULL_REVISION
567
if (self.master_branch._format.stores_revno() or
568
self.config_stack.get('calculate_revnos')):
570
old_revno, master_last = self.master_branch.last_revision_info()
571
except errors.UnsupportedOperation:
572
master_last = self.master_branch.last_revision()
573
old_revno = self.branch.revision_id_to_revno(master_last)
575
master_last = self.master_branch.last_revision()
577
if master_last != first_tree_parent:
578
if master_last != breezy.revision.NULL_REVISION:
579
raise errors.OutOfDateTree(self.work_tree)
580
if (old_revno is not None and
581
self.branch.repository.has_revision(first_tree_parent)):
582
new_revno = old_revno + 1
584
# ghost parents never appear in revision history.
586
return old_revno, master_last, new_revno
588
def _process_pre_hooks(self, old_revno, new_revno):
589
"""Process any registered pre commit hooks."""
590
self._set_progress_stage("Running pre_commit hooks")
591
self._process_hooks("pre_commit", old_revno, new_revno)
593
def _process_post_hooks(self, old_revno, new_revno):
594
"""Process any registered post commit hooks."""
595
# Process the post commit hooks, if any
596
self._set_progress_stage("Running post_commit hooks")
597
# old style commit hooks - should be deprecated ? (obsoleted in
598
# 0.15^H^H^H^H 2.5.0)
599
post_commit = self.config_stack.get('post_commit')
600
if post_commit is not None:
601
hooks = post_commit.split(' ')
602
# this would be nicer with twisted.python.reflect.namedAny
604
result = eval(hook + '(branch, rev_id)',
605
{'branch': self.branch,
607
'rev_id': self.rev_id})
608
# process new style post commit hooks
609
self._process_hooks("post_commit", old_revno, new_revno)
611
def _process_hooks(self, hook_name, old_revno, new_revno):
612
if not Branch.hooks[hook_name]:
615
# new style commit hooks:
616
if not self.bound_branch:
617
hook_master = self.branch
620
hook_master = self.master_branch
621
hook_local = self.branch
622
# With bound branches, when the master is behind the local branch,
623
# the 'old_revno' and old_revid values here are incorrect.
624
# XXX: FIXME ^. RBC 20060206
626
old_revid = self.parents[0]
628
old_revid = breezy.revision.NULL_REVISION
630
if hook_name == "pre_commit":
631
future_tree = self.builder.revision_tree()
632
tree_delta = future_tree.changes_from(self.basis_tree,
635
for hook in Branch.hooks[hook_name]:
636
# show the running hook in the progress bar. As hooks may
637
# end up doing nothing (e.g. because they are not configured by
638
# the user) this is still showing progress, not showing overall
639
# actions - its up to each plugin to show a UI if it want's to
640
# (such as 'Emailing diff to foo@example.com').
641
self.pb_stage_name = "Running %s hooks [%s]" % \
642
(hook_name, Branch.hooks.get_hook_name(hook))
643
self._emit_progress()
644
if 'hooks' in debug.debug_flags:
645
mutter("Invoking commit hook: %r", hook)
646
if hook_name == "post_commit":
647
hook(hook_local, hook_master, old_revno, old_revid, new_revno,
649
elif hook_name == "pre_commit":
650
hook(hook_local, hook_master,
651
old_revno, old_revid, new_revno, self.rev_id,
652
tree_delta, future_tree)
654
def _update_builder_with_changes(self):
655
"""Update the commit builder with the data about what has changed.
657
specific_files = self.specific_files
658
mutter("Selecting files for commit with filter %r", specific_files)
661
iter_changes = self.work_tree.iter_changes(
662
self.basis_tree, specific_files=specific_files)
664
iter_changes = filter_excluded(iter_changes, self.exclude)
665
iter_changes = self._filter_iter_changes(iter_changes)
666
for path, fs_hash in self.builder.record_iter_changes(
667
self.work_tree, self.basis_revid, iter_changes):
668
self.work_tree._observed_sha1(path, fs_hash)
670
def _filter_iter_changes(self, iter_changes):
671
"""Process iter_changes.
673
This method reports on the changes in iter_changes to the user, and
674
converts 'missing' entries in the iter_changes iterator to 'deleted'
675
entries. 'missing' entries have their
677
:param iter_changes: An iter_changes to process.
678
:return: A generator of changes.
680
reporter = self.reporter
681
report_changes = reporter.is_verbose()
683
for change in iter_changes:
685
old_path = change.path[0]
686
new_path = change.path[1]
687
versioned = change.versioned[1]
688
kind = change.kind[1]
689
versioned = change.versioned[1]
690
if kind is None and versioned:
693
reporter.missing(new_path)
694
if change.kind[0] == 'symlink' and not self.work_tree.supports_symlinks():
695
trace.warning('Ignoring "%s" as symlinks are not '
696
'supported on this filesystem.' % (change.path[0],))
698
deleted_paths.append(change.path[1])
699
# Reset the new path (None) and new versioned flag (False)
700
change = change.discard_new()
701
new_path = change.path[1]
703
elif kind == 'tree-reference':
704
if self.recursive == 'down':
705
self._commit_nested_tree(change.path[1])
706
if change.versioned[0] or change.versioned[1]:
710
reporter.deleted(old_path)
711
elif old_path is None:
712
reporter.snapshot_change(gettext('added'), new_path)
713
elif old_path != new_path:
714
reporter.renamed(gettext('renamed'),
718
or self.work_tree.branch.repository._format.rich_root_data):
719
# Don't report on changes to '' in non rich root
721
reporter.snapshot_change(
722
gettext('modified'), new_path)
723
self._next_progress_entry()
724
# Unversion files that were found to be deleted
725
self.deleted_paths = deleted_paths
727
def _check_strict(self):
728
# XXX: when we use iter_changes this would likely be faster if
729
# iter_changes would check for us (even in the presence of
732
# raise an exception as soon as we find a single unknown.
733
for unknown in self.work_tree.unknowns():
734
raise StrictCommitFailed()
736
def _commit_nested_tree(self, path):
737
"Commit a nested tree."
738
sub_tree = self.work_tree.get_nested_tree(path)
739
# FIXME: be more comprehensive here:
740
# this works when both trees are in --trees repository,
741
# but when both are bound to a different repository,
742
# it fails; a better way of approaching this is to
743
# finally implement the explicit-caches approach design
744
# a while back - RBC 20070306.
745
if sub_tree.branch.repository.has_same_location(
746
self.work_tree.branch.repository):
747
sub_tree.branch.repository = \
748
self.work_tree.branch.repository
750
return sub_tree.commit(message=None, revprops=self.revprops,
751
recursive=self.recursive,
752
message_callback=self.message_callback,
753
timestamp=self.timestamp,
754
timezone=self.timezone,
755
committer=self.committer,
756
allow_pointless=self.allow_pointless,
757
strict=self.strict, verbose=self.verbose,
758
local=self.local, reporter=self.reporter)
759
except PointlessCommit:
760
return self.work_tree.get_reference_revision(path)
762
def _set_progress_stage(self, name, counter=False):
763
"""Set the progress stage and emit an update to the progress bar."""
764
self.pb_stage_name = name
765
self.pb_stage_count += 1
767
self.pb_entries_count = 0
769
self.pb_entries_count = None
770
self._emit_progress()
772
def _next_progress_entry(self):
773
"""Emit an update to the progress bar and increment the entry count."""
774
self.pb_entries_count += 1
775
self._emit_progress()
777
def _emit_progress(self):
778
if self.pb_entries_count is not None:
779
text = gettext("{0} [{1}] - Stage").format(self.pb_stage_name,
780
self.pb_entries_count)
782
text = gettext("%s - Stage") % (self.pb_stage_name, )
783
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)