69
69
# TODO: Perhaps these should just be different formats in which inventories/
70
70
# revisions can be serialized.
72
from cStringIO import StringIO
73
74
from sha import sha
75
from bzrlib.osutils import contains_whitespace, contains_linebreaks
77
def contains_whitespace(s):
78
"""True if there are any whitespace characters in s."""
79
for ch in string.whitespace:
86
def contains_linebreaks(s):
87
"""True if there is any vertical whitespace in s."""
78
95
class Testament(object):
79
96
"""Reduced summary of a revision.
83
100
- produced from a revision
85
102
- loaded from a stream
86
103
- compared to a revision
89
long_header = 'bazaar-ng testament version 1\n'
90
short_header = 'bazaar-ng testament short form 1\n'
93
def from_revision(cls, repository, revision_id):
107
def from_revision(cls, branch, revision_id):
94
108
"""Produce a new testament from a historical revision"""
95
rev = repository.get_revision(revision_id)
96
inventory = repository.get_inventory(revision_id)
97
return cls(rev, inventory)
99
def __init__(self, rev, inventory):
100
"""Create a new testament for rev using inventory."""
101
self.revision_id = str(rev.revision_id)
102
self.committer = rev.committer
103
self.timezone = rev.timezone or 0
104
self.timestamp = rev.timestamp
105
self.message = rev.message
106
self.parent_ids = rev.parent_ids[:]
107
self.inventory = inventory
108
self.revprops = copy(rev.properties)
109
assert not contains_whitespace(self.revision_id)
110
assert not contains_linebreaks(self.committer)
110
rev = branch.get_revision(revision_id)
111
t.revision_id = str(revision_id)
112
t.committer = rev.committer
113
t.timezone = rev.timezone or 0
114
t.timestamp = rev.timestamp
115
t.message = rev.message
116
t.parent_ids = rev.parent_ids[:]
117
t.inventory = branch.get_inventory(revision_id)
118
assert not contains_whitespace(t.revision_id)
119
assert not contains_linebreaks(t.committer)
112
122
def as_text_lines(self):
113
123
"""Yield text form as a sequence of lines.
131
142
for l in self.message.splitlines():
133
144
a('inventory:\n')
134
for path, ie in self._get_entries():
145
for path, ie in self.inventory.iter_entries():
135
146
a(self._entry_to_line(path, ie))
136
r.extend(self._revprops_to_lines())
139
assert isinstance(l, basestring), \
149
assert isinstance(l, str), \
140
150
'%r of type %s is not a plain string' % (l, type(l))
141
return [line.encode('utf-8') for line in r]
143
def _get_entries(self):
144
entries = self.inventory.iter_entries()
148
153
def _escape_path(self, path):
149
154
assert not contains_linebreaks(path)
150
return unicode(path.replace('\\', '/').replace(' ', '\ '))
155
return unicode(path.replace('\\', '/').replace(' ', '\ ')).encode('utf-8')
152
157
def _entry_to_line(self, path, ie):
153
158
"""Turn an inventory entry into a testament line"""
159
l = ' ' + str(ie.kind)
160
l += ' ' + self._escape_path(path)
154
161
assert not contains_whitespace(ie.file_id)
162
l += ' ' + unicode(ie.file_id).encode('utf-8')
158
163
if ie.kind == 'file':
159
164
# TODO: avoid switching on kind
160
165
assert ie.text_sha1
161
content = ie.text_sha1
166
l += ' ' + ie.text_sha1
163
167
elif ie.kind == 'symlink':
164
168
assert ie.symlink_target
165
content = self._escape_path(ie.symlink_target)
168
l = u' %s %s %s%s%s\n' % (ie.kind, self._escape_path(path),
170
content_spacer, content)
169
l += ' ' + self._escape_path(ie.symlink_target)
173
173
def as_text(self):
176
176
def as_short_text(self):
177
177
"""Return short digest-based testament."""
178
return (self.short_header +
179
map(s.update, self.as_text_lines())
180
return ('bazaar-ng testament short form 1\n'
179
181
'revision-id: %s\n'
181
% (self.revision_id, self.as_sha1()))
183
def _revprops_to_lines(self):
184
"""Pack up revision properties."""
185
if not self.revprops:
187
r = ['properties:\n']
188
for name, value in sorted(self.revprops.items()):
189
assert isinstance(name, str)
190
assert not contains_whitespace(name)
191
r.append(' %s:\n' % name)
192
for line in value.splitlines():
193
r.append(u' %s\n' % line)
198
map(s.update, self.as_text_lines())
202
class StrictTestament(Testament):
203
"""This testament format is for use as a checksum in bundle format 0.8"""
205
long_header = 'bazaar-ng testament version 2.1\n'
206
short_header = 'bazaar-ng testament short form 2.1\n'
207
def _entry_to_line(self, path, ie):
208
l = Testament._entry_to_line(self, path, ie)[:-1]
209
l += ' ' + ie.revision
210
l += {True: ' yes\n', False: ' no\n'}[ie.executable]
214
class StrictTestament3(StrictTestament):
215
"""This testament format is for use as a checksum in bundle format 0.9+
217
It differs from StrictTestament by including data about the tree root.
220
long_header = 'bazaar testament version 3 strict\n'
221
short_header = 'bazaar testament short form 3 strict\n'
222
def _get_entries(self):
223
return self.inventory.iter_entries()
225
def _escape_path(self, path):
226
assert not contains_linebreaks(path)
229
return unicode(path.replace('\\', '/').replace(' ', '\ '))
183
% (self.revision_id, s.hexdigest()))