72
from binascii import hexlify
73
70
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
72
import bzrlib.config
82
73
import bzrlib.errors as errors
83
74
from bzrlib.errors import (BzrError, PointlessCommit,
88
from bzrlib.revision import Revision
78
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any,
79
is_inside_or_parent_of_any,
80
quotefn, sha_file, split_lines)
89
81
from bzrlib.testament import Testament
90
82
from bzrlib.trace import mutter, note, warning
91
83
from bzrlib.xml5 import serializer_v5
92
84
from bzrlib.inventory import Inventory, ROOT_ID, InventoryEntry
93
from bzrlib.symbol_versioning import *
85
from bzrlib import symbol_versioning
86
from bzrlib.symbol_versioning import (deprecated_passed,
94
89
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
92
class NullCommitReporter(object):
110
93
"""I report on progress of a commit."""
266
245
self._check_bound_branch()
268
247
# 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():
249
first_tree_parent = self.work_tree.get_parent_ids()[0]
251
# if there are no parents, treat our parent as 'None'
252
# this is so that we still consier the master branch
253
# - in a checkout scenario the tree may have no
254
# parents but the branch may do.
255
first_tree_parent = None
256
master_last = self.master_branch.last_revision()
257
if (master_last is not None and
258
master_last != first_tree_parent):
272
259
raise errors.OutOfDateTree(self.work_tree)
275
262
# raise an exception as soon as we find a single unknown.
276
263
for unknown in self.work_tree.unknowns():
277
264
raise StrictCommitFailed()
279
if timestamp is None:
280
self.timestamp = time.time()
282
self.timestamp = long(timestamp)
284
266
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)
267
self.config = self.branch.get_config()
303
269
if isinstance(message, str):
304
270
message = message.decode(bzrlib.user_encoding)
305
271
assert isinstance(message, unicode), type(message)
314
280
# note that this estimate is too long when we do a partial tree
315
281
# commit which excludes some new files from being considered.
316
282
# The estimate is corrected when we populate the new inv.
317
self.pb_total = len(self.basis_inv) + len(self.work_inv) + 3 - 1
283
self.pb_total = len(self.work_inv) + 5
318
284
self.pb_count = 0
320
286
self._gather_parents()
321
287
if len(self.parents) > 1 and self.specific_files:
322
288
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
323
289
self.specific_files)
324
self._check_parents_present()
291
self.builder = self.branch.get_commit_builder(self.parents,
292
self.config, timestamp, timezone, committer, revprops, rev_id)
326
294
self._remove_deleted()
327
295
self._populate_new_inv()
328
self._store_snapshot()
329
296
self._report_deletes()
331
298
if not (self.allow_pointless
332
299
or len(self.parents) > 1
333
or self.new_inv != self.basis_inv):
300
or self.builder.new_inventory != self.basis_inv):
334
301
raise PointlessCommit()
336
303
self._emit_progress_update()
337
self.inv_sha1 = self.branch.repository.add_inventory(
342
self._emit_progress_update()
343
self._make_revision()
304
# TODO: Now the new inventory is known, check for conflicts and
305
# prompt the user for a commit message.
306
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
307
# weave lines, because nothing should be recorded until it is known
308
# that commit will succeed.
309
self.builder.finish_inventory()
310
self._emit_progress_update()
311
self.rev_id = self.builder.commit(self.message)
312
self._emit_progress_update()
344
313
# revision data is in the local branch now.
346
315
# upload revision data to the master.
347
# this will propogate merged revisions too if needed.
316
# this will propagate merged revisions too if needed.
348
317
if self.bound_branch:
349
318
self.master_branch.repository.fetch(self.branch.repository,
350
319
revision_id=self.rev_id)
479
444
def _gather_parents(self):
480
445
"""Record the parents of a merge for merge detection."""
481
pending_merges = self.work_tree.pending_merges()
446
# TODO: Make sure that this list doesn't contain duplicate
447
# entries and the order is preserved when doing this.
448
self.parents = self.work_tree.get_parent_ids()
483
449
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
450
for revision in self.parents:
490
451
if self.branch.repository.has_revision(revision):
452
mutter('commit parent revision {%s}', revision)
491
453
inventory = self.branch.repository.get_inventory(revision)
492
454
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)
456
mutter('commit parent ghost revision {%s}', revision)
517
458
def _remove_deleted(self):
518
459
"""Remove deleted files from the working inventories.
528
469
specific = self.specific_files
471
deleted_paths = set()
530
472
for path, ie in self.work_inv.iter_entries():
473
if is_inside_any(deleted_paths, path):
474
# The tree will delete the required ids recursively.
531
476
if specific and not is_inside_any(specific, path):
533
478
if not self.work_tree.has_filename(path):
479
deleted_paths.add(path)
534
480
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)
481
deleted_ids.append(ie.file_id)
482
self.work_tree.unversion(deleted_ids)
582
484
def _populate_new_inv(self):
583
485
"""Build revision inventory.
589
491
None; inventory entries that are carried over untouched have their
590
492
revision set to their prior value.
494
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
495
# results to create a new inventory at the same time, which results
496
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
592
498
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():
499
entries = self.work_inv.iter_entries()
500
if not self.builder.record_root_entry:
501
symbol_versioning.warn('CommitBuilders should support recording'
502
' the root entry as of bzr 0.10.', DeprecationWarning,
504
self.builder.new_inventory.add(self.basis_inv.root.copy())
506
self._emit_progress_update()
507
for path, new_ie in entries:
598
508
self._emit_progress_update()
599
509
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)
510
# mutter('check %s {%s}', path, file_id)
511
if (not self.specific_files or
512
is_inside_or_parent_of_any(self.specific_files, path)):
513
# mutter('%s selected for commit', path)
517
# mutter('%s not selected for commit', path)
518
if self.basis_inv.has_id(file_id):
519
ie = self.basis_inv[file_id].copy()
521
# 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)
524
self.builder.record_entry_contents(ie, self.parent_invs,
525
path, self.work_tree)
526
# describe the nature of the change that has occurred relative to
527
# the basis inventory.
528
if (self.basis_inv.has_id(ie.file_id)):
529
basis_ie = self.basis_inv[ie.file_id]
532
change = ie.describe_change(basis_ie, ie)
533
if change in (InventoryEntry.RENAMED,
534
InventoryEntry.MODIFIED_AND_RENAMED):
535
old_path = self.basis_inv.id2path(ie.file_id)
536
self.reporter.renamed(change, old_path, path)
538
self.reporter.snapshot_change(change, path)
540
if not self.specific_files:
543
# ignore removals that don't match filespec
544
for path, new_ie in self.basis_inv.iter_entries():
545
if new_ie.file_id in self.work_inv:
547
if is_inside_any(self.specific_files, path):
551
self.builder.record_entry_contents(ie, self.parent_invs, path,
624
554
def _emit_progress_update(self):
625
555
"""Emit an update to the progress bar."""
626
556
self.pb.update("Committing", self.pb_count, self.pb_total)
627
557
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
559
def _report_deletes(self):
645
560
for path, ie in self.basis_inv.iter_entries():
646
if ie.file_id not in self.new_inv:
561
if ie.file_id not in self.builder.new_inventory:
647
562
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))