~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Martin Pool
  • Date: 2005-05-16 02:19:13 UTC
  • Revision ID: mbp@sourcefrog.net-20050516021913-3a933f871079e3fe
- patch from ddaa to create api/ directory 
  before building API docs

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
 
#
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
 
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
6
6
# (at your option) any later version.
7
 
#
 
7
 
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
#
 
12
 
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
# The newly committed revision is going to have a shape corresponding
19
 
# to that of the working inventory.  Files that are not in the
20
 
# working tree and that were in the predecessor are reported as
21
 
# removed --- this can include files that were either removed from the
22
 
# inventory or deleted in the working tree.  If they were only
23
 
# deleted from disk, they are removed from the working inventory.
24
 
 
25
 
# We then consider the remaining entries, which will be in the new
26
 
# version.  Directory entries are simply copied across.  File entries
27
 
# must be checked to see if a new version of the file should be
28
 
# recorded.  For each parent revision inventory, we check to see what
29
 
# version of the file was present.  If the file was present in at
30
 
# least one tree, and if it was the same version in all the trees,
31
 
# then we can just refer to that version.  Otherwise, a new version
32
 
# representing the merger of the file versions must be added.
33
 
 
34
 
# TODO: Update hashcache before and after - or does the WorkingTree
35
 
# look after that?
36
 
 
37
 
# TODO: Rather than mashing together the ancestry and storing it back,
38
 
# perhaps the weave should have single method which does it all in one
39
 
# go, avoiding a lot of redundant work.
40
 
 
41
 
# TODO: Perhaps give a warning if one of the revisions marked as
42
 
# merged is already in the ancestry, and then don't record it as a
43
 
# distinct parent.
44
 
 
45
 
# TODO: If the file is newly merged but unchanged from the version it
46
 
# merges from, then it should still be reported as newly added
47
 
# relative to the basis revision.
48
 
 
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.
51
 
 
52
 
import os
53
 
import re
54
 
import sys
55
 
import time
56
 
 
57
 
from cStringIO import StringIO
58
 
 
59
 
from bzrlib import (
60
 
    errors,
61
 
    inventory,
62
 
    tree,
63
 
    )
64
 
from bzrlib.branch import Branch
65
 
import bzrlib.config
66
 
from bzrlib.errors import (BzrError, PointlessCommit,
67
 
                           ConflictsInTree,
68
 
                           StrictCommitFailed
69
 
                           )
70
 
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any, 
71
 
                            is_inside_or_parent_of_any,
72
 
                            quotefn, sha_file, split_lines)
73
 
from bzrlib.testament import Testament
74
 
from bzrlib.trace import mutter, note, warning
75
 
from bzrlib.xml5 import serializer_v5
76
 
from bzrlib.inventory import Inventory, InventoryEntry
77
 
from bzrlib import symbol_versioning
78
 
from bzrlib.symbol_versioning import (deprecated_passed,
79
 
        deprecated_function,
80
 
        DEPRECATED_PARAMETER)
81
 
from bzrlib.workingtree import WorkingTree
82
 
import bzrlib.ui
83
 
 
84
 
 
85
 
class NullCommitReporter(object):
86
 
    """I report on progress of a commit."""
87
 
 
88
 
    def snapshot_change(self, change, path):
89
 
        pass
90
 
 
91
 
    def completed(self, revno, rev_id):
92
 
        pass
93
 
 
94
 
    def deleted(self, file_id):
95
 
        pass
96
 
 
97
 
    def escaped(self, escape_count, message):
98
 
        pass
99
 
 
100
 
    def missing(self, path):
101
 
        pass
102
 
 
103
 
    def renamed(self, change, old_path, new_path):
104
 
        pass
105
 
 
106
 
 
107
 
class ReportCommitToLog(NullCommitReporter):
108
 
 
109
 
    def _note(self, format, *args):
110
 
        """Output a message.
111
 
 
112
 
        Subclasses may choose to override this method.
113
 
        """
114
 
        note(format, *args)
115
 
 
116
 
    def snapshot_change(self, change, path):
117
 
        if change == 'unchanged':
118
 
            return
119
 
        if change == 'added' and path == '':
120
 
            return
121
 
        self._note("%s %s", change, path)
122
 
 
123
 
    def completed(self, revno, rev_id):
124
 
        self._note('Committed revision %d.', revno)
 
18
 
 
19
def commit(branch, message, timestamp=None, timezone=None,
 
20
           committer=None,
 
21
           verbose=True,
 
22
           specific_files=None,
 
23
           rev_id=None):
 
