~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 03:38:47 UTC
  • Revision ID: mbp@sourcefrog.net-20050516033847-01780e5b2a08311e
todo

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
 
 
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
    rev_id
 
47
        If set, use this as the new revision id.
 
48
        Useful for test or import commands that need to tightly
 
49
        control what revisions are assigned.  If you duplicate
 
50
        a revision id that exists elsewhere it is your own fault.
 
51
        If null (default), a time/random revision id is generated.
 
52
    """
 
53
 
 
54
    import os, time, tempfile
 
55
 
 
56
    from inventory import Inventory
 
57
    from osutils import isdir, isfile, sha_string, quotefn, \
 
58
         local_time_offset, username, kind_marker, is_inside_any
 
59
    
 
60
    from branch import gen_file_id
 
61
    from errors import BzrError
 
62
    from revision import Revision
 
63
    from trace import mutter, note
 
64
 
 
65
    branch._need_writelock()
 
66
 
 
67
    # First walk over the working inventory; and both update that
 
68
    # and also build a new revision inventory.  The revision
 
69
    # inventory needs to hold the text-id, sha1 and size of the
 
70
    # actual file versions committed in the revision.  (These are
 
71
    # not present in the working inventory.)  We also need to
 
72
    # detect missing/deleted files, and remove them from the
 
73
    # working inventory.
 
74
 
 
75
    work_tree = branch.working_tree()
 
76
    work_inv = work_tree.inventory
 
77
    inv = Inventory()
 
78
    basis = branch.basis_tree()
 
79
    basis_inv = basis.inventory
 
80
    missing_ids = []
 
81
 
 
82
    if verbose:
 
83
        note('looking for changes...')
 
84
        
 
85
    for path, entry in work_inv.iter_entries():
 
86
        ## TODO: Check that the file kind has not changed from the previous
 
87
        ## revision of this file (if any).
 
88
 
 
89
        entry = entry.copy()
 
90
 
 
91
        p = branch.abspath(path)
 
92
        file_id = entry.file_id
 
93
        mutter('commit prep file %s, id %r ' % (p, file_id))
 
94
 
 
95
        if specific_files and not is_inside_any(specific_files, path):
 
96
            if basis_inv.has_id(file_id):
 
97
                # carry over with previous state
 
98
                inv.add(basis_inv[file_id].copy())
 
99
            else:
 
100
                # omit this from committed inventory
 
101
                pass
 
102
            continue
 
103
 
 
104
        if not work_tree.has_id(file_id):
 
105
            if verbose:
 
106
                print('deleted %s%s' % (path, kind_marker(entry.kind)))
 
107
            mutter("    file is missing, removing from inventory")
 
108
            missing_ids.append(file_id)
 
109
            continue
 
110
 
 
111
        inv.add(entry)
 
112
 
 
113
        if basis_inv.has_id(file_id):
 
114
            old_kind = basis_inv[file_id].kind
 
115
            if old_kind != entry.kind:
 
116
                raise BzrError("entry %r changed kind from %r to %r"
 
117
                        % (file_id, old_kind, entry.kind))
 
118
 
 
119
        if entry.kind == 'directory':
 
120
            if not isdir(p):
 
121
                raise BzrError("%s is entered as directory but not a directory"
 
122
                               % quotefn(p))
 
123
        elif entry.kind == 'file':
 
124
            if not isfile(p):
 
125
                raise BzrError("%s is entered as file but is not a file" % quotefn(p))
 
126
 
 
127
            new_sha1 = work_tree.get_file_sha1(file_id)
 
128
 
 
129
            old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
 
130
            if (old_ie
 
131
                and old_ie.text_sha1 == new_sha1):
 
132
                ## assert content == basis.get_file(file_id).read()
 
133
                entry.text_id = old_ie.text_id
 
134
                entry.text_sha1 = new_sha1
 
135
                entry.text_size = old_ie.text_size
 
136
                mutter('    unchanged from previous text_id {%s}' %
 
137
                       entry.text_id)
 
138
            else:
 
139
                content = file(p, 'rb').read()
 
140
 
 
141
                entry.text_sha1 = sha_string(content)
 
142
                entry.text_size = len(content)
 
143
 
 
144
                entry.text_id = gen_file_id(entry.name)
 
145
                branch.text_store.add(content, entry.text_id)
 
146
                mutter('    stored with text_id {%s}' % entry.text_id)
 
147
                if verbose:
 
148
                    if not old_ie:
 
149
                        print('added %s' % path)
 
150
                    elif (old_ie.name == entry.name
 
151
                          and old_ie.parent_id == entry.parent_id):
 
152
                        print('modified %s' % path)
 
153
                    else:
 
154
                        print('renamed %s' % path)
 
155
 
 
156
 
 
157
    for file_id in missing_ids:
 
158
        # Any files that have been deleted are now removed from the
 
159
        # working inventory.  Files that were not selected for commit
 
160
        # are left as they were in the working inventory and ommitted
 
161
        # from the revision inventory.
 
162
        
 
163
        # have to do this later so we don't mess up the iterator.
 
164
        # since parents may be removed before their children we
 
165
        # have to test.
 
166
 
 
167
        # FIXME: There's probably a better way to do this; perhaps
 
168
        # the workingtree should know how to filter itbranch.
 
169
        if work_inv.has_id(file_id):
 
170
            del work_inv[file_id]
 
171
 
 
172
 
 
173
    if rev_id is None:
 
174
        rev_id = _gen_revision_id(time.time())
 
175
    inv_id = rev_id
 
176
 
 
177
    inv_tmp = tempfile.TemporaryFile()
 
178
    inv.write_xml(inv_tmp)
 
179
    inv_tmp.seek(0)
 
180
    branch.inventory_store.add(inv_tmp, inv_id)
 
181
    mutter('new inventory_id is {%s}' % inv_id)
 
182
 
 
183
    branch._write_inventory(work_inv)
 
184
 
 
185
    if timestamp == None:
 
186
        timestamp = time.time()
 
187
 
 
188
    if committer == None:
 
189
        committer = username()
 
190
 
 
191
    if timezone == None:
 
192
        timezone = local_time_offset()
 
193
 
 
194
    mutter("building commit log message")
 
195
    rev = Revision(timestamp=timestamp,
 
196
                   timezone=timezone,
 
197
                   committer=committer,
 
198
                   precursor = branch.last_patch(),
 
199
                   message = message,
 
200
                   inventory_id=inv_id,
 
201
                   revision_id=rev_id)
 
202
 
 
203
    rev_tmp = tempfile.TemporaryFile()
 
204
    rev.write_xml(rev_tmp)
 
205
    rev_tmp.seek(0)
 
206
    branch.revision_store.add(rev_tmp, rev_id)
 
207
    mutter("new revision_id is {%s}" % rev_id)
 
208
 
 
209
    ## XXX: Everything up to here can simply be orphaned if we abort
 
210
    ## the commit; it will leave junk files behind but that doesn't
 
211
    ## matter.
 
212
 
 
213
    ## TODO: Read back the just-generated changeset, and make sure it
 
214
    ## applies and recreates the right state.
 
215
 
 
216
    ## TODO: Also calculate and store the inventory SHA1
 
217
    mutter("committing patch r%d" % (branch.revno() + 1))
 
218
 
 
219
    branch.append_revision(rev_id)
 
220
 
 
221
    if verbose:
 
222
        note("commited r%d" % branch.revno())
 
223
 
 
224
 
 
225
 
 
226
def _gen_revision_id(when):
 
227
    """Return new revision-id."""
 
228
    from binascii import hexlify
 
229
    from osutils import rand_bytes, compact_date, user_email
 
230
 
 
231
    s = '%s-%s-' % (user_email(), compact_date(when))
 
232
    s += hexlify(rand_bytes(8))
 
233
    return s
 
234
 
 
235