~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: John Arbash Meinel
  • Date: 2005-09-17 21:57:11 UTC
  • mto: (1393.2.1)
  • mto: This revision was merged to the branch mainline in revision 1396.
  • Revision ID: john@arbash-meinel.com-20050917215711-9fa31e650a1f2fd8
Got HttpTransport tests to pass. Check for EAGAIN, pass permit_failure around, etc

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
 
18
18
 
19
 
def commit(branch, message, timestamp=None, timezone=None,
 
19
def commit(branch, message,
 
20
           timestamp=None,
 
21
           timezone=None,
20
22
           committer=None,
21
 
           verbose=False):
 
23
           verbose=True,
 
24
           specific_files=None,
 
25
           rev_id=None,
 
26
           allow_pointless=True):
22
27
    """Commit working copy as a new revision.
23
28
 
24
29
    The basic approach is to add all the file texts into the
35
40
    be robust against files disappearing, moving, etc.  So the
36
41
    whole thing is a bit hard.
37
42
 
 
43
    This raises PointlessCommit if there are no changes, no new merges,
 
44
    and allow_pointless  is false.
 
45
 
38
46
    timestamp -- if not None, seconds-since-epoch for a
39
47
         postdated/predated commit.
40
 
    """
41
 
 
42
 
    import os, time, tempfile
43
 
 
44
 
    from inventory import Inventory
45
 
    from osutils import isdir, isfile, sha_string, quotefn, \
46
 
         local_time_offset, username
 
48
 
 
49
    specific_files
 
50
        If true, commit only those files.
 
51
 
 
52
    rev_id
 
53
        If set, use this as the new revision id.
 
54
        Useful for test or import commands that need to tightly
 
55
        control what revisions are assigned.  If you duplicate
 
56
        a revision id that exists elsewhere it is your own fault.
 
57
        If null (default), a time/random revision id is generated.
 