24
    """Commit working copy as a new revision.
 
25
 
 
26
    The basic approach is to add all the file texts into the
 
27
    store, then the inventory, then make a new revision pointing
 
28
    to that inventory and store that.
 
29
 
 
30
    This is not quite safe if the working copy changes during the
 
31
    commit; for the moment that is simply not allowed.  A better
 
32
    approach is to make a temporary copy of the files before
 
33
    computing their hashes, and then add those hashes in turn to
 
34
    the inventory.  This should mean at least that there are no
 
35
    broken hash pointers.  There is no way we can get a snapshot
 
36
    of the whole directory at an instant.  This would also have to
 
37
    be robust against files disappearing, moving, etc.  So the
 
38
    whole thing is a bit hard.
 
39
 
 
40
    timestamp -- if not None, seconds-since-epoch for a
 
41
         postdated/predated commit.
 
42
 
 
43
    specific_files
 
44
        If true, commit only those files.
 
45
    """
 
46
 
 
47
    import os, time, tempfile
 
48
 
 
49
    from inventory import Inventory
 
50
    from osutils import isdir, isfile, sha_string, quotefn, \
 
51
         local_time_offset, username, kind_marker, is_inside_any
125
52
    
126
 
    def deleted(self, file_id):
127
 
        self._note('deleted %s', file_id)
128
 
 
129
 
    def escaped(self, escape_count, message):
130
 
        self._note("replaced %d control characters in message", escape_count)
131
 
 
132
 
    def missing(self, path):
133
 
        self._note('missing %s', path)
134
 
 
135
 
    def renamed(self, change, old_path, new_path):
136
 
        self._note('%s %s => %s', change, old_path, new_path)
137
 
 
138
 
 
139
 
class Commit(object):
140
 
    """Task of committing a new revision.
141
 
 
142
 
    This is a MethodObject: it accumulates state as the commit is
143
 
    prepared, and then it is discarded.  It doesn't represent
144
 
    historical revisions, just the act of recording a new one.
145
 
 
146
 
            missing_ids
147
 
            Modified to hold a list of files that have been deleted from
148
 
            the working directory; these should be removed from the
149
 
            working inventory.
150
 
    """
151
 
    def __init__(self,
152
 
                 reporter=None,
153
 
                 config=None):
154
 
        if reporter is not None:
155
 
            self.reporter = reporter
156
 
        else:
157
 
            self.reporter = NullCommitReporter()
158
 
        self.config = config
159
 
        
160
 
    def commit(self,
161
 
               message=None,
162
 
               timestamp=None,
163
 
               timezone=None,
164
 
               committer=None,
165
 
               specific_files=None,
166
 
               rev_id=None,
167
 
               allow_pointless=True,
168
 
               strict=False,
169
 
               verbose=False,
170
 
               revprops=None,
171
 
               working_tree=None,
172
 
               local=False,
173
 
               reporter=None,
174
 
               config=None,
175
 
               message_callback=None,
176
 
               recursive='down'):
177
 
        """Commit working copy as a new revision.
178
 
 
179
 
        :param message: the commit message (it or message_callback is required)
180
 
 
181
 
        :param timestamp: if not None, seconds-since-epoch for a
182
 
            postdated/predated commit.
183
 
 
184
 
        :param specific_files: If true, commit only those files.
185
 
 
186
 
        :param rev_id: If set, use this as the new revision id.
187
 
            Useful for test or import commands that need to tightly
188
 
            control what revisions are assigned.  If you duplicate
189
 
            a revision id that exists elsewhere it is your own fault.
190
 
            If null (default), a time/random revision id is generated.
191
 
 
192
 
        :param allow_pointless: If true (default), commit even if nothing
193
 
            has changed and no merges are recorded.
194
 
 
195
 
        :param strict: If true, don't allow a commit if the working tree
196
 
            contains unknown files.
197
 
 
198
 
        :param revprops: Properties for new revision
199
 
        :param local: Perform a local only commit.
200
 
        :param recursive: If set to 'down', commit in any subtrees that have
201
 
            pending changes of any sort during this commit.
202
 
        """
203
 
        mutter('preparing to commit')
204
 
 
205
 
        if working_tree is None:
206
 
            raise BzrError("working_tree must be passed into commit().")
207
 
        else:
208
 
            self.work_tree = working_tree
209
 
            self.branch = self.work_tree.branch
210
 
            if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
211
 
                if not self.branch.repository.supports_rich_root():
212
 
                    raise errors.RootNotRich()
213
 
        if message_callback is None:
214
 
            if message is not None:
215
 
                if isinstance(message, str):
216
 
                    message = message.decode(bzrlib.user_encoding)
217
 
                message_callback = lambda x: message
218
 
            else:
219
 
                raise BzrError("The message or message_callback keyword"
220
 
                               " parameter is required for commit().")
221
 
 
222
 
        self.bound_branch = None
223
 
        self.local = local
224
 
        self.master_branch = None
