35
40
be robust against files disappearing, moving, etc. So the
36
41
whole thing is a bit hard.
43
This raises PointlessCommit if there are no changes, no new merges,
44
and allow_pointless is false.
38
46
timestamp -- if not None, seconds-since-epoch for a
39
47
postdated/predated commit.
42
import os, time, tempfile
44
from inventory import Inventory
45
from osutils import isdir, isfile, sha_string, quotefn, \
46
local_time_offset, username
50
If true, commit only those files.
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.
60
import time, tempfile, re
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
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
80
work_tree = branch.working_tree()
81
work_inv = work_tree.inventory
82
basis = branch.basis_tree()
83
basis_inv = basis.inventory
86
# note('looking for changes...')
87
# print 'looking for changes...'
88
# disabled; should be done at a higher level
91
pending_merges = branch.pending_merges()
93
missing_ids, new_inv, any_changes = \
94
_gather_commit(branch,
101
if not (any_changes or allow_pointless or pending_merges):
102
raise PointlessCommit()
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.
110
# have to do this later so we don't mess up the iterator.
111
# since parents may be removed before their children we
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]
120
rev_id = _gen_revision_id(branch, time.time())
123
inv_tmp = tempfile.TemporaryFile()
125
serializer_v4.write_inventory(new_inv, inv_tmp)
127
branch.inventory_store.add(inv_tmp, inv_id)
128
mutter('new inventory_id is {%s}' % inv_id)
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)
135
branch._write_inventory(work_inv)
137
if timestamp == None:
138
timestamp = time.time()
140
if committer == None:
141
committer = username(branch)
144
timezone = local_time_offset()
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]'
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(
159
lambda match: match.group(0).encode('unicode_escape'),
162
note("replaced %d control characters in message", escape_count)
163
rev = Revision(timestamp=timestamp,
168
inventory_sha1=inv_sha1,
172
precursor_id = branch.last_patch()
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))
179
rev_tmp = tempfile.TemporaryFile()
180
serializer_v4.write_revision(rev, rev_tmp)
182
branch.revision_store.add(rev_tmp, rev_id)
183
mutter("new revision_id is {%s}" % rev_id)
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
189
## TODO: Read back the just-generated changeset, and make sure it
190
## applies and recreates the right state.
192
## TODO: Also calculate and store the inventory SHA1
193
mutter("committing patch r%d" % (branch.revno() + 1))
195
branch.append_revision(rev_id)
197
branch.set_pending_merges([])
200
# disabled; should go through logging
201
# note("commited r%d" % branch.revno())
202
# print ("commited r%d" % branch.revno())
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
214
s = '%s-%s-' % (user_email(branch), compact_date(when))
215
s += hexlify(rand_bytes(8))
219
def _gather_commit(branch, work_tree, work_inv, basis_inv, specific_files,
221
"""Build inventory preparatory to commit.
223
Returns missing_ids, new_inv, any_changes.
225
This adds any changed files into the text store, and sets their
226
test-id, sha and size in the returned inventory appropriately.
229
Modified to hold a list of files that have been deleted from
230
the working directory; these should be removed from the
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
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
54
branch._need_writelock()
56
## TODO: Show branch names
58
# TODO: Don't commit if there are no changes, unless forced?
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
68
work_inv = branch.read_working_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
243
inv = Inventory(work_inv.root.file_id)
73
246
for path, entry in work_inv.iter_entries():
74
## TODO: Cope with files that have gone missing.
76
247
## TODO: Check that the file kind has not changed from the previous
77
248
## revision of this file (if any).
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))
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())
260
# omit this from committed inventory
264
if not work_tree.has_id(file_id):
266
print('deleted %s%s' % (path, kind_marker(entry.kind)))
86
268
mutter(" file is missing, removing from inventory")
88
show_status('D', entry.kind, quotefn(path))
89
269
missing_ids.append(file_id)
92
# TODO: Handle files that have been deleted
94
# TODO: Maybe a special case for empty files? Seems a
95
# waste to store them many times.
272
# this is present in the new inventory; may be new, modified or
274
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
99
if basis_inv.has_id(file_id):
100
old_kind = basis_inv[file_id].kind
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))
105
285
if entry.kind == 'directory':
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"
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))
112
content = file(p, 'rb').read()
114
entry.text_sha1 = sha_string(content)
115
entry.text_size = len(content)
117
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
293
new_sha1 = work_tree.get_file_sha1(file_id)
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}' %
304
content = file(p, 'rb').read()
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)
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)
133
elif (old_ie.name == entry.name
134
and old_ie.parent_id == entry.parent_id):
139
show_status(state, entry.kind, quotefn(path))
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
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]
152
inv_id = rev_id = _gen_revision_id(time.time())
154
inv_tmp = tempfile.TemporaryFile()
155
inv.write_xml(inv_tmp)
157
branch.inventory_store.add(inv_tmp, inv_id)
158
mutter('new inventory_id is {%s}' % inv_id)
160
branch._write_inventory(work_inv)
162
if timestamp == None:
163
timestamp = time.time()
165
if committer == None:
166
committer = username()
169
timezone = local_time_offset()
171
mutter("building commit log message")
172
rev = Revision(timestamp=timestamp,
175
precursor = branch.last_patch(),
180
rev_tmp = tempfile.TemporaryFile()
181
rev.write_xml(rev_tmp)
183
branch.revision_store.add(rev_tmp, rev_id)
184
mutter("new revision_id is {%s}" % rev_id)
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
190
## TODO: Read back the just-generated changeset, and make sure it
191
## applies and recreates the right state.
193
## TODO: Also calculate and store the inventory SHA1
194
mutter("committing patch r%d" % (branch.revno() + 1))
197
branch.append_revision(rev_id)
200
note("commited r%d" % branch.revno())
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
209
s = '%s-%s-' % (user_email(), compact_date(when))
210
s += hexlify(rand_bytes(8))
316
marked = path + kind_marker(entry.kind)
318
print 'added', marked
320
elif old_ie == entry:
322
elif (old_ie.name == entry.name
323
and old_ie.parent_id == entry.parent_id):
324
print 'modified', marked
327
print 'renamed', marked
329
elif old_ie != entry:
332
return missing_ids, inv, any_changes