58
    """
 
59
 
 
60
    import time, tempfile, re
 
61
 
 
62
    from bzrlib.osutils import local_time_offset, username
 
63
    from bzrlib.branch import gen_file_id
 
64
    from bzrlib.errors import BzrError, PointlessCommit
 
65
    from bzrlib.revision import Revision, RevisionReference
 
66
    from bzrlib.trace import mutter, note
 
67
    from bzrlib.xml import serializer_v4
 
68
 
 
69
    branch.lock_write()
 
70
 
 
71
    try:
 
72
        # First walk over the working inventory; and both update that
 
73
        # and also build a new revision inventory.  The revision
 
74
        # inventory needs to hold the text-id, sha1 and size of the
 
75
        # actual file versions committed in the revision.  (These are
 
76
        # not present in the working inventory.)  We also need to
 
77
        # detect missing/deleted files, and remove them from the
 
78
        # working inventory.
 
79
 
 
80
        work_tree = branch.working_tree()
 
81
        work_inv = work_tree.inventory
 
82
        basis = branch.basis_tree()
 
83
        basis_inv = basis.inventory
 
84
 
 
85
        if verbose:
 
86
            # note('looking for changes...')
 
87
            # print 'looking for changes...'
 
88
            # disabled; should be done at a higher level
 
89
            pass
 
90
 
 
91
        pending_merges = branch.pending_merges()
 
92
 
 
93
        missing_ids, new_inv, any_changes = \
 
94
                     _gather_commit(branch,
 
95
                                    work_tree,
 
96
                                    work_inv,
 
97
                                    basis_inv,
 
98
                                    specific_files,
 
99
                                    verbose)
 
100
 
 
101
        if not (any_changes or allow_pointless or pending_merges):
 
102
            raise PointlessCommit()
 
103
 
 
104
        for file_id in missing_ids:
 
105
            # Any files that have been deleted are now removed from the
 
106
            # working inventory.  Files that were not selected for commit
 
107
            # are left as they were in the working inventory and ommitted
 
108
            # from the revision inventory.
 
109
 
 
110
            # have to do this later so we don't mess up the iterator.
 
111
            # since parents may be removed before their children we
 
112
            # have to test.
 
113
 
 
114
            # FIXME: There's probably a better way to do this; perhaps
 
115
            # the workingtree should know how to filter itbranch.
 
116
            if work_inv.has_id(file_id):
 
117
                del work_inv[file_id]
 
118
 
 
119
        if rev_id is None:
 
120
            rev_id = _gen_revision_id(branch, time.time())
 
121
        inv_id = rev_id
 
122
 
 
123
        inv_tmp = tempfile.TemporaryFile()
 
124
        
 
125
        serializer_v4.write_inventory(new_inv, inv_tmp)
 
126
        inv_tmp.seek(0)
 
127
        branch.inventory_store.add(inv_tmp, inv_id)
 
128
        mutter('new inventory_id is {%s}' % inv_id)
 
129
 
 
130
        # We could also just sha hash the inv_tmp file
 
131
        # however, in the case that branch.inventory_store.add()
 
132
        # ever actually does anything special
 
133
        inv_sha1 = branch.get_inventory_sha1(inv_id)
 
134
 
 
135
        branch._write_inventory(work_inv)
 
136
 
 
137
        if timestamp == None:
 
138
            timestamp = time.time()
 
139
 
 
140
        if committer == None:
 
141
            committer = username(branch)
 
142
 
 
143
        if timezone == None:
 
144
            timezone = local_time_offset()
 
145
 
 
146
        mutter("building commit log message")
 
147
        # Python strings can include characters that can't be
 
148
        # represented in well-formed XML; escape characters that
 
149
        # aren't listed in the XML specification
 
150
        # (http://www.w3.org/TR/REC-xml/#NT-Char).
 
151
        if isinstance(message, unicode):
 
152
            char_pattern = u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]'
 
153
        else:
 
154
            # Use a regular 'str' as pattern to avoid having re.subn
 
155
            # return 'unicode' results.
 
156
            char_pattern = '[^x09\x0A\x0D\x20-\xFF]'
 
157
        message, escape_count = re.subn(
 
158
            char_pattern,
 
159
            lambda match: match.group(0).encode('unicode_escape'),
 
160
            message)
 
161
        if escape_count:
 
162
            note("replaced %d control characters in message", escape_count)
 
163
        rev = Revision(timestamp=timestamp,
 
164
                       timezone=timezone,
 
165
                       committer=committer,
 
166
                       message = message,
 
167
                       inventory_id=inv_id,
 
168
                       inventory_sha1=inv_sha1,
 
169
                       revision_id=rev_id)
 
170
 
 
171
        rev.parents = []
 
172
        precursor_id = branch.last_patch()
 
173
        if precursor_id:
 
174
            precursor_sha1 = branch.get_revision_sha1(precursor_id)
 
175
            rev.parents.append(RevisionReference(precursor_id, precursor_sha1))
 
176
        for merge_rev in pending_merges:
 
177
            rev.parents.append(RevisionReference(merge_rev))            
 
178
 
 
179
        rev_tmp = tempfile.TemporaryFile()
 
180
        serializer_v4.write_revision(rev, rev_tmp)
 
181
        rev_tmp.seek(0)
 
182
        branch.revision_store.add(rev_tmp, rev_id)
 
183
        mutter("new revision_id is {%s}" % rev_id)
 
184
 
 
185
        ## XXX: Everything up to here can simply be orphaned if we abort
 
186
        ## the commit; it will leave junk files behind but that doesn't
 
187
        ## matter.
 
188
 
 
189
        ## TODO: Read back the just-generated changeset, and make sure it
 
190
        ## applies and recreates the right state.
 
191
 
 
192
        ## TODO: Also calculate and store the inventory SHA1
 
193
        mutter("committing patch r%d" % (branch.revno() + 1))
 
194
 
 
195
        branch.append_revision(rev_id)
 
196
 
 
197
        branch.set_pending_merges([])
 
198
 
 
199
        if verbose:
 
200
            # disabled; should go through logging
 
201
            # note("commited r%d" % branch.revno())
 
202
            # print ("commited r%d" % branch.revno())
 
203
            pass
 
204
    finally:
 
205
        branch.unlock()
 
206
 
 
207
 
 
208
 
 
209
def _gen_revision_id(branch, when):
 
210
    """Return new revision-id."""
 
211
    from binascii import hexlify
 
212
    from bzrlib.osutils import rand_bytes, compact_date, user_email
 
213
 
 
214
    s = '%s-%s-' % (user_email(branch), compact_date(when))
 
215
    s += hexlify(rand_bytes(8))
 
216
    return s
 
217
 
 
218
 
 
219
def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files,
 
220
                   verbose):
 
221
    """Build inventory preparatory to commit.
 
222
 
 
223
    Returns missing_ids, new_inv, any_changes.
 
224
 
 
225
    This adds any changed files into the text store, and sets their
 
226
    test-id, sha and size in the returned inventory appropriately.
 
227
 
 
228
    missing_ids
 
229
        Modified to hold a list of files that have been deleted from
 
230
        the working directory; these should be removed from the
 
231
        working inventory.
 