225
 
        self.master_locked = False
226
 
        self.rev_id = None
227
 
        self.specific_files = specific_files
228
 
        self.allow_pointless = allow_pointless
229
 
        self.recursive = recursive
230
 
        self.revprops = revprops
231
 
        self.message_callback = message_callback
232
 
        self.timestamp = timestamp
233
 
        self.timezone = timezone
234
 
        self.committer = committer
235
 
        self.strict = strict
236
 
        self.verbose = verbose
237
 
 
238
 
        if reporter is None and self.reporter is None:
239
 
            self.reporter = NullCommitReporter()
240
 
        elif reporter is not None:
241
 
            self.reporter = reporter
242
 
 
243
 
        self.work_tree.lock_write()
244
 
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
245
 
        self.basis_tree = self.work_tree.basis_tree()
246
 
        self.basis_tree.lock_read()
247
 
        try:
248
 
            # Cannot commit with conflicts present.
249
 
            if len(self.work_tree.conflicts()) > 0:
250
 
                raise ConflictsInTree
251
 
 
252
 
            # Setup the bound branch variables as needed.
253
 
            self._check_bound_branch()
254
 
 
255
 
            # Check that the working tree is up to date
256
 
            old_revno,new_revno = self._check_out_of_date_tree()
257
 
 
258
 
            if strict:
259
 
                # raise an exception as soon as we find a single unknown.
260
 
                for unknown in self.work_tree.unknowns():
261
 
                    raise StrictCommitFailed()
262
 
                   
263
 
            if self.config is None:
264
 
                self.config = self.branch.get_config()
265
 
 
266
 
            self.work_inv = self.work_tree.inventory
267
 
            self.basis_inv = self.basis_tree.inventory
268
 
            if specific_files is not None:
269
 
                # Ensure specified files are versioned
270
 
                # (We don't actually need the ids here)
271
 
                # XXX: Dont we have filter_unversioned to do this more
272
 
                # cheaply?
273
 
                tree.find_ids_across_trees(specific_files,
274
 
                                           [self.basis_tree, self.work_tree])
275
 
 
276
 
            # Setup the progress bar. As the number of files that need to be
277
 
            # committed in unknown, progress is reported as stages.
278
 
            # We keep track of entries separately though and include that
279
 
            # information in the progress bar during the relevant stages.
280
 
            self.pb_stage_name = ""
281
 
            self.pb_stage_count = 0
282
 
            self.pb_stage_total = 4
283
 
            if self.bound_branch:
284
 
                self.pb_stage_total += 1
285
 
            self.pb.show_pct = False
286
 
            self.pb.show_spinner = False
287
 
            self.pb.show_eta = False
288
 
            self.pb.show_count = True
289
 
            self.pb.show_bar = False
290
 
 
291
 
            self._gather_parents()
292
 
            if len(self.parents) > 1 and self.specific_files:
293
 
                raise errors.CannotCommitSelectedFileMerge(self.specific_files)
294
 
            
295
 
            # Build the new inventory
296
 
            self._emit_progress_set_stage("Collecting changes", show_entries=True)
297
 
            self.builder = self.branch.get_commit_builder(self.parents,
298
 
                self.config, timestamp, timezone, committer, revprops, rev_id)
299
 
            self._remove_deleted()
300
 
            self._populate_new_inv()
301
 
            self._report_deletes()
302
 
            self._check_pointless()
303
 
 
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._emit_progress_set_stage("Saving data locally")
310
 
            self.builder.finish_inventory()
311
 
            message = message_callback(self)
312
 
            assert isinstance(message, unicode), type(message)
313
 
            self.message = message
314
 
            self._escape_commit_message()
315
 
 
316
 
            # Add revision data to the local branch
317
 
            self.rev_id = self.builder.commit(self.message)
318
 
            
319
 
            # Upload revision data to the master.
320
 
            # this will propagate merged revisions too if needed.
321
 
            if self.bound_branch:
322
 
                self._emit_progress_set_stage("Uploading data to master branch")
323
 
                self.master_branch.repository.fetch(self.branch.repository,
324
 
                                                    revision_id=self.rev_id)
325
 
                # now the master has the revision data
326
 
                # 'commit' to the master first so a timeout here causes the
327
 
                # local branch to be out of date
328
 
                self.master_branch.set_last_revision_info(new_revno,
329
 
                                                          self.rev_id)
330
 
 
331
 
            # and now do the commit locally.
332
 
            self.branch.set_last_revision_info(new_revno, self.rev_id)
333
 
 
334
 
            # Make the working tree up to date with the branch
335
 
            self._emit_progress_set_stage("Updating the working tree")
336
 
            rev_tree = self.builder.revision_tree()
337
 
            self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
