~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Robert Collins
  • Date: 2005-10-03 05:54:35 UTC
  • mto: (1393.1.30)
  • mto: This revision was merged to the branch mainline in revision 1400.
  • Revision ID: robertc@robertcollins.net-20051003055434-c8ebd30d1de10247
move exporting functionality into inventory.py - uncovers bug in symlink support

Show diffs side-by-side

added added

removed removed

Lines of Context:
44
44
# TODO: Update hashcache before and after - or does the WorkingTree
45
45
# look after that?
46
46
 
 
47
# This code requires all merge parents to be present in the branch.
 
48
# We could relax this but for the sake of simplicity the constraint is
 
49
# here for now.  It's not totally clear to me how we'd know which file
 
50
# need new text versions if some parents are absent.  -- mbp 20050915
 
51
 
47
52
# TODO: Rather than mashing together the ancestry and storing it back,
48
53
# perhaps the weave should have single method which does it all in one
49
54
# go, avoiding a lot of redundant work.
56
61
# merges from, then it should still be reported as newly added
57
62
# relative to the basis revision.
58
63
 
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.
62
 
 
63
 
# TODO: If commit fails, leave the message in a file somewhere.
64
 
 
65
64
 
66
65
import os
67
66
import re
72
71
from binascii import hexlify
73
72
from cStringIO import StringIO
74
73
 
75
 
from bzrlib.osutils import (local_time_offset,
76
 
                            rand_bytes, compact_date,
 
74
from bzrlib.osutils import (local_time_offset, username,
 
75
                            rand_bytes, compact_date, user_email,
77
76
                            kind_marker, is_inside_any, quotefn,
78
77
                            sha_string, sha_strings, sha_file, isdir, isfile,
79
78
                            split_lines)
80
79
from bzrlib.branch import gen_file_id
81
 
import bzrlib.config
82
80
from bzrlib.errors import (BzrError, PointlessCommit,
83
81
                           HistoryMissing,
84
 
                           ConflictsInTree,
85
 
                           StrictCommitFailed
86
82
                           )
87
 
import bzrlib.gpg as gpg
88
83
from bzrlib.revision import Revision
89
 
from bzrlib.testament import Testament
90
84
from bzrlib.trace import mutter, note, warning
91
85
from bzrlib.xml5 import serializer_v5
92
 
from bzrlib.inventory import Inventory, ROOT_ID
 
86
from bzrlib.inventory import Inventory
93
87
from bzrlib.weave import Weave
94
88
from bzrlib.weavefile import read_weave, write_weave_v5
95
89
from bzrlib.atomicfile import AtomicFile
108
102
 
109
103
class NullCommitReporter(object):
110
104
    """I report on progress of a commit."""
111
 
 
112
 
    def snapshot_change(self, change, path):
113
 
        pass
114
 
 
115
 
    def completed(self, revno, rev_id):
116
 
        pass
117
 
 
118
 
    def deleted(self, file_id):
119
 
        pass
120
 
 
121
 
    def escaped(self, escape_count, message):
122
 
        pass
123
 
 
124
 
    def missing(self, path):
125
 
        pass
 
105
    def added(self, path):
 
106
        pass
 
107
 
 
108
    def removed(self, path):
 
109
        pass
 
110
 
 
111
    def renamed(self, old_path, new_path):
 
112
        pass
 
113
 
126
114
 
127
115
class ReportCommitToLog(NullCommitReporter):
128
 
 
129
 
    def snapshot_change(self, change, path):
130
 
        note("%s %s", change, path)
131
 
 
132
 
    def completed(self, revno, rev_id):
133
 
        note('committed r%d {%s}', revno, rev_id)
134
 
    
135
 
    def deleted(self, file_id):
136
 
        note('deleted %s', file_id)
137
 
 
138
 
    def escaped(self, escape_count, message):
139
 
        note("replaced %d control characters in message", escape_count)
140
 
 
141
 
    def missing(self, path):
142
 
        note('missing %s', path)
 
116
    def added(self, path):
 
117
        note('added %s', path)
 
118
 
 
119
    def removed(self, path):
 
120
        note('removed %s', path)
 
121
 
 
122
    def renamed(self, old_path, new_path):
 
123
        note('renamed %s => %s', old_path, new_path)
 
124
 
143
125
 
144
126
class Commit(object):
145
127
    """Task of committing a new revision.
154
136
            working inventory.
155
137
    """
156
138
    def __init__(self,
157
 
                 reporter=None,
158
 
                 config=None):
 
139
                 reporter=None):
159
140
        if reporter is not None:
