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
93
from bzrlib.symbol_versioning import *
89
from bzrlib.inventory import Inventory, ROOT_ID, 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."""
245
240
self.reporter = reporter
247
242
self.work_tree.lock_write()
243
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
245
# Cannot commit with conflicts present.
246
if len(self.work_tree.conflicts())>0:
247
raise ConflictsInTree
249
249
# setup the bound branch variables as needed.
250
250
self._check_bound_branch()
252
252
# 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():
254
first_tree_parent = self.work_tree.get_parent_ids()[0]
256
# if there are no parents, treat our parent as 'None'
257
# this is so that we still consier the master branch
258
# - in a checkout scenario the tree may have no
259
# parents but the branch may do.
260
first_tree_parent = None
261
master_last = self.master_branch.last_revision()
262
if (master_last is not None and
263
master_last != first_tree_parent):
256
264
raise errors.OutOfDateTree(self.work_tree)
259
267
# raise an exception as soon as we find a single unknown.
260
268
for unknown in self.work_tree.unknowns():
261
269
raise StrictCommitFailed()
263
if timestamp is None:
264
self.timestamp = time.time()
266
self.timestamp = long(timestamp)
268
271
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)
272
self.config = self.branch.get_config()
287
274
if isinstance(message, str):
288
275
message = message.decode(bzrlib.user_encoding)
289
276
assert isinstance(message, unicode), type(message)
293
280
self.work_inv = self.work_tree.inventory
294
281
self.basis_tree = self.work_tree.basis_tree()
295
282
self.basis_inv = self.basis_tree.inventory
283
if specific_files is not None:
284
# Ensure specified files are versioned
285
# (We don't actually need the ids here)
286
tree.find_ids_across_trees(specific_files,
287
[self.basis_tree, self.work_tree])
288
# one to finish, one for rev and inventory, and one for each
289
# inventory entry, and the same for the new inventory.
290
# note that this estimate is too long when we do a partial tree
291
# commit which excludes some new files from being considered.
292
# The estimate is corrected when we populate the new inv.
293
self.pb_total = len(self.work_inv) + 5
297
296
self._gather_parents()
298
297
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()
298
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
301
self.builder = self.branch.get_commit_builder(self.parents,
302
self.config, timestamp, timezone, committer, revprops, rev_id)
302
304
self._remove_deleted()
303
305
self._populate_new_inv()
304
self._store_snapshot()
305
306
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()
308
self._check_pointless()
310
self._emit_progress_update()
311
# TODO: Now the new inventory is known, check for conflicts and
312
# prompt the user for a commit message.
313
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
314
# weave lines, because nothing should be recorded until it is known
315
# that commit will succeed.
316
self.builder.finish_inventory()
317
self._emit_progress_update()
318
self.rev_id = self.builder.commit(self.message)
319
self._emit_progress_update()
321
320
# revision data is in the local branch now.
323
322
# upload revision data to the master.
324
# this will propogate merged revisions too if needed.
323
# this will propagate merged revisions too if needed.
325
324
if self.bound_branch:
326
325
self.master_branch.repository.fetch(self.branch.repository,
327
326
revision_id=self.rev_id)
391
409
self.bound_branch = self.branch
392
410
self.master_branch.lock_write()
393
411
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)
414
"""Cleanup any open locks, progress bars etc."""
415
cleanups = [self._cleanup_bound_branch,
416
self.work_tree.unlock,
418
found_exception = None
419
for cleanup in cleanups:
422
# we want every cleanup to run no matter what.
423
# so we have a catchall here, but we will raise the
424
# last encountered exception up the stack: and
425
# typically this will be useful enough.
428
if found_exception is not None:
429
# don't do a plan raise, because the last exception may have been
430
# trashed, e is our sure-to-work exception even though it loses the
431
# full traceback. XXX: RBC 20060421 perhaps we could check the
432
# exc_info and if its the same one do a plain raise otherwise
433
# 'raise e' as we do now.
403
436
def _cleanup_bound_branch(self):
404
437
"""Executed at the end of a try/finally to cleanup a bound branch.
428
466
def _gather_parents(self):
429
467
"""Record the parents of a merge for merge detection."""
430
pending_merges = self.work_tree.pending_merges()
468
# TODO: Make sure that this list doesn't contain duplicate
469
# entries and the order is preserved when doing this.
470
self.parents = self.work_tree.get_parent_ids()
432
471
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
472
for revision in self.parents:
439
473
if self.branch.repository.has_revision(revision):
474
mutter('commit parent revision {%s}', revision)
440
475
inventory = self.branch.repository.get_inventory(revision)
441
476
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)
478
mutter('commit parent ghost revision {%s}', revision)
466
480
def _remove_deleted(self):
467
481
"""Remove deleted files from the working inventories.
477
491
specific = self.specific_files
493
deleted_paths = set()
479
494
for path, ie in self.work_inv.iter_entries():
495
if is_inside_any(deleted_paths, path):
496
# The tree will delete the required ids recursively.
480
498
if specific and not is_inside_any(specific, path):
482
500
if not self.work_tree.has_filename(path):
501
deleted_paths.add(path)
483
502
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)
503
deleted_ids.append(ie.file_id)
504
self.work_tree.unversion(deleted_ids)
514
506
def _populate_new_inv(self):
515
507
"""Build revision inventory.
521
513
None; inventory entries that are carried over untouched have their
522
514
revision set to their prior value.
516
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
517
# results to create a new inventory at the same time, which results
518
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
524
520
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():
521
entries = self.work_inv.iter_entries()
522
if not self.builder.record_root_entry:
523
symbol_versioning.warn('CommitBuilders should support recording'
524
' the root entry as of bzr 0.10.', DeprecationWarning,
526
self.builder.new_inventory.add(self.basis_inv.root.copy())
528
self._emit_progress_update()
529
for path, new_ie in entries:
530
self._emit_progress_update()
527
531
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)
532
# mutter('check %s {%s}', path, file_id)
533
if (not self.specific_files or
534
is_inside_or_parent_of_any(self.specific_files, path)):
535
# mutter('%s selected for commit', path)
539
# mutter('%s not selected for commit', path)
540
if self.basis_inv.has_id(file_id):
541
ie = self.basis_inv[file_id].copy()
543
# 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())
546
self.builder.record_entry_contents(ie, self.parent_invs,
547
path, self.work_tree)
548
# describe the nature of the change that has occurred relative to
549
# the basis inventory.
550
if (self.basis_inv.has_id(ie.file_id)):
551
basis_ie = self.basis_inv[ie.file_id]
554
change = ie.describe_change(basis_ie, ie)
555
if change in (InventoryEntry.RENAMED,
556
InventoryEntry.MODIFIED_AND_RENAMED):
557
old_path = self.basis_inv.id2path(ie.file_id)
558
self.reporter.renamed(change, old_path, path)
560
self.reporter.snapshot_change(change, path)
562
if not self.specific_files:
565
# ignore removals that don't match filespec
566
for path, new_ie in self.basis_inv.iter_entries():
567
if new_ie.file_id in self.work_inv:
569
if is_inside_any(self.specific_files, path):
573
self.builder.record_entry_contents(ie, self.parent_invs, path,
576
def _emit_progress_update(self):
577
"""Emit an update to the progress bar."""
578
self.pb.update("Committing", self.pb_count, self.pb_total)
564
581
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))
582
for path, ie in self.basis_inv.iter_entries():
583
if ie.file_id not in self.builder.new_inventory:
584
self.reporter.deleted(path)