15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
# XXX: Can we do any better about making interrupted commits change
19
# nothing? Perhaps the best approach is to integrate commit of
20
# AtomicFiles with releasing the lock on the Branch.
22
# TODO: Separate 'prepare' phase where we find a list of potentially
23
# committed files. We then can then pause the commit to prompt for a
24
# commit message, knowing the summary will be the same as what's
25
# actually used for the commit. (But perhaps simpler to simply get
26
# the tree status, then use that for a selective commit?)
28
18
# The newly committed revision is going to have a shape corresponding
29
19
# to that of the working inventory. Files that are not in the
30
20
# working tree and that were in the predecessor are reported as
56
46
# merges from, then it should still be reported as newly added
57
47
# relative to the basis revision.
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.
49
# TODO: Change the parameter 'rev_id' to 'revision_id' to be consistent with
50
# the rest of the code; add a deprecation of the old name.
72
from binascii import hexlify
73
57
from cStringIO import StringIO
75
from bzrlib.atomicfile import AtomicFile
76
from bzrlib.osutils import (local_time_offset,
77
rand_bytes, compact_date,
78
kind_marker, is_inside_any, quotefn,
79
sha_file, isdir, isfile,
81
63
import bzrlib.config
82
import bzrlib.errors as errors
83
64
from bzrlib.errors import (BzrError, PointlessCommit,
88
from bzrlib.revision import Revision
68
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any,
69
is_inside_or_parent_of_any,
70
quotefn, sha_file, split_lines)
89
71
from bzrlib.testament import Testament
90
72
from bzrlib.trace import mutter, note, warning
91
73
from bzrlib.xml5 import serializer_v5
92
from bzrlib.inventory import Inventory, ROOT_ID
93
from bzrlib.symbol_versioning import *
74
from bzrlib.inventory import Inventory, InventoryEntry
75
from bzrlib import symbol_versioning
76
from bzrlib.symbol_versioning import (deprecated_passed,
94
79
from bzrlib.workingtree import WorkingTree
97
@deprecated_function(zero_seven)
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.
105
## XXX: Remove this in favor of Branch.commit?
106
Commit().commit(*args, **kwargs)
109
82
class NullCommitReporter(object):
110
83
"""I report on progress of a commit."""
124
97
def missing(self, path):
100
def renamed(self, change, old_path, new_path):
128
104
class ReportCommitToLog(NullCommitReporter):
106
# this may be more useful if 'note' was replaced by an overridable
107
# method on self, which would allow more trivial subclassing.
108
# alternative, a callable could be passed in, allowing really trivial
109
# reuse for some uis. RBC 20060511
130
111
def snapshot_change(self, change, path):
131
112
if change == 'unchanged':
114
if change == 'added' and path == '':
133
116
note("%s %s", change, path)
135
118
def completed(self, revno, rev_id):
225
212
self.work_tree = working_tree
226
213
self.branch = self.work_tree.branch
228
raise BzrError("The message keyword parameter is required for commit().")
214
if message_callback is None:
215
if message is not None:
216
if isinstance(message, str):
217
message = message.decode(bzrlib.user_encoding)
218
message_callback = lambda x: message
220
raise BzrError("The message or message_callback keyword"
221
" parameter is required for commit().")
230
self.weave_store = self.branch.repository.weave_store
231
223
self.bound_branch = None
232
224
self.local = local
233
225
self.master_branch = None
234
226
self.master_locked = False
236
228
self.specific_files = specific_files
237
229
self.allow_pointless = allow_pointless
239
if revprops is not None:
240
self.revprops.update(revprops)
242
231
if reporter is None and self.reporter is None:
243
232
self.reporter = NullCommitReporter()
245
234
self.reporter = reporter
247
236
self.work_tree.lock_write()
237
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
239
# Cannot commit with conflicts present.
240
if len(self.work_tree.conflicts())>0:
241
raise ConflictsInTree
249
243
# setup the bound branch variables as needed.
250
244
self._check_bound_branch()
252
246
# check for out of date working trees
253
# if we are bound, then self.branch is the master branch and this
254
# test is thus all we need.
255
if self.work_tree.last_revision() != self.master_branch.last_revision():
248
first_tree_parent = self.work_tree.get_parent_ids()[0]
250
# if there are no parents, treat our parent as 'None'
251
# this is so that we still consier the master branch
252
# - in a checkout scenario the tree may have no
253
# parents but the branch may do.
254
first_tree_parent = None
255
master_last = self.master_branch.last_revision()
256
if (master_last is not None and
257
master_last != first_tree_parent):
256
258
raise errors.OutOfDateTree(self.work_tree)
259
261
# raise an exception as soon as we find a single unknown.
260
262
for unknown in self.work_tree.unknowns():
261
263
raise StrictCommitFailed()
263
if timestamp is None:
264
self.timestamp = time.time()
266
self.timestamp = long(timestamp)
268
265
if self.config is None:
269
self.config = bzrlib.config.BranchConfig(self.branch)
272
self.rev_id = _gen_revision_id(self.config, self.timestamp)
276
if committer is None:
277
self.committer = self.config.username()
279
assert isinstance(committer, basestring), type(committer)
280
self.committer = committer
283
self.timezone = local_time_offset()
285
self.timezone = int(timezone)
287
if isinstance(message, str):
288
message = message.decode(bzrlib.user_encoding)
289
assert isinstance(message, unicode), type(message)
290
self.message = message
291
self._escape_commit_message()
266
self.config = self.branch.get_config()
293
268
self.work_inv = self.work_tree.inventory
294
269
self.basis_tree = self.work_tree.basis_tree()
295
270
self.basis_inv = self.basis_tree.inventory
271
if specific_files is not None:
272
# Ensure specified files are versioned
273
# (We don't actually need the ids here)
274
tree.find_ids_across_trees(specific_files,
275
[self.basis_tree, self.work_tree])
276
# one to finish, one for rev and inventory, and one for each
277
# inventory entry, and the same for the new inventory.
278
# note that this estimate is too long when we do a partial tree
279
# commit which excludes some new files from being considered.
280
# The estimate is corrected when we populate the new inv.
281
self.pb_total = len(self.work_inv) + 5
297
284
self._gather_parents()
298
285
if len(self.parents) > 1 and self.specific_files:
299
raise NotImplementedError('selected-file commit of merges is not supported yet')
300
self._check_parents_present()
286
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
289
self.builder = self.branch.get_commit_builder(self.parents,
290
self.config, timestamp, timezone, committer, revprops, rev_id)
302
292
self._remove_deleted()
303
293
self._populate_new_inv()
304
self._store_snapshot()
305
294
self._report_deletes()
307
if not (self.allow_pointless
308
or len(self.parents) > 1
309
or self.new_inv != self.basis_inv):
310
raise PointlessCommit()
312
if len(self.work_tree.conflicts())>0:
313
raise ConflictsInTree
315
self.inv_sha1 = self.branch.repository.add_inventory(
320
self._make_revision()
296
self._check_pointless()
298
self._emit_progress_update()
299
# TODO: Now the new inventory is known, check for conflicts and
300
# prompt the user for a commit message.
301
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
302
# weave lines, because nothing should be recorded until it is known
303
# that commit will succeed.
304
self.builder.finish_inventory()
305
self._emit_progress_update()
306
message = message_callback(self)
307
assert isinstance(message, unicode), type(message)
308
self.message = message
309
self._escape_commit_message()
311
self.rev_id = self.builder.commit(self.message)
312
self._emit_progress_update()
321
313
# revision data is in the local branch now.
323
315
# upload revision data to the master.
324
# this will propogate merged revisions too if needed.
316
# this will propagate merged revisions too if needed.
325
317
if self.bound_branch:
326
318
self.master_branch.repository.fetch(self.branch.repository,
327
319
revision_id=self.rev_id)
346
338
{'branch':self.branch,
348
340
'rev_id':self.rev_id})
341
self._emit_progress_update()
350
self._cleanup_bound_branch()
351
self.work_tree.unlock()
346
def _any_real_changes(self):
347
"""Are there real changes between new_inventory and basis?
349
For trees without rich roots, inv.root.revision changes every commit.
350
But if that is the only change, we want to treat it as though there
353
new_entries = self.builder.new_inventory.iter_entries()
354
basis_entries = self.basis_inv.iter_entries()
355
new_path, new_root_ie = new_entries.next()
356
basis_path, basis_root_ie = basis_entries.next()
358
# This is a copy of InventoryEntry.__eq__ only leaving out .revision
359
def ie_equal_no_revision(this, other):
360
return ((this.file_id == other.file_id)
361
and (this.name == other.name)
362
and (this.symlink_target == other.symlink_target)
363
and (this.text_sha1 == other.text_sha1)
364
and (this.text_size == other.text_size)
365
and (this.text_id == other.text_id)
366
and (this.parent_id == other.parent_id)
367
and (this.kind == other.kind)
368
and (this.executable == other.executable)
370
if not ie_equal_no_revision(new_root_ie, basis_root_ie):
373
for new_ie, basis_ie in zip(new_entries, basis_entries):
374
if new_ie != basis_ie:
377
# No actual changes present
380
def _check_pointless(self):
381
if self.allow_pointless:
383
# A merge with no effect on files
384
if len(self.parents) > 1:
386
# work around the fact that a newly-initted tree does differ from its
388
if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
389
raise PointlessCommit()
390
# Shortcut, if the number of entries changes, then we obviously have
392
if len(self.builder.new_inventory) != len(self.basis_inv):
394
# If length == 1, then we only have the root entry. Which means
395
# that there is no real difference (only the root could be different)
396
if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
398
raise PointlessCommit()
353
400
def _check_bound_branch(self):
354
401
"""Check to see if the local branch is bound.
391
438
self.bound_branch = self.branch
392
439
self.master_branch.lock_write()
393
440
self.master_locked = True
395
#### # Check to see if we have any pending merges. If we do
396
#### # those need to be pushed into the master branch
397
#### pending_merges = self.work_tree.pending_merges()
398
#### if pending_merges:
399
#### for revision_id in pending_merges:
400
#### self.master_branch.repository.fetch(self.bound_branch.repository,
401
#### revision_id=revision_id)
443
"""Cleanup any open locks, progress bars etc."""
444
cleanups = [self._cleanup_bound_branch,
445
self.work_tree.unlock,
447
found_exception = None
448
for cleanup in cleanups:
451
# we want every cleanup to run no matter what.
452
# so we have a catchall here, but we will raise the
453
# last encountered exception up the stack: and
454
# typically this will be useful enough.
457
if found_exception is not None:
458
# don't do a plan raise, because the last exception may have been
459
# trashed, e is our sure-to-work exception even though it loses the
460
# full traceback. XXX: RBC 20060421 perhaps we could check the
461
# exc_info and if its the same one do a plain raise otherwise
462
# 'raise e' as we do now.
403
465
def _cleanup_bound_branch(self):
404
466
"""Executed at the end of a try/finally to cleanup a bound branch.
428
495
def _gather_parents(self):
429
496
"""Record the parents of a merge for merge detection."""
430
pending_merges = self.work_tree.pending_merges()
497
# TODO: Make sure that this list doesn't contain duplicate
498
# entries and the order is preserved when doing this.
499
self.parents = self.work_tree.get_parent_ids()
432
500
self.parent_invs = []
433
self.present_parents = []
434
precursor_id = self.branch.last_revision()
436
self.parents.append(precursor_id)
437
self.parents += pending_merges
438
501
for revision in self.parents:
439
502
if self.branch.repository.has_revision(revision):
503
mutter('commit parent revision {%s}', revision)
440
504
inventory = self.branch.repository.get_inventory(revision)
441
505
self.parent_invs.append(inventory)
442
self.present_parents.append(revision)
444
def _check_parents_present(self):
445
for parent_id in self.parents:
446
mutter('commit parent revision {%s}', parent_id)
447
if not self.branch.repository.has_revision(parent_id):
448
if parent_id == self.branch.last_revision():
449
warning("parent is missing %r", parent_id)
450
raise HistoryMissing(self.branch, 'revision', parent_id)
452
mutter("commit will ghost revision %r", parent_id)
454
def _make_revision(self):
455
"""Record a new revision object for this commit."""
456
rev = Revision(timestamp=self.timestamp,
457
timezone=self.timezone,
458
committer=self.committer,
459
message=self.message,
460
inventory_sha1=self.inv_sha1,
461
revision_id=self.rev_id,
462
properties=self.revprops)
463
rev.parent_ids = self.parents
464
self.branch.repository.add_revision(self.rev_id, rev, self.new_inv, self.config)
507
mutter('commit parent ghost revision {%s}', revision)
466
509
def _remove_deleted(self):
467
510
"""Remove deleted files from the working inventories.
477
520
specific = self.specific_files
522
deleted_paths = set()
479
523
for path, ie in self.work_inv.iter_entries():
524
if is_inside_any(deleted_paths, path):
525
# The tree will delete the required ids recursively.
480
527
if specific and not is_inside_any(specific, path):
482
529
if not self.work_tree.has_filename(path):
530
deleted_paths.add(path)
483
531
self.reporter.missing(path)
484
deleted_ids.append((path, ie.file_id))
486
deleted_ids.sort(reverse=True)
487
for path, file_id in deleted_ids:
488
del self.work_inv[file_id]
489
self.work_tree._write_inventory(self.work_inv)
491
def _store_snapshot(self):
492
"""Pass over inventory and record a snapshot.
494
Entries get a new revision when they are modified in
495
any way, which includes a merge with a new set of
496
parents that have the same entry.
498
# XXX: Need to think more here about when the user has
499
# made a specific decision on a particular value -- c.f.
501
for path, ie in self.new_inv.iter_entries():
502
previous_entries = ie.find_previous_heads(
505
self.branch.repository.get_transaction())
506
if ie.revision is None:
507
change = ie.snapshot(self.rev_id, path, previous_entries,
508
self.work_tree, self.weave_store,
509
self.branch.get_transaction())
512
self.reporter.snapshot_change(change, path)
532
deleted_ids.append(ie.file_id)
533
self.work_tree.unversion(deleted_ids)
514
535
def _populate_new_inv(self):
515
536
"""Build revision inventory.
521
542
None; inventory entries that are carried over untouched have their
522
543
revision set to their prior value.
545
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
546
# results to create a new inventory at the same time, which results
547
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
524
549
mutter("Selecting files for commit with filter %s", self.specific_files)
525
self.new_inv = Inventory(revision_id=self.rev_id)
526
for path, new_ie in self.work_inv.iter_entries():
550
assert self.work_inv.root is not None
551
entries = self.work_inv.iter_entries()
552
if not self.builder.record_root_entry:
553
symbol_versioning.warn('CommitBuilders should support recording'
554
' the root entry as of bzr 0.10.', DeprecationWarning,
556
self.builder.new_inventory.add(self.basis_inv.root.copy())
558
self._emit_progress_update()
559
for path, new_ie in entries:
560
self._emit_progress_update()
527
561
file_id = new_ie.file_id
528
mutter('check %s {%s}', path, new_ie.file_id)
529
if self.specific_files:
530
if not is_inside_any(self.specific_files, path):
531
mutter('%s not selected for commit', path)
532
self._carry_entry(file_id)
562
# mutter('check %s {%s}', path, file_id)
563
if (not self.specific_files or
564
is_inside_or_parent_of_any(self.specific_files, path)):
565
# mutter('%s selected for commit', path)
569
# mutter('%s not selected for commit', path)
570
if self.basis_inv.has_id(file_id):
571
ie = self.basis_inv[file_id].copy()
573
# this entry is new and not being committed
535
# this is selected, ensure its parents are too.
536
parent_id = new_ie.parent_id
537
while parent_id != ROOT_ID:
538
if not self.new_inv.has_id(parent_id):
539
ie = self._select_entry(self.work_inv[parent_id])
540
mutter('%s selected for commit because of %s',
541
self.new_inv.id2path(parent_id), path)
543
ie = self.new_inv[parent_id]
544
if ie.revision is not None:
546
mutter('%s selected for commit because of %s',
547
self.new_inv.id2path(parent_id), path)
548
parent_id = ie.parent_id
549
mutter('%s selected for commit', path)
550
self._select_entry(new_ie)
552
def _select_entry(self, new_ie):
553
"""Make new_ie be considered for committing."""
559
def _carry_entry(self, file_id):
560
"""Carry the file unchanged from the basis revision."""
561
if self.basis_inv.has_id(file_id):
562
self.new_inv.add(self.basis_inv[file_id].copy())
575
self.builder.record_entry_contents(ie, self.parent_invs,
576
path, self.work_tree)
577
# describe the nature of the change that has occurred relative to
578
# the basis inventory.
579
if (self.basis_inv.has_id(ie.file_id)):
580
basis_ie = self.basis_inv[ie.file_id]
583
change = ie.describe_change(basis_ie, ie)
584
if change in (InventoryEntry.RENAMED,
585
InventoryEntry.MODIFIED_AND_RENAMED):
586
old_path = self.basis_inv.id2path(ie.file_id)
587
self.reporter.renamed(change, old_path, path)
589
self.reporter.snapshot_change(change, path)
591
if not self.specific_files:
594
# ignore removals that don't match filespec
595
for path, new_ie in self.basis_inv.iter_entries():
596
if new_ie.file_id in self.work_inv:
598
if is_inside_any(self.specific_files, path):
602
self.builder.record_entry_contents(ie, self.parent_invs, path,
605
def _emit_progress_update(self):
606
"""Emit an update to the progress bar."""
607
self.pb.update("Committing", self.pb_count, self.pb_total)
564
610
def _report_deletes(self):
565
for file_id in self.basis_inv:
566
if file_id not in self.new_inv:
567
self.reporter.deleted(self.basis_inv.id2path(file_id))
569
def _gen_revision_id(config, when):
570
"""Return new revision-id."""
571
s = '%s-%s-' % (config.user_email(), compact_date(when))
572
s += hexlify(rand_bytes(8))
611
for path, ie in self.basis_inv.iter_entries():
612
if ie.file_id not in self.builder.new_inventory:
613
self.reporter.deleted(path)