160
141
            self.reporter = reporter
161
142
        else:
162
143
            self.reporter = NullCommitReporter()
163
 
        if config is not None:
164
 
            self.config = config
165
 
        else:
166
 
            self.config = None
 
144
 
167
145
        
168
146
    def commit(self,
169
147
               branch, message,
173
151
               specific_files=None,
174
152
               rev_id=None,
175
153
               allow_pointless=True,
176
 
               strict=False,
177
 
               verbose=False,
178
 
               revprops=None):
 
154
               verbose=False):
179
155
        """Commit working copy as a new revision.
180
156
 
181
157
        timestamp -- if not None, seconds-since-epoch for a
191
167
 
192
168
        allow_pointless -- If true (default), commit even if nothing
193
169
            has changed and no merges are recorded.
194
 
 
195
 
        strict -- If true, don't allow a commit if the working tree
196
 
            contains unknown files.
197
 
 
198
 
        revprops -- Properties for new revision
199
170
        """
200
171
        mutter('preparing to commit')
201
172
 
204
175
        self.rev_id = rev_id
205
176
        self.specific_files = specific_files
206
177
        self.allow_pointless = allow_pointless
207
 
        self.revprops = revprops
208
 
 
209
 
        if strict and branch.unknowns():
210
 
            raise StrictCommitFailed()
211
178
 
212
179
        if timestamp is None:
213
180
            self.timestamp = time.time()
214
181
        else:
215
182
            self.timestamp = long(timestamp)
216
183
            
217
 
        if self.config is None:
218
 
            self.config = bzrlib.config.BranchConfig(self.branch)
219
 
 
220
184
        if rev_id is None:
221
 
            self.rev_id = _gen_revision_id(self.config, self.timestamp)
 
185
            self.rev_id = _gen_revision_id(self.branch, self.timestamp)
222
186
        else:
223
187
            self.rev_id = rev_id
224
188
 
225
189
        if committer is None:
226
 
            self.committer = self.config.username()
 
190
            self.committer = username(self.branch)
227
191
        else:
228
192
            assert isinstance(committer, basestring), type(committer)
229
193
            self.committer = committer
259
223
                    or self.new_inv != self.basis_inv):
260
224
                raise PointlessCommit()
261
225
 
262
 
            if len(list(self.work_tree.iter_conflicts()))>0:
263
 
                raise ConflictsInTree
264
 
 
265
226
            self._record_inventory()
 
227
            self._record_ancestry()
266
228
            self._make_revision()
267
 
            self.reporter.completed(self.branch.revno()+1, self.rev_id)
 
229
            note('committed r%d {%s}', (self.branch.revno() + 1),
 
230
                 self.rev_id)
268
231
            self.branch.append_revision(self.rev_id)
269
232
            self.branch.set_pending_merges([])
270
233
        finally:
276
239
        self.inv_sha1 = sha_string(inv_text)
277
240
        s = self.branch.control_weaves
278
241
        s.add_text('inventory', self.rev_id,
279
 
                   split_lines(inv_text), self.present_parents,
280
 
                   self.branch.get_transaction())
 
242
                   split_lines(inv_text), self.present_parents)
281
243
 
282
244
    def _escape_commit_message(self):
283
245
        """Replace xml-incompatible control characters."""
296
258
            lambda match: match.group(0).encode('unicode_escape'),
297
259
            self.message)
298
260
        if escape_count:
299
 
            self.reporter.escaped(escape_count, self.message)
 
261
            note("replaced %d control characters in message", escape_count)
 
262
 
 
263
    def _record_ancestry(self):
 
264
        """Append merged revision ancestry to the ancestry file.
 
265
 
 
266
        This should be the merged ancestry of all parents, plus the
 
267
        new revision id."""
 
268
        s = self.branch.control_weaves
 
269
        w = s.get_weave_or_empty('ancestry')
 
270
        lines = self._make_ancestry(w)
 
271
        w.add(self.rev_id, self.present_parents, lines)
 
272
        s.put_weave('ancestry', w)
 
273
 
 
274
    def _make_ancestry(self, ancestry_weave):
 
275
        """Return merged ancestry lines.
 
276
 
 
277
        The lines are revision-ids followed by newlines."""
 
278
        parent_ancestries = [ancestry_weave.get(p) for p in self.present_parents]
 
279
        new_lines = merge_ancestry_lines(self.rev_id, parent_ancestries)
 
280
        mutter('merged ancestry of {%s}:\n%s', self.rev_id, ''.join(new_lines))
 
281
        return new_lines
300
282
 
301
283
    def _gather_parents(self):
