1
# Copyright (C) 2005 Canonical Ltd
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.
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.
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
19
def commit(branch, message, timestamp=None, timezone=None,
24
"""Commit working copy as a new revision.
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.
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.
40
timestamp -- if not None, seconds-since-epoch for a
41
postdated/predated commit.
44
If true, commit only those files.
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.
54
import os, time, tempfile
56
from inventory import Inventory
57
from osutils import isdir, isfile, sha_string, quotefn, \
58
local_time_offset, username, kind_marker, is_inside_any
60
from branch import gen_file_id
61
from errors import BzrError
62
from revision import Revision
63
from trace import mutter, note
65
branch._need_writelock()
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
75
work_tree = branch.working_tree()
76
work_inv = work_tree.inventory
78
basis = branch.basis_tree()
79
basis_inv = basis.inventory
83
note('looking for changes...')
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).
91
p = branch.abspath(path)
92
file_id = entry.file_id
93
mutter('commit prep file %s, id %r ' % (p, file_id))
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())
100
# omit this from committed inventory
104
if not work_tree.has_id(file_id):
106
print('deleted %s%s' % (path, kind_marker(entry.kind)))
107
mutter(" file is missing, removing from inventory")
108
missing_ids.append(file_id)
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))
119
if entry.kind == 'directory':
121
raise BzrError("%s is entered as directory but not a directory"
123
elif entry.kind == 'file':
125
raise BzrError("%s is entered as file but is not a file" % quotefn(p))
127
new_sha1 = work_tree.get_file_sha1(file_id)
129
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
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}' %
139
content = file(p, 'rb').read()
141
entry.text_sha1 = sha_string(content)
142
entry.text_size = len(content)
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)
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)
154
print('renamed %s' % path)
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.
163
# have to do this later so we don't mess up the iterator.
164
# since parents may be removed before their children we
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]
174
rev_id = _gen_revision_id(time.time())
177
inv_tmp = tempfile.TemporaryFile()
178
inv.write_xml(inv_tmp)
180
branch.inventory_store.add(inv_tmp, inv_id)
181
mutter('new inventory_id is {%s}' % inv_id)
183
branch._write_inventory(work_inv)
185
if timestamp == None:
186
timestamp = time.time()
188
if committer == None:
189
committer = username()
192
timezone = local_time_offset()
194
mutter("building commit log message")
195
rev = Revision(timestamp=timestamp,
198
precursor = branch.last_patch(),
203
rev_tmp = tempfile.TemporaryFile()
204
rev.write_xml(rev_tmp)
206
branch.revision_store.add(rev_tmp, rev_id)
207
mutter("new revision_id is {%s}" % rev_id)
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
213
## TODO: Read back the just-generated changeset, and make sure it
214
## applies and recreates the right state.
216
## TODO: Also calculate and store the inventory SHA1
217
mutter("committing patch r%d" % (branch.revno() + 1))
219
branch.append_revision(rev_id)
222
note("commited r%d" % branch.revno())
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
231
s = '%s-%s-' % (user_email(), compact_date(when))
232
s += hexlify(rand_bytes(8))