338
 
            self.reporter.completed(new_revno, self.rev_id)
339
 
 
340
 
            # Process the post commit hooks, if any
341
 
            self._emit_progress_set_stage("Running post commit hooks")
342
 
            self._process_hooks(old_revno, new_revno)
343
 
        finally:
344
 
            self._cleanup()
345
 
        return self.rev_id
346
 
 
347
 
    def _any_real_changes(self):
348
 
        """Are there real changes between new_inventory and basis?
349
 
 
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
352
 
        are *no* changes.
353
 
        """
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()
358
 
 
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)
370
 
                    and (this.reference_revision == other.reference_revision)
371
 
                    )
372
 
        if not ie_equal_no_revision(new_root_ie, basis_root_ie):
373
 
            return True
374
 
 
375
 
        for new_ie, basis_ie in zip(new_entries, basis_entries):
376
 
            if new_ie != basis_ie:
377
 
                return True
378
 
 
379
 
        # No actual changes present
380
 
        return False
381
 
 
382
 
    def _check_pointless(self):
383
 
        if self.allow_pointless:
384
 
            return
385
 
        # A merge with no effect on files
386
 
        if len(self.parents) > 1:
387
 
            return
388
 
        # work around the fact that a newly-initted tree does differ from its
389
 
        # basis
390
 
        if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
391
 
            raise PointlessCommit()
392
 
        # Shortcut, if the number of entries changes, then we obviously have
393
 
        # a change
394
 
        if len(self.builder.new_inventory) != len(self.basis_inv):
395
 
            return
396
 
        # If length == 1, then we only have the root entry. Which means
397
 
        # that there is no real difference (only the root could be different)
398
 
        if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
399
 
            return
400
 
        raise PointlessCommit()
401
 
 
402
 
    def _check_bound_branch(self):
403
 
        """Check to see if the local branch is bound.
404
 
 
405
 
        If it is bound, then most of the commit will actually be
406
 
        done using the remote branch as the target branch.
407
 
        Only at the end will the local branch be updated.
408
 
        """
409
 
        if self.local and not self.branch.get_bound_location():
410
 
            raise errors.LocalRequiresBoundBranch()
411
 
 
412
 
        if not self.local:
413
 
            self.master_branch = self.branch.get_master_branch()
414
 
 
415
 
        if not self.master_branch:
416
 
            # make this branch the reference branch for out of date checks.
417
 
            self.master_branch = self.branch
418
 
            return
419
 
 
420
 
        # If the master branch is bound, we must fail
421
 
        master_bound_location = self.master_branch.get_bound_location()
422
 
        if master_bound_location:
423
 
            raise errors.CommitToDoubleBoundBranch(self.branch,
424
 
                    self.master_branch, master_bound_location)
425
 
 
426
 
        # TODO: jam 20051230 We could automatically push local
427
 
        #       commits to the remote branch if they would fit.
428
 
        #       But for now, just require remote to be identical
429
 
        #       to local.
430
 
        
431
 
        # Make sure the local branch is identical to the master
432
 
        master_info = self.master_branch.last_revision_info()
433
 
        local_info = self.branch.last_revision_info()
434
 
        if local_info != master_info:
435
 
            raise errors.BoundBranchOutOfDate(self.branch,
436
 
                    self.master_branch)
437
 
 
438
 
        # Now things are ready to change the master branch
439
 
        # so grab the lock
440
 
        self.bound_branch = self.branch
441
 
        self.master_branch.lock_write()
442
 
        self.master_locked = True
443
 
 
444
 
    def _check_out_of_date_tree(self):
445
 
        """Check that the working tree is up to date.
446
 
 
447
 
        :return: old_revision_number,new_revision_number tuple
448
 
        """
449
 
        try:
450
 
            first_tree_parent = self.work_tree.get_parent_ids()[0]
451
 
        except IndexError:
452
 
            # if there are no parents, treat our parent as 'None'
453
 
            # this is so that we still consider the master branch
454
 
            # - in a checkout scenario the tree may have no
455
 
            # parents but the branch may do.
456
 
            first_tree_parent = bzrlib.revision.NULL_REVISION
457
 
        old_revno, master_last = self.master_branch.last_revision_info()
458
 
        if master_last != first_tree_parent:
459
 
            if master_last != bzrlib.revision.NULL_REVISION:
460
 
                raise errors.OutOfDateTree(self.work_tree)
461
 
        if self.branch.repository.has_revision(first_tree_parent):
462
 
            new_revno = old_revno + 1
463
 
        else:
464
 
            # ghost parents never appear in revision history.
465
 
            new_revno = 1
466
 
        return old_revno,new_revno
467
 
 
468
 
    def _process_hooks(self, old_revno, new_revno):
469
 
        """Process any registered commit hooks."""