302
284
        """Record the parents of a merge for merge detection."""
303
285
        pending_merges = self.branch.pending_merges()
304
286
        self.parents = []
305
 
        self.parent_invs = []
 
287
        self.parent_trees = []
306
288
        self.present_parents = []
307
289
        precursor_id = self.branch.last_revision()
308
290
        if precursor_id:
310
292
        self.parents += pending_merges
311
293
        for revision in self.parents:
312
294
            if self.branch.has_revision(revision):
313
 
                self.parent_invs.append(self.branch.get_inventory(revision))
 
295
                self.parent_trees.append(self.branch.revision_tree(revision))
314
296
                self.present_parents.append(revision)
315
297
 
316
298
    def _check_parents_present(self):
318
300
            mutter('commit parent revision {%s}', parent_id)
319
301
            if not self.branch.has_revision(parent_id):
320
302
                if parent_id == self.branch.last_revision():
321
 
                    warning("parent is missing %r", parent_id)
 
303
                    warning("parent is pissing %r", parent_id)
322
304
                    raise HistoryMissing(self.branch, 'revision', parent_id)
323
305
                else:
324
306
                    mutter("commit will ghost revision %r", parent_id)
330
312
                            committer=self.committer,
331
313
                            message=self.message,
332
314
                            inventory_sha1=self.inv_sha1,
333
 
                            revision_id=self.rev_id,
334
 
                            properties=self.revprops)
 
315
                            revision_id=self.rev_id)
335
316
        self.rev.parent_ids = self.parents
336
317
        rev_tmp = StringIO()
337
318
        serializer_v5.write_revision(self.rev, rev_tmp)
338
319
        rev_tmp.seek(0)
339
 
        if self.config.signature_needed():
340
 
            plaintext = Testament(self.rev, self.new_inv).as_short_text()
341
 
            self.branch.store_revision_signature(gpg.GPGStrategy(self.config),
342
 
                                                 plaintext, self.rev_id)
343
320
        self.branch.revision_store.add(rev_tmp, self.rev_id)
344
321
        mutter('new revision_id is {%s}', self.rev_id)
345
322
 
 
323
 
346
324
    def _remove_deleted(self):
347
325
        """Remove deleted files from the working inventories.
348
326
 
360
338
            if specific and not is_inside_any(specific, path):
361
339
                continue
362
340
            if not self.work_tree.has_filename(path):
363
 
                self.reporter.missing(path)
364
 
                deleted_ids.append((path, ie.file_id))
 
341
                note('missing %s', path)
 
342
                deleted_ids.append(ie.file_id)
365
343
        if deleted_ids:
366
 
            deleted_ids.sort(reverse=True)
367
 
            for path, file_id in deleted_ids:
368
 
                del self.work_inv[file_id]
 
344
            for file_id in deleted_ids:
 
345
                if file_id in self.work_inv:
 
346
                    del self.work_inv[file_id]
369
347
            self.branch._write_inventory(self.work_inv)
370
348
 
 
349
 
 
350
    def _find_entry_parents(self, file_id):
 
351
        """Return the text versions and hashes for all file parents.
 
352
 
 
353
        Returned as a map from text version to inventory entry.
 
354
 
 
355
        This is a set containing the file versions in all parents
 
356
        revisions containing the file.  If the file is new, the set
 
357
        will be empty."""
 
358
        r = {}
 
359
        for tree in self.parent_trees:
 
360
            if file_id in tree.inventory:
 
361
                ie = tree.inventory[file_id]
 
362
                assert ie.file_id == file_id
 
363
                if ie.revision in r:
 
364
                    assert r[ie.revision] == ie
 
365
                else:
 
366
                    r[ie.revision] = ie
 
367
        return r
 
368
 
371
369
    def _store_snapshot(self):
372
370
        """Pass over inventory and record a snapshot.
373
371
 
374
372
        Entries get a new revision when they are modified in 
375
373
        any way, which includes a merge with a new set of
376
 
        parents that have the same entry. 
 
374
        parents that have the same entry. Currently we do not
 
375
        check for that set being ancestors of each other - and
 
376
        we should - only parallel children should count for this
 
377
        test see find_entry_parents to correct this. FIXME <---
 
378
        I.e. if we are merging in revision FOO, and our
 
379
        copy of file id BAR is identical to FOO.BAR, we should
 
380
        generate a new revision of BAR IF and only IF FOO is
 
381
        neither a child of our current tip, nor an ancestor of
 
382
        our tip. The presence of FOO in our store should not 
 
383
        affect this logic UNLESS we are doing a merge of FOO,
 