232
    """
 
233
    from bzrlib.inventory import Inventory
 
234
    from bzrlib.osutils import isdir, isfile, sha_string, quotefn, \
 
235
         local_time_offset, username, kind_marker, is_inside_any
47
236
    
48
 
    from branch import gen_file_id
49
 
    from errors import BzrError
50
 
    from revision import Revision
51
 
    from textui import show_status
52
 
    from trace import mutter, note
53
 
 
54
 
    branch._need_writelock()
55
 
 
56
 
    ## TODO: Show branch names
57
 
 
58
 
    # TODO: Don't commit if there are no changes, unless forced?
59
 
 
60
 
    # First walk over the working inventory; and both update that
61
 
    # and also build a new revision inventory.  The revision
62
 
    # inventory needs to hold the text-id, sha1 and size of the
63
 
    # actual file versions committed in the revision.  (These are
64
 
    # not present in the working inventory.)  We also need to
65
 
    # detect missing/deleted files, and remove them from the
66
 
    # working inventory.
67
 
 
68
 
    work_inv = branch.read_working_inventory()
69
 
    inv = Inventory()
70
 
    basis = branch.basis_tree()
71
 
    basis_inv = basis.inventory
 
237
    from bzrlib.branch import gen_file_id
 
238
    from bzrlib.errors import BzrError
 
239
    from bzrlib.revision import Revision
 
240
    from bzrlib.trace import mutter, note
 
241
 
 
242
    any_changes = False
 
243
    inv = Inventory(work_inv.root.file_id)
72
244
    missing_ids = []
 
245
    
73
246
    for path, entry in work_inv.iter_entries():
74
 
        ## TODO: Cope with files that have gone missing.
75
 
 
76
247
        ## TODO: Check that the file kind has not changed from the previous
77
248
        ## revision of this file (if any).
78
249
 
79
 
        entry = entry.copy()
80
 
 
81
250
        p = branch.abspath(path)
82
251
        file_id = entry.file_id
83
252
        mutter('commit prep file %s, id %r ' % (p, file_id))
84
253
 
85
 
        if not os.path.exists(p):
 
254
        if specific_files and not is_inside_any(specific_files, path):
 
255
            mutter('  skipping file excluded from commit')
 
256
            if basis_inv.has_id(file_id):
 
257
                # carry over with previous state
 
258
                inv.add(basis_inv[file_id].copy())
 
259
            else:
 
260
                # omit this from committed inventory
 
261
                pass
 
262
            continue
 
263
 
 
264
        if not work_tree.has_id(file_id):
 
265
            if verbose:
 
266
                print('deleted %s%s' % (path, kind_marker(entry.kind)))
 
267
            any_changes = True
86
268
            mutter("    file is missing, removing from inventory")
87
 
            if verbose:
88
 
                show_status('D', entry.kind, quotefn(path))
89
269
            missing_ids.append(file_id)
90
270
            continue
91
271
 
92
 
        # TODO: Handle files that have been deleted
93
 
 
94
 
        # TODO: Maybe a special case for empty files?  Seems a
95
 
        # waste to store them many times.
96
 
 
 
272
        # this is present in the new inventory; may be new, modified or
 
273
        # unchanged.
 
274
        old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
 
275
        
 
276
        entry = entry.copy()
97
277
        inv.add(entry)
98
278
 
99
 
        if basis_inv.has_id(file_id):
100
 
            old_kind = basis_inv[file_id].kind
 
279
        if old_ie:
 
280
            old_kind = old_ie.kind
101
281
            if old_kind != entry.kind:
102
282
                raise BzrError("entry %r changed kind from %r to %r"
103
283
                        % (file_id, old_kind, entry.kind))
104
284
 
105
285
        if entry.kind == 'directory':
106
286
            if not isdir(p):
107
 
                raise BzrError("%s is entered as directory but not a directory" % quotefn(p))
 
287
                raise BzrError("%s is entered as directory but not a directory"
 
288
                               % quotefn(p))
108
289
        elif entry.kind == 'file':
109
290
            if not isfile(p):
110
291
                raise BzrError("%s is entered as file but is not a file" % quotefn(p))
111
292
 
112
 
            content = file(p, 'rb').read()
113
 
 
114
 
            entry.text_sha1 = sha_string(content)
115
 
            entry.text_size = len(content)
116
 
 
117
 
            old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
 
293
            new_sha1 = work_tree.get_file_sha1(file_id)
 
294
 
118
295
            if (old_ie
119
 
                and (old_ie.text_size == entry.text_size)
120
 
                and (old_ie.text_sha1 == entry.text_sha1)):
 
296
                and old_ie.text_sha1 == new_sha1):
121
297
                ## assert content == basis.get_file(file_id).read()
122
 
                entry.text_id = basis_inv[file_id].text_id
 
298
                entry.text_id = old_ie.text_id
 
299
                entry.text_sha1 = new_sha1
 
300
                entry.text_size = old_ie.text_size
123
301
                mutter('    unchanged from previous text_id {%s}' %
124
302
                       entry.text_id)
125
 
 
126
303
            else:
 
304
                content = file(p, 'rb').read()
 
305
 
 
306
                # calculate the sha again, just in case the file contents
 
307
                # changed since we updated the cache
 
308
                entry.text_sha1 = sha_string(content)
 
309
                entry.text_size = len(content)
 
310
 
127
311
                entry.text_id = gen_file_id(entry.name)
128
312
                branch.text_store.add(content, entry.text_id)
129
313
                mutter('    stored with text_id {%s}' % entry.text_id)
130
 
                if verbose:
131
 
                    if not old_ie:
132
 
                        state = 'A'
133
 
                    elif (old_ie.name == entry.name
134
 
                          and old_ie.parent_id == entry.parent_id):
135
 
                        state = 'M'
136
 
                    else:
137
 
                        state = 'R'
138
 
 
139
 
                    show_status(state, entry.kind, quotefn(path))
140
 
 
141
 
    for file_id in missing_ids:
142
 
        # have to do this later so we don't mess up the iterator.
143
 
        # since parents may be removed before their children we
144
 
        # have to test.
145
 
 
146
 
        # FIXME: There's probably a better way to do this; perhaps
147
 
        # the workingtree should know how to filter itbranch.
148
 
        if work_inv.has_id(file_id):
149
 
            del work_inv[file_id]
150
 
 
151
 
 
152
 
    inv_id = rev_id = _gen_revision_id(time.time())
153
 
 
154
 
    inv_tmp = tempfile.TemporaryFile()
155
 
    inv.write_xml(inv_tmp)
156
 
    inv_tmp.seek(0)
157
 
    branch.inventory_store.add(inv_tmp, inv_id)
158
 
    mutter('new inventory_id is {%s}' % inv_id)
159
 
 
160
 
    branch._write_inventory(work_inv)
161
 
 
162
 
    if timestamp == None:
163
 
        timestamp = time.time()
164
 
 
165
 
    if committer == None:
166
 
        committer = username()
167
 
 
168
 
    if timezone == None:
169
 
        timezone = local_time_offset()
170
 
 
171
 
    mutter("building commit log message")
172
 
    rev = Revision(timestamp=timestamp,
173
 
                   timezone=timezone,
174
 
                   committer=committer,
175
 
                   precursor = branch.last_patch(),
176
 
                   message = message,
177
 
                   inventory_id=inv_id,
178
 
                   revision_id=rev_id)
179
 
 
180
 
    rev_tmp = tempfile.TemporaryFile()
181
 
    rev.write_xml(rev_tmp)
182
 
    rev_tmp.seek(0)
183
 
    branch.revision_store.add(rev_tmp, rev_id)
184
 
    mutter("new revision_id is {%s}" % rev_id)
185
 
 
186
 
    ## XXX: Everything up to here can simply be orphaned if we abort
187
 
    ## the commit; it will leave junk files behind but that doesn't
188
 
    ## matter.
189
 
 
190
 
    ## TODO: Read back the just-generated changeset, and make sure it
191
 
    ## applies and recreates the right state.
192
 
 
193
 
    ## TODO: Also calculate and store the inventory SHA1
194
 
    mutter("committing patch r%d" % (branch.revno() + 1))
195
 
 
196
 
 
197
 
    branch.append_revision(rev_id)
198
 
 
199
 
    if verbose:
200
 
        note("commited r%d" % branch.revno())
201
 
 
202
 
 
203
 
 
204
 
def _gen_revision_id(when):
205
 
    """Return new revision-id."""
206
 
    from binascii import hexlify
207
 
    from osutils import rand_bytes, compact_date, user_email
208
 
 
209
 
    s = '%s-%s-' % (user_email(), compact_date(when))
210
 
    s += hexlify(rand_bytes(8))
211
 
    return s
 
314
 
 
315
        if verbose:
 
316
            marked = path + kind_marker(entry.kind)
 
317
            if not old_ie:
 
318
                print 'added', marked
 
319
                any_changes = True
 
320
            elif old_ie == entry:
 
321
                pass                    # unchanged
 
322
            elif (old_ie.name == entry.name
 
323
                  and old_ie.parent_id == entry.parent_id):
 
324
                print 'modified', marked
 
325
                any_changes = True
 
326
            else:
 
327
                print 'renamed', marked
 
328
                any_changes = True
 
329
        elif old_ie != entry:
 
330
            any_changes = True
 
331
 
 
332
    return missing_ids, inv, any_changes
212
333
 
213
334