470
 
        # old style commit hooks - should be deprecated ? (obsoleted in
471
 
        # 0.15)
472
 
        if self.config.post_commit() is not None:
473
 
            hooks = self.config.post_commit().split(' ')
474
 
            # this would be nicer with twisted.python.reflect.namedAny
475
 
            for hook in hooks:
476
 
                result = eval(hook + '(branch, rev_id)',
477
 
                              {'branch':self.branch,
478
 
                               'bzrlib':bzrlib,
479
 
                               'rev_id':self.rev_id})
480
 
        # new style commit hooks:
481
 
        if not self.bound_branch:
482
 
            hook_master = self.branch
483
 
            hook_local = None
484
 
        else:
485
 
            hook_master = self.master_branch
486
 
            hook_local = self.branch
487
 
        # With bound branches, when the master is behind the local branch,
488
 
        # the 'old_revno' and old_revid values here are incorrect.
489
 
        # XXX: FIXME ^. RBC 20060206
490
 
        if self.parents:
491
 
            old_revid = self.parents[0]
492
 
        else:
493
 
            old_revid = bzrlib.revision.NULL_REVISION
494
 
        for hook in Branch.hooks['post_commit']:
495
 
            hook(hook_local, hook_master, old_revno, old_revid, new_revno,
496
 
                self.rev_id)
497
 
 
498
 
    def _cleanup(self):
499
 
        """Cleanup any open locks, progress bars etc."""
500
 
        cleanups = [self._cleanup_bound_branch,
501
 
                    self.basis_tree.unlock,
502
 
                    self.work_tree.unlock,
503
 
                    self.pb.finished]
504
 
        found_exception = None
505
 
        for cleanup in cleanups:
506
 
            try:
507
 
                cleanup()
508
 
            # we want every cleanup to run no matter what.
509
 
            # so we have a catchall here, but we will raise the
510
 
            # last encountered exception up the stack: and
511
 
            # typically this will be useful enough.
512
 
            except Exception, e:
513
 
                found_exception = e
514
 
        if found_exception is not None: 
515
 
            # don't do a plan raise, because the last exception may have been
516
 
            # trashed, e is our sure-to-work exception even though it loses the
517
 
            # full traceback. XXX: RBC 20060421 perhaps we could check the
518
 
            # exc_info and if its the same one do a plain raise otherwise 
519
 
            # 'raise e' as we do now.
520
 
            raise e
521
 
 
522
 
    def _cleanup_bound_branch(self):
523
 
        """Executed at the end of a try/finally to cleanup a bound branch.
524
 
 
525
 
        If the branch wasn't bound, this is a no-op.
526
 
        If it was, it resents self.branch to the local branch, instead
527
 
        of being the master.
528
 
        """
529
 
        if not self.bound_branch:
530
 
            return
531
 
        if self.master_locked:
532
 
            self.master_branch.unlock()
533
 
 
534
 
    def _escape_commit_message(self):
535
 
        """Replace xml-incompatible control characters."""
536
 
        # FIXME: RBC 20060419 this should be done by the revision
537
 
        # serialiser not by commit. Then we can also add an unescaper
538
 
        # in the deserializer and start roundtripping revision messages
539
 
        # precisely. See repository_implementations/test_repository.py
540
 
        
541
 
        # Python strings can include characters that can't be
542
 
        # represented in well-formed XML; escape characters that
543
 
        # aren't listed in the XML specification
544
 
        # (http://www.w3.org/TR/REC-xml/#NT-Char).
545
 
        self.message, escape_count = re.subn(
546
 
            u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
547
 
            lambda match: match.group(0).encode('unicode_escape'),
548
 
            self.message)
549
 
        if escape_count:
550
 
            self.reporter.escaped(escape_count, self.message)
551
 
 
552
 
    def _gather_parents(self):
553
 
        """Record the parents of a merge for merge detection."""
554
 
        # TODO: Make sure that this list doesn't contain duplicate 
555
 
        # entries and the order is preserved when doing this.
556
 
        self.parents = self.work_tree.get_parent_ids()
557
 
        self.parent_invs = [self.basis_inv]
558
 
        for revision in self.parents[1:]:
559
 
            if self.branch.repository.has_revision(revision):
560
 
                mutter('commit parent revision {%s}', revision)
561
 
                inventory = self.branch.repository.get_inventory(revision)
562
 
                self.parent_invs.append(inventory)
563
 
            else:
564
 
                mutter('commit parent ghost revision {%s}', revision)
565
 
 
566
 
    def _remove_deleted(self):
567
 
        """Remove deleted files from the working inventories.
568
 
 
569
 
        This is done prior to taking the working inventory as the
570
 
        basis for the new committed inventory.
571
 
 
572
 
        This returns true if any files
573
 
        *that existed in the basis inventory* were deleted.
574
 
        Files that were added and deleted
575
 
        in the working copy don't matter.
576
 
        """
