1
1
# Copyright (C) 2005, 2006 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
18
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.
21
22
# TODO: Separate 'prepare' phase where we find a list of potentially
22
23
# committed files. We then can then pause the commit to prompt for a
62
63
# 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
72
from cStringIO import StringIO
74
from bzrlib.atomicfile import AtomicFile
78
75
import bzrlib.config
76
import bzrlib.errors as errors
79
77
from bzrlib.errors import (BzrError, PointlessCommit,
86
84
from bzrlib.testament import Testament
87
85
from bzrlib.trace import mutter, note, warning
88
86
from bzrlib.xml5 import serializer_v5
89
from bzrlib.inventory import Inventory, InventoryEntry
90
from bzrlib import symbol_versioning
87
from bzrlib.inventory import Inventory, ROOT_ID, InventoryEntry
91
88
from bzrlib.symbol_versioning import (deprecated_passed,
92
89
deprecated_function,
93
91
DEPRECATED_PARAMETER)
94
92
from bzrlib.workingtree import WorkingTree
95
@deprecated_function(zero_seven)
96
def commit(*args, **kwargs):
97
"""Commit a new revision to a branch.
99
Function-style interface for convenience of old callers.
101
New code should use the Commit class instead.
103
## XXX: Remove this in favor of WorkingTree.commit?
104
Commit().commit(*args, **kwargs)
97
107
class NullCommitReporter(object):
98
108
"""I report on progress of a commit."""
126
136
def snapshot_change(self, change, path):
127
137
if change == 'unchanged':
129
if change == 'added' and path == '':
131
139
note("%s %s", change, path)
133
141
def completed(self, revno, rev_id):
215
223
mutter('preparing to commit')
217
225
if deprecated_passed(branch):
218
symbol_versioning.warn("Commit.commit (branch, ...): The branch parameter is "
226
warnings.warn("Commit.commit (branch, ...): The branch parameter is "
219
227
"deprecated as of bzr 0.8. Please use working_tree= instead.",
220
228
DeprecationWarning, stacklevel=2)
221
229
self.branch = branch
252
260
self._check_bound_branch()
254
262
# check for out of date working trees
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):
263
# if we are bound, then self.branch is the master branch and this
264
# test is thus all we need.
265
if self.work_tree.last_revision() != self.master_branch.last_revision():
266
266
raise errors.OutOfDateTree(self.work_tree)
282
282
self.work_inv = self.work_tree.inventory
283
283
self.basis_tree = self.work_tree.basis_tree()
284
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])
290
285
# one to finish, one for rev and inventory, and one for each
291
286
# inventory entry, and the same for the new inventory.
292
287
# note that this estimate is too long when we do a partial tree
299
294
if len(self.parents) > 1 and self.specific_files:
300
295
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
301
296
self.specific_files)
297
self._check_parents_present()
303
298
self.builder = self.branch.get_commit_builder(self.parents,
304
299
self.config, timestamp, timezone, committer, revprops, rev_id)
307
302
self._populate_new_inv()
308
303
self._report_deletes()
310
self._check_pointless()
305
if not (self.allow_pointless
306
or len(self.parents) > 1
307
or self.builder.new_inventory != self.basis_inv):
308
raise PointlessCommit()
312
310
self._emit_progress_update()
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.
311
# TODO: Now the new inventory is known, check for conflicts and prompt the
312
# user for a commit message.
318
313
self.builder.finish_inventory()
319
314
self._emit_progress_update()
320
315
self.rev_id = self.builder.commit(self.message)
334
329
# and now do the commit locally.
335
330
self.branch.append_revision(self.rev_id)
337
rev_tree = self.builder.revision_tree()
338
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
332
self.work_tree.set_pending_merges([])
333
self.work_tree.set_last_revision(self.rev_id)
339
334
# now the work tree is up to date with the branch
341
336
self.reporter.completed(self.branch.revno(), self.rev_id)
353
348
return self.rev_id
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()
409
350
def _check_bound_branch(self):
410
351
"""Check to see if the local branch is bound.
509
450
self.parent_invs = []
510
451
for revision in self.parents:
511
452
if self.branch.repository.has_revision(revision):
512
mutter('commit parent revision {%s}', revision)
513
453
inventory = self.branch.repository.get_inventory(revision)
514
454
self.parent_invs.append(inventory)
516
mutter('commit parent ghost revision {%s}', revision)
456
def _check_parents_present(self):
457
for parent_id in self.parents:
458
mutter('commit parent revision {%s}', parent_id)
459
if not self.branch.repository.has_revision(parent_id):
460
if parent_id == self.branch.last_revision():
461
warning("parent is missing %r", parent_id)
462
raise BzrCheckError("branch %s is missing revision {%s}"
463
% (self.branch, parent_id))
518
465
def _remove_deleted(self):
519
466
"""Remove deleted files from the working inventories.
529
476
specific = self.specific_files
531
deleted_paths = set()
532
478
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.
536
479
if specific and not is_inside_any(specific, path):
538
481
if not self.work_tree.has_filename(path):
539
deleted_paths.add(path)
540
482
self.reporter.missing(path)
541
deleted_ids.append(ie.file_id)
542
self.work_tree.unversion(deleted_ids)
483
deleted_ids.append((path, ie.file_id))
485
deleted_ids.sort(reverse=True)
486
for path, file_id in deleted_ids:
487
del self.work_inv[file_id]
488
self.work_tree._write_inventory(self.work_inv)
544
490
def _populate_new_inv(self):
545
491
"""Build revision inventory.
551
497
None; inventory entries that are carried over untouched have their
552
498
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?
558
500
mutter("Selecting files for commit with filter %s", self.specific_files)
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:
501
# iter_entries does not visit the ROOT_ID node so we need to call
502
# self._emit_progress_update once by hand.
503
self._emit_progress_update()
504
for path, new_ie in self.work_inv.iter_entries():
569
505
self._emit_progress_update()
570
506
file_id = new_ie.file_id
571
# mutter('check %s {%s}', path, file_id)
507
mutter('check %s {%s}', path, file_id)
572
508
if (not self.specific_files or
573
509
is_inside_or_parent_of_any(self.specific_files, path)):
574
# mutter('%s selected for commit', path)
510
mutter('%s selected for commit', path)
575
511
ie = new_ie.copy()
576
512
ie.revision = None
578
# mutter('%s not selected for commit', path)
514
mutter('%s not selected for commit', path)
579
515
if self.basis_inv.has_id(file_id):
580
516
ie = self.basis_inv[file_id].copy()
582
518
# this entry is new and not being committed
584
521
self.builder.record_entry_contents(ie, self.parent_invs,
585
522
path, self.work_tree)
586
523
# describe the nature of the change that has occurred relative to
598
535
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,
614
537
def _emit_progress_update(self):
615
538
"""Emit an update to the progress bar."""
616
539
self.pb.update("Committing", self.pb_count, self.pb_total)