384
        or a child of FOO.
377
385
        """
378
386
        # XXX: Need to think more here about when the user has
379
387
        # made a specific decision on a particular value -- c.f.
380
388
        # mark-merge.  
381
389
        for path, ie in self.new_inv.iter_entries():
382
 
            previous_entries = ie.find_previous_heads(
383
 
                self.parent_invs, 
384
 
                self.weave_store.get_weave_or_empty(ie.file_id,
385
 
                    self.branch.get_transaction()))
 
390
            previous_entries = self._find_entry_parents(ie. file_id)
386
391
            if ie.revision is None:
387
392
                change = ie.snapshot(self.rev_id, path, previous_entries,
388
 
                                     self.work_tree, self.weave_store,
389
 
                                     self.branch.get_transaction())
 
393
                                     self.work_tree, self.weave_store)
390
394
            else:
391
395
                change = "unchanged"
392
 
            self.reporter.snapshot_change(change, path)
 
396
            note("%s %s", change, path)
393
397
 
394
398
    def _populate_new_inv(self):
395
399
        """Build revision inventory.
409
413
            if self.specific_files:
410
414
                if not is_inside_any(self.specific_files, path):
411
415
                    mutter('%s not selected for commit', path)
412
 
                    self._carry_entry(file_id)
 
416
                    self._carry_file(file_id)
413
417
                    continue
414
 
                else:
415
 
                    # this is selected, ensure its parents are too.
416
 
                    parent_id = new_ie.parent_id
417
 
                    while parent_id != ROOT_ID:
418
 
                        if not self.new_inv.has_id(parent_id):
419
 
                            ie = self._select_entry(self.work_inv[parent_id])
420
 
                            mutter('%s selected for commit because of %s',
421
 
                                   self.new_inv.id2path(parent_id), path)
422
 
 
423
 
                        ie = self.new_inv[parent_id]
424
 
                        if ie.revision is not None:
425
 
                            ie.revision = None
426
 
                            mutter('%s selected for commit because of %s',
427
 
                                   self.new_inv.id2path(parent_id), path)
428
 
                        parent_id = ie.parent_id
429
418
            mutter('%s selected for commit', path)
430
 
            self._select_entry(new_ie)
431
 
 
432
 
    def _select_entry(self, new_ie):
433
 
        """Make new_ie be considered for committing."""
434
 
        ie = new_ie.copy()
435
 
        ie.revision = None
436
 
        self.new_inv.add(ie)
437
 
        return ie
438
 
 
439
 
    def _carry_entry(self, file_id):
 
419
            ie = new_ie.copy()
 
420
            ie.revision = None
 
421
            self.new_inv.add(ie)
 
422
 
 
423
    def _carry_file(self, file_id):
440
424
        """Carry the file unchanged from the basis revision."""
441
425
        if self.basis_inv.has_id(file_id):
442
426
            self.new_inv.add(self.basis_inv[file_id].copy())
444
428
    def _report_deletes(self):
445
429
        for file_id in self.basis_inv:
446
430
            if file_id not in self.new_inv:
447
 
                self.reporter.deleted(self.basis_inv.id2path(file_id))
448
 
 
449
 
def _gen_revision_id(config, when):
 
431
                note('deleted %s', self.basis_inv.id2path(file_id))
 
432
 
 
433
 
 
434
 
 
435
def _gen_revision_id(branch, when):
450
436
    """Return new revision-id."""
451
 
    s = '%s-%s-' % (config.user_email(), compact_date(when))
 
437
    s = '%s-%s-' % (user_email(branch), compact_date(when))
452
438
    s += hexlify(rand_bytes(8))
453
439
    return s
 
440
 
 
441
 
 
442
 
 
443
    
 
444
def merge_ancestry_lines(rev_id, ancestries):
 
445
    """Return merged ancestry lines.
 
446
 
 
447
    rev_id -- id of the new revision
 
448
    
 
449
    ancestries -- a sequence of ancestries for parent revisions,
 
450
        as newline-terminated line lists.
 
451
    """
 
452
    if len(ancestries) == 0:
 
453
        return [rev_id + '\n']
 
454
    seen = set(ancestries[0])
 
455
    ancs = ancestries[0][:]    
 
456
    for parent_ancestry in ancestries[1:]:
 
457
        for line in parent_ancestry:
 
458
            assert line[-1] == '\n'
 
459
            if line not in seen:
 
460
                ancs.append(line)
 
461
                seen.add(line)
 
462
    r = rev_id + '\n'
 
463
    assert r not in seen
 
464
    ancs.append(r)
 
465
    return ancs