577
 
        specific = self.specific_files
578
 
        deleted_ids = []
579
 
        deleted_paths = set()
580
 
        for path, ie in self.work_inv.iter_entries():
581
 
            if is_inside_any(deleted_paths, path):
582
 
                # The tree will delete the required ids recursively.
583
 
                continue
584
 
            if specific and not is_inside_any(specific, path):
585
 
                continue
586
 
            if not self.work_tree.has_filename(path):
587
 
                deleted_paths.add(path)
588
 
                self.reporter.missing(path)
589
 
                deleted_ids.append(ie.file_id)
590
 
        self.work_tree.unversion(deleted_ids)
591
 
 
592
 
    def _populate_new_inv(self):
593
 
        """Build revision inventory.
594
 
 
595
 
        This creates a new empty inventory. Depending on
596
 
        which files are selected for commit, and what is present in the
597
 
        current tree, the new inventory is populated. inventory entries 
598
 
        which are candidates for modification have their revision set to
599
 
        None; inventory entries that are carried over untouched have their
600
 
        revision set to their prior value.
601
 
        """
602
 
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
603
 
        # results to create a new inventory at the same time, which results
604
 
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
605
 
        # ADHB 11-07-2006
606
 
        mutter("Selecting files for commit with filter %s", self.specific_files)
607
 
        assert self.work_inv.root is not None
608
 
        entries = self.work_inv.iter_entries()
609
 
        if not self.builder.record_root_entry:
610
 
            symbol_versioning.warn('CommitBuilders should support recording'
611
 
                ' the root entry as of bzr 0.10.', DeprecationWarning, 
612
 
                stacklevel=1)
613
 
            self.builder.new_inventory.add(self.basis_inv.root.copy())
614
 
            entries.next()
615
 
        self.pb_entries_total = len(self.work_inv)
616
 
        for path, new_ie in entries:
617
 
            self._emit_progress_next_entry()
618
 
            file_id = new_ie.file_id
619
 
            try:
620
 
                kind = self.work_tree.kind(file_id)
621
 
                if kind == 'tree-reference' and self.recursive == 'down':
622
 
                    # nested tree: commit in it
623
 
                    sub_tree = WorkingTree.open(self.work_tree.abspath(path))
624
 
                    # FIXME: be more comprehensive here:
625
 
                    # this works when both trees are in --trees repository,
626
 
                    # but when both are bound to a different repository,
627
 
                    # it fails; a better way of approaching this is to 
628
 
                    # finally implement the explicit-caches approach design
629
 
                    # a while back - RBC 20070306.
630
 
                    if (sub_tree.branch.repository.bzrdir.root_transport.base
631
 
                        ==
632
 
                        self.work_tree.branch.repository.bzrdir.root_transport.base):
633
 
                        sub_tree.branch.repository = \
634
 
                            self.work_tree.branch.repository
635
 
                    try:
636
 
                        sub_tree.commit(message=None, revprops=self.revprops,
637
 
                            recursive=self.recursive,
638
 
                            message_callback=self.message_callback,
639
 
                            timestamp=self.timestamp, timezone=self.timezone,
640
 
                            committer=self.committer,
641
 
                            allow_pointless=self.allow_pointless,
642
 
                            strict=self.strict, verbose=self.verbose,
643
 
                            local=self.local, reporter=self.reporter)
644
 
                    except errors.PointlessCommit:
645
 
                        pass
646
 
                if kind != new_ie.kind:
647
 
                    new_ie = inventory.make_entry(kind, new_ie.name,
648
 
                                                  new_ie.parent_id, file_id)
649
 
            except errors.NoSuchFile:
 
53
    from branch import gen_file_id
 
54
    from errors import BzrError
 
55
    from revision import Revision
 
56
    from trace import mutter, note
 
57
 
 
58
    branch._need_writelock()
 
59
 
 
60
    ## TODO: Show branch names
 
61
 
 
62
    # TODO: Don't commit if there are no changes, unless forced?
 
63
 
 
64
    # First walk over the working inventory; and both update that
 
65
    # and also build a new revision inventory.  The revision
 
66
    # inventory needs to hold the text-id, sha1 and size of the
 
67
    # actual file versions committed in the revision.  (These are
 
68
    # not present in the working inventory.)  We also need to
 
69
    # detect missing/deleted files, and remove them from the
 
70
    # working inventory.
 
71
 
 
72
    work_tree = branch.working_tree()
 
73
    work_inv = work_tree.inventory
 
74
    inv = Inventory()
 
75
    basis = branch.basis_tree()
 
76
    basis_inv = basis.inventory
 
