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?)
18
28
# The newly committed revision is going to have a shape corresponding
19
29
# to that of the working inventory. Files that are not in the
20
30
# working tree and that were in the predecessor are reported as
46
56
# merges from, then it should still be reported as newly added
47
57
# relative to the basis revision.
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.
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.
72
from binascii import hexlify
57
73
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,
63
81
import bzrlib.config
82
import bzrlib.errors as errors
64
83
from bzrlib.errors import (BzrError, PointlessCommit,
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)
88
from bzrlib.revision import Revision
71
89
from bzrlib.testament import Testament
72
90
from bzrlib.trace import mutter, note, warning
73
91
from bzrlib.xml5 import serializer_v5
74
from bzrlib.inventory import Inventory, InventoryEntry
75
from bzrlib import symbol_versioning
76
from bzrlib.symbol_versioning import (deprecated_passed,
92
from bzrlib.inventory import Inventory, ROOT_ID
93
from bzrlib.symbol_versioning import *
79
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)
83
109
class NullCommitReporter(object):
98
124
def missing(self, path):
101
def renamed(self, change, old_path, new_path):
105
128
class ReportCommitToLog(NullCommitReporter):
107
# this may be more useful if 'note' was replaced by an overridable
108
# method on self, which would allow more trivial subclassing.
109
# alternative, a callable could be passed in, allowing really trivial
110
# reuse for some uis. RBC 20060511
112
130
def snapshot_change(self, change, path):
113
131
if change == 'unchanged':
115
if change == 'added' and path == '':
117
133
note("%s %s", change, path)
119
135
def completed(self, revno, rev_id):
213
225
self.work_tree = working_tree
214
226
self.branch = self.work_tree.branch
215
if message_callback is None:
216
if message is not None:
217
if isinstance(message, str):
218
message = message.decode(bzrlib.user_encoding)
219
message_callback = lambda x: message
221
raise BzrError("The message or message_callback keyword"
222
" parameter is required for commit().")
228
raise BzrError("The message keyword parameter is required for commit().")
230
self.weave_store = self.branch.repository.weave_store
224
231
self.bound_branch = None
225
232
self.local = local
226
233
self.master_branch = None
227
234
self.master_locked = False
229
236
self.specific_files = specific_files
230
237
self.allow_pointless = allow_pointless
239
if revprops is not None:
240
self.revprops.update(revprops)
232
242
if reporter is None and self.reporter is None:
233
243
self.reporter = NullCommitReporter()
235
245
self.reporter = reporter
237
247
self.work_tree.lock_write()
238
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
240
# Cannot commit with conflicts present.
241
if len(self.work_tree.conflicts())>0:
242
raise ConflictsInTree
244
249
# setup the bound branch variables as needed.
245
250
self._check_bound_branch()
247
252
# check for out of date working trees
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):
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():
259
256
raise errors.OutOfDateTree(self.work_tree)
262
259
# raise an exception as soon as we find a single unknown.
263
260
for unknown in self.work_tree.unknowns():
264
261
raise StrictCommitFailed()
263
if timestamp is None:
264
self.timestamp = time.time()
266
self.timestamp = long(timestamp)
266
268
if self.config is None:
267
self.config = self.branch.get_config()
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()
269
293
self.work_inv = self.work_tree.inventory
270
294
self.basis_tree = self.work_tree.basis_tree()
271
295
self.basis_inv = self.basis_tree.inventory
272
if specific_files is not None:
273
# Ensure specified files are versioned
274
# (We don't actually need the ids here)
275
tree.find_ids_across_trees(specific_files,
276
[self.basis_tree, self.work_tree])
277
# one to finish, one for rev and inventory, and one for each
278
# inventory entry, and the same for the new inventory.
279
# note that this estimate is too long when we do a partial tree
280
# commit which excludes some new files from being considered.
281
# The estimate is corrected when we populate the new inv.
282
self.pb_total = len(self.work_inv) + 5
285
297
self._gather_parents()
286
298
if len(self.parents) > 1 and self.specific_files:
287
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
290
self.builder = self.branch.get_commit_builder(self.parents,
291
self.config, timestamp, timezone, committer, revprops, rev_id)
299
raise NotImplementedError('selected-file commit of merges is not supported yet')
300
self._check_parents_present()
293
302
self._remove_deleted()
294
303
self._populate_new_inv()
304
self._store_snapshot()
295
305
self._report_deletes()
297
self._check_pointless()
299
self._emit_progress_update()
300
# TODO: Now the new inventory is known, check for conflicts and
301
# prompt the user for a commit message.
302
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
303
# weave lines, because nothing should be recorded until it is known
304
# that commit will succeed.
305
self.builder.finish_inventory()
306
self._emit_progress_update()
307
message = message_callback(self)
308
assert isinstance(message, unicode), type(message)
309
self.message = message
310
self._escape_commit_message()
312
self.rev_id = self.builder.commit(self.message)
313
self._emit_progress_update()
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(list(self.work_tree.iter_conflicts()))>0:
313
raise ConflictsInTree
315
self.inv_sha1 = self.branch.repository.add_inventory(
320
self._make_revision()
314
321
# revision data is in the local branch now.
316
323
# upload revision data to the master.
317
# this will propagate merged revisions too if needed.
324
# this will propogate merged revisions too if needed.
318
325
if self.bound_branch:
319
326
self.master_branch.repository.fetch(self.branch.repository,
320
327
revision_id=self.rev_id)
339
350
{'branch':self.branch,
341
352
'rev_id':self.rev_id})
342
self._emit_progress_update()
347
def _any_real_changes(self):
348
"""Are there real changes between new_inventory and basis?
350
For trees without rich roots, inv.root.revision changes every commit.
351
But if that is the only change, we want to treat it as though there
354
new_entries = self.builder.new_inventory.iter_entries()
355
basis_entries = self.basis_inv.iter_entries()
356
new_path, new_root_ie = new_entries.next()
357
basis_path, basis_root_ie = basis_entries.next()
359
# This is a copy of InventoryEntry.__eq__ only leaving out .revision
360
def ie_equal_no_revision(this, other):
361
return ((this.file_id == other.file_id)
362
and (this.name == other.name)
363
and (this.symlink_target == other.symlink_target)
364
and (this.text_sha1 == other.text_sha1)
365
and (this.text_size == other.text_size)
366
and (this.text_id == other.text_id)
367
and (this.parent_id == other.parent_id)
368
and (this.kind == other.kind)
369
and (this.executable == other.executable)
371
if not ie_equal_no_revision(new_root_ie, basis_root_ie):
374
for new_ie, basis_ie in zip(new_entries, basis_entries):
375
if new_ie != basis_ie:
378
# No actual changes present
381
def _check_pointless(self):
382
if self.allow_pointless:
384
# A merge with no effect on files
385
if len(self.parents) > 1:
387
# work around the fact that a newly-initted tree does differ from its
389
if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
390
raise PointlessCommit()
391
# Shortcut, if the number of entries changes, then we obviously have
393
if len(self.builder.new_inventory) != len(self.basis_inv):
395
# If length == 1, then we only have the root entry. Which means
396
# that there is no real difference (only the root could be different)
397
if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
399
raise PointlessCommit()
354
self._cleanup_bound_branch()
355
self.work_tree.unlock()
401
357
def _check_bound_branch(self):
402
358
"""Check to see if the local branch is bound.
439
395
self.bound_branch = self.branch
440
396
self.master_branch.lock_write()
441
397
self.master_locked = True
444
"""Cleanup any open locks, progress bars etc."""
445
cleanups = [self._cleanup_bound_branch,
446
self.work_tree.unlock,
448
found_exception = None
449
for cleanup in cleanups:
452
# we want every cleanup to run no matter what.
453
# so we have a catchall here, but we will raise the
454
# last encountered exception up the stack: and
455
# typically this will be useful enough.
458
if found_exception is not None:
459
# don't do a plan raise, because the last exception may have been
460
# trashed, e is our sure-to-work exception even though it loses the
461
# full traceback. XXX: RBC 20060421 perhaps we could check the
462
# exc_info and if its the same one do a plain raise otherwise
463
# 'raise e' as we do now.
399
#### # Check to see if we have any pending merges. If we do
400
#### # those need to be pushed into the master branch
401
#### pending_merges = self.work_tree.pending_merges()
402
#### if pending_merges:
403
#### for revision_id in pending_merges:
404
#### self.master_branch.repository.fetch(self.bound_branch.repository,
405
#### revision_id=revision_id)
466
407
def _cleanup_bound_branch(self):
467
408
"""Executed at the end of a try/finally to cleanup a bound branch.
496
432
def _gather_parents(self):
497
433
"""Record the parents of a merge for merge detection."""
498
# TODO: Make sure that this list doesn't contain duplicate
499
# entries and the order is preserved when doing this.
500
self.parents = self.work_tree.get_parent_ids()
434
pending_merges = self.work_tree.pending_merges()
501
436
self.parent_invs = []
437
self.present_parents = []
438
precursor_id = self.branch.last_revision()
440
self.parents.append(precursor_id)
441
self.parents += pending_merges
502
442
for revision in self.parents:
503
443
if self.branch.repository.has_revision(revision):
504
mutter('commit parent revision {%s}', revision)
505
444
inventory = self.branch.repository.get_inventory(revision)
506
445
self.parent_invs.append(inventory)
508
mutter('commit parent ghost revision {%s}', revision)
446
self.present_parents.append(revision)
448
def _check_parents_present(self):
449
for parent_id in self.parents:
450
mutter('commit parent revision {%s}', parent_id)
451
if not self.branch.repository.has_revision(parent_id):
452
if parent_id == self.branch.last_revision():
453
warning("parent is missing %r", parent_id)
454
raise HistoryMissing(self.branch, 'revision', parent_id)
456
mutter("commit will ghost revision %r", parent_id)
458
def _make_revision(self):
459
"""Record a new revision object for this commit."""
460
rev = Revision(timestamp=self.timestamp,
461
timezone=self.timezone,
462
committer=self.committer,
463
message=self.message,
464
inventory_sha1=self.inv_sha1,
465
revision_id=self.rev_id,
466
properties=self.revprops)
467
rev.parent_ids = self.parents
468
self.branch.repository.add_revision(self.rev_id, rev, self.new_inv, self.config)
510
470
def _remove_deleted(self):
511
471
"""Remove deleted files from the working inventories.
521
481
specific = self.specific_files
523
deleted_paths = set()
524
483
for path, ie in self.work_inv.iter_entries():
525
if is_inside_any(deleted_paths, path):
526
# The tree will delete the required ids recursively.
528
484
if specific and not is_inside_any(specific, path):
530
486
if not self.work_tree.has_filename(path):
531
deleted_paths.add(path)
532
487
self.reporter.missing(path)
533
deleted_ids.append(ie.file_id)
534
self.work_tree.unversion(deleted_ids)
488
deleted_ids.append((path, ie.file_id))
490
deleted_ids.sort(reverse=True)
491
for path, file_id in deleted_ids:
492
del self.work_inv[file_id]
493
self.work_tree._write_inventory(self.work_inv)
495
def _store_snapshot(self):
496
"""Pass over inventory and record a snapshot.
498
Entries get a new revision when they are modified in
499
any way, which includes a merge with a new set of
500
parents that have the same entry.
502
# XXX: Need to think more here about when the user has
503
# made a specific decision on a particular value -- c.f.
505
for path, ie in self.new_inv.iter_entries():
506
previous_entries = ie.find_previous_heads(
509
self.branch.repository.get_transaction())
510
if ie.revision is None:
511
change = ie.snapshot(self.rev_id, path, previous_entries,
512
self.work_tree, self.weave_store,
513
self.branch.get_transaction())
516
self.reporter.snapshot_change(change, path)
536
518
def _populate_new_inv(self):
537
519
"""Build revision inventory.
543
525
None; inventory entries that are carried over untouched have their
544
526
revision set to their prior value.
546
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
547
# results to create a new inventory at the same time, which results
548
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
550
528
mutter("Selecting files for commit with filter %s", self.specific_files)
551
assert self.work_inv.root is not None
552
entries = self.work_inv.iter_entries()
553
if not self.builder.record_root_entry:
554
symbol_versioning.warn('CommitBuilders should support recording'
555
' the root entry as of bzr 0.10.', DeprecationWarning,
557
self.builder.new_inventory.add(self.basis_inv.root.copy())
559
self._emit_progress_update()
560
for path, new_ie in entries:
561
self._emit_progress_update()
529
self.new_inv = Inventory()
530
for path, new_ie in self.work_inv.iter_entries():
562
531
file_id = new_ie.file_id
563
# mutter('check %s {%s}', path, file_id)
564
if (not self.specific_files or
565
is_inside_or_parent_of_any(self.specific_files, path)):
566
# mutter('%s selected for commit', path)
570
# mutter('%s not selected for commit', path)
571
if self.basis_inv.has_id(file_id):
572
ie = self.basis_inv[file_id].copy()
574
# this entry is new and not being committed
532
mutter('check %s {%s}', path, new_ie.file_id)
533
if self.specific_files:
534
if not is_inside_any(self.specific_files, path):
535
mutter('%s not selected for commit', path)
536
self._carry_entry(file_id)
576
self.builder.record_entry_contents(ie, self.parent_invs,
577
path, self.work_tree)
578
# describe the nature of the change that has occurred relative to
579
# the basis inventory.
580
if (self.basis_inv.has_id(ie.file_id)):
581
basis_ie = self.basis_inv[ie.file_id]
584
change = ie.describe_change(basis_ie, ie)
585
if change in (InventoryEntry.RENAMED,
586
InventoryEntry.MODIFIED_AND_RENAMED):
587
old_path = self.basis_inv.id2path(ie.file_id)
588
self.reporter.renamed(change, old_path, path)
590
self.reporter.snapshot_change(change, path)
592
if not self.specific_files:
595
# ignore removals that don't match filespec
596
for path, new_ie in self.basis_inv.iter_entries():
597
if new_ie.file_id in self.work_inv:
599
if is_inside_any(self.specific_files, path):
603
self.builder.record_entry_contents(ie, self.parent_invs, path,
606
def _emit_progress_update(self):
607
"""Emit an update to the progress bar."""
608
self.pb.update("Committing", self.pb_count, self.pb_total)
539
# this is selected, ensure its parents are too.
540
parent_id = new_ie.parent_id
541
while parent_id != ROOT_ID:
542
if not self.new_inv.has_id(parent_id):
543
ie = self._select_entry(self.work_inv[parent_id])
544
mutter('%s selected for commit because of %s',
545
self.new_inv.id2path(parent_id), path)
547
ie = self.new_inv[parent_id]
548
if ie.revision is not None:
550
mutter('%s selected for commit because of %s',
551
self.new_inv.id2path(parent_id), path)
552
parent_id = ie.parent_id
553
mutter('%s selected for commit', path)
554
self._select_entry(new_ie)
556
def _select_entry(self, new_ie):
557
"""Make new_ie be considered for committing."""
563
def _carry_entry(self, file_id):
564
"""Carry the file unchanged from the basis revision."""
565
if self.basis_inv.has_id(file_id):
566
self.new_inv.add(self.basis_inv[file_id].copy())
611
568
def _report_deletes(self):
612
for path, ie in self.basis_inv.iter_entries():
613
if ie.file_id not in self.builder.new_inventory:
614
self.reporter.deleted(path)
569
for file_id in self.basis_inv:
570
if file_id not in self.new_inv:
571
self.reporter.deleted(self.basis_inv.id2path(file_id))
573
def _gen_revision_id(config, when):
574
"""Return new revision-id."""
575
s = '%s-%s-' % (config.user_email(), compact_date(when))
576
s += hexlify(rand_bytes(8))