63
62
# TODO: If commit fails, leave the message in a file somewhere.
64
# TODO: Change the parameter 'rev_id' to 'revision_id' to be consistent with
65
# the rest of the code; add a deprecation of the old name.
72
from binascii import hexlify
73
72
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
78
import bzrlib.config
82
import bzrlib.errors as errors
83
79
from bzrlib.errors import (BzrError, PointlessCommit,
88
from bzrlib.revision import Revision
83
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any,
84
is_inside_or_parent_of_any,
85
quotefn, sha_file, split_lines)
89
86
from bzrlib.testament import Testament
90
87
from bzrlib.trace import mutter, note, warning
91
88
from bzrlib.xml5 import serializer_v5
92
from bzrlib.inventory import Inventory, ROOT_ID, InventoryEntry
93
from bzrlib.symbol_versioning import *
89
from bzrlib.inventory import Inventory, InventoryEntry
90
from bzrlib import symbol_versioning
91
from bzrlib.symbol_versioning import (deprecated_passed,
94
94
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
97
class NullCommitReporter(object):
110
98
"""I report on progress of a commit."""
266
252
self._check_bound_branch()
268
254
# check for out of date working trees
269
# if we are bound, then self.branch is the master branch and this
270
# test is thus all we need.
271
if self.work_tree.last_revision() != self.master_branch.last_revision():
256
first_tree_parent = self.work_tree.get_parent_ids()[0]
258
# if there are no parents, treat our parent as 'None'
259
# this is so that we still consier the master branch
260
# - in a checkout scenario the tree may have no
261
# parents but the branch may do.
262
first_tree_parent = None
263
master_last = self.master_branch.last_revision()
264
if (master_last is not None and
265
master_last != first_tree_parent):
272
266
raise errors.OutOfDateTree(self.work_tree)
275
269
# raise an exception as soon as we find a single unknown.
276
270
for unknown in self.work_tree.unknowns():
277
271
raise StrictCommitFailed()
279
if timestamp is None:
280
self.timestamp = time.time()
282
self.timestamp = long(timestamp)
284
273
if self.config is None:
285
self.config = bzrlib.config.BranchConfig(self.branch)
288
self.rev_id = _gen_revision_id(self.config, self.timestamp)
292
if committer is None:
293
self.committer = self.config.username()
295
assert isinstance(committer, basestring), type(committer)
296
self.committer = committer
299
self.timezone = local_time_offset()
301
self.timezone = int(timezone)
274
self.config = self.branch.get_config()
303
276
if isinstance(message, str):
304
277
message = message.decode(bzrlib.user_encoding)
305
278
assert isinstance(message, unicode), type(message)
309
282
self.work_inv = self.work_tree.inventory
310
283
self.basis_tree = self.work_tree.basis_tree()
311
284
self.basis_inv = self.basis_tree.inventory
285
if specific_files is not None:
286
# Ensure specified files are versioned
287
# (We don't actually need the ids here)
288
tree.find_ids_across_trees(specific_files,
289
[self.basis_tree, self.work_tree])
312
290
# one to finish, one for rev and inventory, and one for each
313
291
# inventory entry, and the same for the new inventory.
314
292
# note that this estimate is too long when we do a partial tree
315
293
# commit which excludes some new files from being considered.
316
294
# The estimate is corrected when we populate the new inv.
317
self.pb_total = len(self.basis_inv) + len(self.work_inv) + 3 - 1
295
self.pb_total = len(self.work_inv) + 5
318
296
self.pb_count = 0
320
298
self._gather_parents()
321
299
if len(self.parents) > 1 and self.specific_files:
322
300
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
323
301
self.specific_files)
324
self._check_parents_present()
303
self.builder = self.branch.get_commit_builder(self.parents,
304
self.config, timestamp, timezone, committer, revprops, rev_id)
326
306
self._remove_deleted()
327
307
self._populate_new_inv()
328
self._store_snapshot()
329
308
self._report_deletes()
331
if not (self.allow_pointless
332
or len(self.parents) > 1
333
or self.new_inv != self.basis_inv):
334
raise PointlessCommit()
310
self._check_pointless()
336
312
self._emit_progress_update()
337
self.inv_sha1 = self.branch.repository.add_inventory(
342
self._emit_progress_update()
343
self._make_revision()
313
# TODO: Now the new inventory is known, check for conflicts and
314
# prompt the user for a commit message.
315
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
316
# weave lines, because nothing should be recorded until it is known
317
# that commit will succeed.
318
self.builder.finish_inventory()
319
self._emit_progress_update()
320
self.rev_id = self.builder.commit(self.message)
321
self._emit_progress_update()
344
322
# revision data is in the local branch now.
346
324
# upload revision data to the master.
347
# this will propogate merged revisions too if needed.
325
# this will propagate merged revisions too if needed.
348
326
if self.bound_branch:
349
327
self.master_branch.repository.fetch(self.branch.repository,
350
328
revision_id=self.rev_id)
372
350
self._emit_progress_update()
355
def _any_real_changes(self):
356
"""Are there real changes between new_inventory and basis?
358
For trees without rich roots, inv.root.revision changes every commit.
359
But if that is the only change, we want to treat it as though there
362
new_entries = self.builder.new_inventory.iter_entries()
363
basis_entries = self.basis_inv.iter_entries()
364
new_path, new_root_ie = new_entries.next()
365
basis_path, basis_root_ie = basis_entries.next()
367
# This is a copy of InventoryEntry.__eq__ only leaving out .revision
368
def ie_equal_no_revision(this, other):
369
return ((this.file_id == other.file_id)
370
and (this.name == other.name)
371
and (this.symlink_target == other.symlink_target)
372
and (this.text_sha1 == other.text_sha1)
373
and (this.text_size == other.text_size)
374
and (this.text_id == other.text_id)
375
and (this.parent_id == other.parent_id)
376
and (this.kind == other.kind)
377
and (this.executable == other.executable)
379
if not ie_equal_no_revision(new_root_ie, basis_root_ie):
382
for new_ie, basis_ie in zip(new_entries, basis_entries):
383
if new_ie != basis_ie:
386
# No actual changes present
389
def _check_pointless(self):
390
if self.allow_pointless:
392
# A merge with no effect on files
393
if len(self.parents) > 1:
395
# work around the fact that a newly-initted tree does differ from its
397
if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
398
raise PointlessCommit()
399
# Shortcut, if the number of entries changes, then we obviously have
401
if len(self.builder.new_inventory) != len(self.basis_inv):
403
# If length == 1, then we only have the root entry. Which means
404
# that there is no real difference (only the root could be different)
405
if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
407
raise PointlessCommit()
376
409
def _check_bound_branch(self):
377
410
"""Check to see if the local branch is bound.
479
504
def _gather_parents(self):
480
505
"""Record the parents of a merge for merge detection."""
481
pending_merges = self.work_tree.pending_merges()
506
# TODO: Make sure that this list doesn't contain duplicate
507
# entries and the order is preserved when doing this.
508
self.parents = self.work_tree.get_parent_ids()
483
509
self.parent_invs = []
484
self.present_parents = []
485
precursor_id = self.branch.last_revision()
487
self.parents.append(precursor_id)
488
self.parents += pending_merges
489
510
for revision in self.parents:
490
511
if self.branch.repository.has_revision(revision):
512
mutter('commit parent revision {%s}', revision)
491
513
inventory = self.branch.repository.get_inventory(revision)
492
514
self.parent_invs.append(inventory)
493
self.present_parents.append(revision)
495
def _check_parents_present(self):
496
for parent_id in self.parents:
497
mutter('commit parent revision {%s}', parent_id)
498
if not self.branch.repository.has_revision(parent_id):
499
if parent_id == self.branch.last_revision():
500
warning("parent is missing %r", parent_id)
501
raise HistoryMissing(self.branch, 'revision', parent_id)
503
mutter("commit will ghost revision %r", parent_id)
505
def _make_revision(self):
506
"""Record a new revision object for this commit."""
507
rev = Revision(timestamp=self.timestamp,
508
timezone=self.timezone,
509
committer=self.committer,
510
message=self.message,
511
inventory_sha1=self.inv_sha1,
512
revision_id=self.rev_id,
513
properties=self.revprops)
514
rev.parent_ids = self.parents
515
self.branch.repository.add_revision(self.rev_id, rev, self.new_inv, self.config)
516
mutter('commit parent ghost revision {%s}', revision)
517
518
def _remove_deleted(self):
518
519
"""Remove deleted files from the working inventories.
528
529
specific = self.specific_files
531
deleted_paths = set()
530
532
for path, ie in self.work_inv.iter_entries():
533
if is_inside_any(deleted_paths, path):
534
# The tree will delete the required ids recursively.
531
536
if specific and not is_inside_any(specific, path):
533
538
if not self.work_tree.has_filename(path):
539
deleted_paths.add(path)
534
540
self.reporter.missing(path)
535
deleted_ids.append((path, ie.file_id))
537
deleted_ids.sort(reverse=True)
538
for path, file_id in deleted_ids:
539
del self.work_inv[file_id]
540
self.work_tree._write_inventory(self.work_inv)
542
def _store_snapshot(self):
543
"""Pass over inventory and record a snapshot.
545
Entries get a new revision when they are modified in
546
any way, which includes a merge with a new set of
547
parents that have the same entry.
549
# XXX: Need to think more here about when the user has
550
# made a specific decision on a particular value -- c.f.
553
# iter_entries does not visit the ROOT_ID node so we need to call
554
# self._emit_progress_update once by hand.
555
self._emit_progress_update()
556
for path, ie in self.new_inv.iter_entries():
557
self._emit_progress_update()
558
previous_entries = ie.find_previous_heads(
561
self.branch.repository.get_transaction())
562
if ie.revision is None:
563
# we are creating a new revision for ie in the history store
565
ie.snapshot(self.rev_id, path, previous_entries,
566
self.work_tree, self.weave_store,
567
self.branch.repository.get_transaction())
568
# describe the nature of the change that has occured relative to
569
# the basis inventory.
570
if (self.basis_inv.has_id(ie.file_id)):
571
basis_ie = self.basis_inv[ie.file_id]
574
change = ie.describe_change(basis_ie, ie)
575
if change in (InventoryEntry.RENAMED,
576
InventoryEntry.MODIFIED_AND_RENAMED):
577
old_path = self.basis_inv.id2path(ie.file_id)
578
self.reporter.renamed(change, old_path, path)
580
self.reporter.snapshot_change(change, path)
541
deleted_ids.append(ie.file_id)
542
self.work_tree.unversion(deleted_ids)
582
544
def _populate_new_inv(self):
583
545
"""Build revision inventory.
589
551
None; inventory entries that are carried over untouched have their
590
552
revision set to their prior value.
554
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
555
# results to create a new inventory at the same time, which results
556
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
592
558
mutter("Selecting files for commit with filter %s", self.specific_files)
593
self.new_inv = Inventory(revision_id=self.rev_id)
594
# iter_entries does not visit the ROOT_ID node so we need to call
595
# self._emit_progress_update once by hand.
596
self._emit_progress_update()
597
for path, new_ie in self.work_inv.iter_entries():
559
assert self.work_inv.root is not None
560
entries = self.work_inv.iter_entries()
561
if not self.builder.record_root_entry:
562
symbol_versioning.warn('CommitBuilders should support recording'
563
' the root entry as of bzr 0.10.', DeprecationWarning,
565
self.builder.new_inventory.add(self.basis_inv.root.copy())
567
self._emit_progress_update()
568
for path, new_ie in entries:
598
569
self._emit_progress_update()
599
570
file_id = new_ie.file_id
600
mutter('check %s {%s}', path, new_ie.file_id)
601
if self.specific_files:
602
if not is_inside_any(self.specific_files, path):
603
mutter('%s not selected for commit', path)
604
self._carry_entry(file_id)
571
# mutter('check %s {%s}', path, file_id)
572
if (not self.specific_files or
573
is_inside_or_parent_of_any(self.specific_files, path)):
574
# mutter('%s selected for commit', path)
578
# mutter('%s not selected for commit', path)
579
if self.basis_inv.has_id(file_id):
580
ie = self.basis_inv[file_id].copy()
582
# this entry is new and not being committed
607
# this is selected, ensure its parents are too.
608
parent_id = new_ie.parent_id
609
while parent_id != ROOT_ID:
610
if not self.new_inv.has_id(parent_id):
611
ie = self._select_entry(self.work_inv[parent_id])
612
mutter('%s selected for commit because of %s',
613
self.new_inv.id2path(parent_id), path)
615
ie = self.new_inv[parent_id]
616
if ie.revision is not None:
618
mutter('%s selected for commit because of %s',
619
self.new_inv.id2path(parent_id), path)
620
parent_id = ie.parent_id
621
mutter('%s selected for commit', path)
622
self._select_entry(new_ie)
584
self.builder.record_entry_contents(ie, self.parent_invs,
585
path, self.work_tree)
586
# describe the nature of the change that has occurred relative to
587
# the basis inventory.
588
if (self.basis_inv.has_id(ie.file_id)):
589
basis_ie = self.basis_inv[ie.file_id]
592
change = ie.describe_change(basis_ie, ie)
593
if change in (InventoryEntry.RENAMED,
594
InventoryEntry.MODIFIED_AND_RENAMED):
595
old_path = self.basis_inv.id2path(ie.file_id)
596
self.reporter.renamed(change, old_path, path)
598
self.reporter.snapshot_change(change, path)
600
if not self.specific_files:
603
# ignore removals that don't match filespec
604
for path, new_ie in self.basis_inv.iter_entries():
605
if new_ie.file_id in self.work_inv:
607
if is_inside_any(self.specific_files, path):
611
self.builder.record_entry_contents(ie, self.parent_invs, path,
624
614
def _emit_progress_update(self):
625
615
"""Emit an update to the progress bar."""
626
616
self.pb.update("Committing", self.pb_count, self.pb_total)
627
617
self.pb_count += 1
629
def _select_entry(self, new_ie):
630
"""Make new_ie be considered for committing."""
636
def _carry_entry(self, file_id):
637
"""Carry the file unchanged from the basis revision."""
638
if self.basis_inv.has_id(file_id):
639
self.new_inv.add(self.basis_inv[file_id].copy())
641
# this entry is new and not being committed
644
619
def _report_deletes(self):
645
620
for path, ie in self.basis_inv.iter_entries():
646
if ie.file_id not in self.new_inv:
621
if ie.file_id not in self.builder.new_inventory:
647
622
self.reporter.deleted(path)
649
def _gen_revision_id(config, when):
650
"""Return new revision-id."""
651
s = '%s-%s-' % (config.user_email(), compact_date(when))
652
s += hexlify(rand_bytes(8))