77
    missing_ids = []
 
78
 
 
79
    print 'looking for changes...'
 
80
    for path, entry in work_inv.iter_entries():
 
81
        ## TODO: Cope with files that have gone missing.
 
82
 
 
83
        ## TODO: Check that the file kind has not changed from the previous
 
84
        ## revision of this file (if any).
 
85
 
 
86
        entry = entry.copy()
 
87
 
 
88
        p = branch.abspath(path)
 
89
        file_id = entry.file_id
 
90
        mutter('commit prep file %s, id %r ' % (p, file_id))
 
91
 
 
92
        if specific_files and not is_inside_any(specific_files, path):
 
93
            if basis_inv.has_id(file_id):
 
94
                # carry over with previous state
 
95
                inv.add(basis_inv[file_id].copy())
 
96
            else:
 
97
                # omit this from committed inventory
650
98
                pass
651
 
            # mutter('check %s {%s}', path, file_id)
652
 
            if (not self.specific_files or 
653
 
                is_inside_or_parent_of_any(self.specific_files, path)):
654
 
                    # mutter('%s selected for commit', path)
655
 
                    ie = new_ie.copy()
656
 
                    ie.revision = None
 
99
            continue
 
100
 
 
101
        if not work_tree.has_id(file_id):
 
102
            note('deleted %s%s' % (path, kind_marker(entry.kind)))
 
103
            mutter("    file is missing, removing from inventory")
 
104
            missing_ids.append(file_id)
 
105
            continue
 
106
 
 
107
        inv.add(entry)
 
108
 
 
109
        if basis_inv.has_id(file_id):
 
110
            old_kind = basis_inv[file_id].kind
 
111
            if old_kind != entry.kind:
 
112
                raise BzrError("entry %r changed kind from %r to %r"
 
113
                        % (file_id, old_kind, entry.kind))
 
114
 
 
115
        if entry.kind == 'directory':
 
116
            if not isdir(p):
 
117
                raise BzrError("%s is entered as directory but not a directory"
 
118
                               % quotefn(p))
 
119
        elif entry.kind == 'file':
 
120
            if not isfile(p):
 
121
                raise BzrError("%s is entered as file but is not a file" % quotefn(p))
 
122
 
 
123
            new_sha1 = work_tree.get_file_sha1(file_id)
 
124
 
 
125
            old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
 
126
            if (old_ie
 
127
                and old_ie.text_sha1 == new_sha1):
 
128
                ## assert content == basis.get_file(file_id).read()
 
129
                entry.text_id = old_ie.text_id
 
130
                entry.text_sha1 = new_sha1
 
131
                entry.text_size = old_ie.text_size
 
132
                mutter('    unchanged from previous text_id {%s}' %
 
133
                       entry.text_id)
657
134
            else:
658
 
                # mutter('%s not selected for commit', path)
659
 
                if self.basis_inv.has_id(file_id):
660
 
                    ie = self.basis_inv[file_id].copy()
 
135
                content = file(p, 'rb').read()
 
136
 
 
137
                entry.text_sha1 = sha_string(content)
 
138
                entry.text_size = len(content)
 
139
 
 
140
                entry.text_id = gen_file_id(entry.name)
 
141
                branch.text_store.add(content, entry.text_id)
 
142
                mutter('    stored with text_id {%s}' % entry.text_id)
 
143
                if not old_ie:
 
144
                    note('added %s' % path)
 
145
                elif (old_ie.name == entry.name
 
146
                      and old_ie.parent_id == entry.parent_id):
 
147
                    note('modified %s' % path)
661
148
                else:
662
 
                    # this entry is new and not being committed
663
 
                    continue
664
 
            self.builder.record_entry_contents(ie, self.parent_invs, 
665
 
                path, self.work_tree)
666
 
            # describe the nature of the change that has occurred relative to
667
 
            # the basis inventory.
668
 
            if (self.basis_inv.has_id(ie.file_id)):
669
 
                basis_ie = self.basis_inv[ie.file_id]
670
 
            else:
671
 
                basis_ie = None
672
 
            change = ie.describe_change(basis_ie, ie)
673
 
            if change in (InventoryEntry.RENAMED, 
674
 
                InventoryEntry.MODIFIED_AND_RENAMED):
675
 
                old_path = self.basis_inv.id2path(ie.file_id)
676
 
                self.reporter.renamed(change, old_path, path)
677
 
            else:
678
 
                self.reporter.snapshot_change(change, path)
679
 
 
680
 
        if not self.specific_files:
681
 
            return
682
 
 
683
 
        # ignore removals that don't match filespec
684
 
        for path, new_ie in self.basis_inv.iter_entries():
685
 
            if new_ie.file_id in self.work_inv:
686
 
                continue
687
 
            if is_inside_any(self.specific_files, path):
688
 
                continue
689
 
            ie = new_ie.copy()
690
 
            ie.revision = None
691
 
            self.builder.record_entry_contents(ie, self.parent_invs, path,
692
 
                                               self.basis_tree)
693
 
 
694
 
    def _emit_progress_set_stage(self, name, show_entries=False):
695
 
        """Set the progress stage and emit an update to the progress bar."""
696
 
        self.pb_stage_name = name
697
 
        self.pb_stage_count += 1
698
 
        self.pb_entries_show = show_entries
699
 
        if show_entries:
700
 
            self.pb_entries_count = 0
701
 
            self.pb_entries_total = '?'
702
 
        self._emit_progress()
703
 
 
704
 
    def _emit_progress_next_entry(self):
705
 
        """Emit an update to the progress bar and increment the file count."""
706
 
        self.pb_entries_count += 1
707
 
        self._emit_progress()
708
 
 
709
 
    def _emit_progress(self):
710
 
        if self.pb_entries_show:
711
 
            text = "%s [Entry %d/%s] - Stage" % (self.pb_stage_name,
712
 
                self.pb_entries_count,str(self.pb_entries_total))
713
 
        else:
714
 
            text = "%s - Stage" % (self.pb_stage_name)
715
 
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
716
 
 
717
 
    def _report_deletes(self):
718
 
        for path, ie in self.basis_inv.iter_entries():
719
 
            if ie.file_id not in self.builder.new_inventory:
720
 
                self.reporter.deleted(path)
 
149
                    note('renamed %s' % path)
 
150
 
 
151
 
 
152
    for file_id in missing_ids:
 
153
        # Any files that have been deleted are now removed from the
 
154
        # working inventory.  Files that were not selected for commit
 
155
        # are left as they were in the working inventory and ommitted
 
156
        # from the revision inventory.
 
157
        
 
158
        # have to do this later so we don't mess up the iterator.
 
159
        # since parents may be removed before their children we
 
160
        # have to test.
 
161
 
 
162
        # FIXME: There's probably a better way to do this; perhaps
 
163
        # the workingtree should know how to filter itbranch.
 
164
        if work_inv.has_id(file_id):
 
165
            del work_inv[file_id]
 
166
 
 
167
 
 
168
    if rev_id is None:
 
169
        rev_id = _gen_revision_id(time.time())
 
170
    inv_id = rev_id
 
171
 
 
172
    inv_tmp = tempfile.TemporaryFile()
 
173
    inv.write_xml(inv_tmp)
 
174
    inv_tmp.seek(0)
 
175
    branch.inventory_store.add(inv_tmp, inv_id)
 
176
    mutter('new inventory_id is {%s}' % inv_id)
 
177
 
 
178
    branch._write_inventory(work_inv)
 
179
 
 
180
    if timestamp == None:
 
181
        timestamp = time.time()
 
182
 
 
183
    if committer == None:
 
184
        committer = username()
 
185
 
 
186
    if timezone == None:
 
187
        timezone = local_time_offset()
 
188
 
 
189
    mutter("building commit log message")
 
190
    rev = Revision(timestamp=timestamp,
 
191
                   timezone=timezone,
 
192
                   committer=committer,
 
193
                   precursor = branch.last_patch(),
 
194
                   message = message,
 
195
                   inventory_id=inv_id,
 
196
                   revision_id=rev_id)
 
197
 
 
198
    rev_tmp = tempfile.TemporaryFile()
 
199
    rev.write_xml(rev_tmp)
 
200
    rev_tmp.seek(0)
 
201
    branch.revision_store.add(rev_tmp, rev_id)
 
202
    mutter("new revision_id is {%s}" % rev_id)
 
203
 
 
204
    ## XXX: Everything up to here can simply be orphaned if we abort
 
205
    ## the commit; it will leave junk files behind but that doesn't
 
206
    ## matter.
 
207
 
 
208
    ## TODO: Read back the just-generated changeset, and make sure it
 
209
    ## applies and recreates the right state.
 
210
 
 
211
    ## TODO: Also calculate and store the inventory SHA1
 
212
    mutter("committing patch r%d" % (branch.revno() + 1))
 
213
 
 
214
 
 
215
    branch.append_revision(rev_id)
 
216
 
 
217
    note("commited r%d" % branch.revno())
 
218
 
 
219
 
 
220
 
 
221
def _gen_revision_id(when):
 
222
    """Return new revision-id."""
 
223
    from binascii import hexlify
 
224
    from osutils import rand_bytes, compact_date, user_email
 
225
 
 
226
    s = '%s-%s-' % (user_email(), compact_date(when))
 
227
    s += hexlify(rand_bytes(8))
 
228
    return s
 
229
 
721
230