24
from StringIO import StringIO
24
from cStringIO import StringIO
26
from bzrlib.errors import NoWorkingTree
27
from bzrlib.osutils import local_time_offset, format_date
28
import bzrlib.revision
27
29
from bzrlib.rio import RioReader, RioWriter, Stanza
28
from bzrlib.osutils import local_time_offset, format_date
31
def get_file_revisions(branch, check=False):
32
"""Get the last changed revision for all files.
34
:param branch: The branch we are checking.
35
:param check: See if there are uncommitted changes.
36
:return: ({file_path => last changed revision}, Tree_is_clean)
40
basis_tree = branch.basis_tree()
41
for path, ie in basis_tree.inventory.iter_entries():
42
file_revisions[path] = ie.revision
45
# Without checking, the tree looks clean
46
return file_revisions, clean
48
new_tree = branch.working_tree()
50
# Without a working tree, everything is clean
51
return file_revisions, clean
53
from bzrlib.diff import compare_trees
54
delta = compare_trees(basis_tree, new_tree, want_unchanged=False)
56
# Using a 2-pass algorithm for renames. This is because you might have
57
# renamed something out of the way, and then created a new file
58
# in which case we would rather see the new marker
59
# Or you might have removed the target, and then renamed
60
# in which case we would rather see the renamed marker
61
for old_path, new_path, file_id, kind, text_mod, meta_mod in delta.renamed:
63
file_revisions[old_path] = u'renamed to %s' % (new_path,)
64
for path, file_id, kind in delta.removed:
66
file_revisions[path] = 'removed'
67
for path, file_id, kind in delta.added:
69
file_revisions[path] = 'new'
70
for old_path, new_path, file_id, kind, text_mod, meta_mod in delta.renamed:
72
file_revisions[new_path] = u'renamed from %s' % (old_path,)
73
for path, file_id, kind, text_mod, meta_mod in delta.modified:
75
file_revisions[path] = 'modified'
77
for info in new_tree.list_files():
78
path, status = info[0:2]
80
file_revisions[path] = 'unversioned'
83
return file_revisions, clean
86
32
# This contains a map of format id => formatter
103
49
timezone='original', show_offset=True)
106
def generate_rio_version(branch, to_file,
107
check_for_clean=False,
108
include_revision_history=False,
109
include_file_revisions=False):
110
"""Create the version file for this project.
112
:param branch: The branch to write information about
113
:param to_file: The file to write the information
114
:param check_for_clean: If true, check if the branch is clean.
115
This can be expensive for large trees. This is also only
116
valid for branches with working trees.
117
:param include_revision_history: Write out the list of revisions, and
118
the commit message associated with each
119
:param include_file_revisions: Write out the set of last changed revision
123
info.add('build-date', create_date_str())
124
info.add('revno', str(branch.revno()))
126
# XXX: Compatibility pre/post storage
127
repo = getattr(branch, 'repository', branch)
129
last_rev_id = branch.last_revision()
130
if last_rev_id is not None:
131
info.add('revision-id', last_rev_id)
132
rev = repo.get_revision(last_rev_id)
133
info.add('date', create_date_str(rev.timestamp, rev.timezone))
135
if branch.nick is not None:
136
info.add('branch-nick', branch.nick)
140
if check_for_clean or include_file_revisions:
141
file_revisions, clean = get_file_revisions(branch, check=check_for_clean)
145
info.add('clean', 'True')
147
info.add('clean', 'False')
149
if include_revision_history:
150
revs = branch.revision_history()
153
rev = repo.get_revision(rev_id)
154
log.add('id', rev_id)
155
log.add('message', rev.message)
156
log.add('date', create_date_str(rev.timestamp, rev.timezone))
158
log_writer = RioWriter(to_file=sio)
159
log_writer.write_stanza(log)
160
info.add('revisions', sio.getvalue())
162
if include_file_revisions:
164
for path in sorted(file_revisions.keys()):
165
files.add('path', path)
166
files.add('revision', file_revisions[path])
168
file_writer = RioWriter(to_file=sio)
169
file_writer.write_stanza(files)
170
info.add('file-revisions', sio.getvalue())
172
writer = RioWriter(to_file=to_file)
173
writer.write_stanza(info)
176
version_formats['rio'] = generate_rio_version
52
class VersionInfoBuilder(object):
53
"""A class which lets you build up information about a revision."""
55
def __init__(self, branch, working_tree=None,
56
check_for_clean=False,
57
include_revision_history=False,
58
include_file_revisions=False,
60
"""Build up information about the given branch.
61
If working_tree is given, it can be checked for changes.
63
:param branch: The branch to work on
64
:param working_tree: If supplied, preferentially check
65
the working tree for changes.
66
:param check_for_clean: If False, we will skip the expense
67
of looking for changes.
68
:param include_revision_history: If True, the output
69
will include the full mainline revision history, including
71
:param include_file_revisions: The output should
72
include the explicit last-changed revision for each file.
75
self._working_tree = working_tree
76
self._check = check_for_clean
77
self._include_history = include_revision_history
78
self._include_file_revs = include_file_revisions
81
self._file_revisions = {}
82
self._revision_history_info= []
84
def _extract_file_revisions(self):
85
"""Extract the working revisions for all files"""
87
# Things seem clean if we never look :)
90
if self._working_tree is not None:
91
basis_tree = self._working_tree.basis_tree()
93
basis_tree = self._branch.basis_tree()
95
# Build up the list from the basis inventory
96
for info in basis_tree.list_files():
97
self._file_revisions[info[0]] = info[-1].revision
99
if not self._check or self._working_tree is None:
102
# We have both a working tree, and we are checking
103
delta = bzrlib.delta.compare_trees(basis_tree, self._working_tree,
104
want_unchanged=False)
106
# Using a 2-pass algorithm for renames. This is because you might have
107
# renamed something out of the way, and then created a new file
108
# in which case we would rather see the new marker
109
# Or you might have removed the target, and then renamed
110
# in which case we would rather see the renamed marker
111
for (old_path, new_path, file_id,
112
kind, text_mod, meta_mod) in delta.renamed:
114
self._file_revisions[old_path] = u'renamed to %s' % (new_path,)
115
for path, file_id, kind in delta.removed:
117
self._file_revisions[path] = 'removed'
118
for path, file_id, kind in delta.added:
120
self._file_revisions[path] = 'new'
121
for (old_path, new_path, file_id,
122
kind, text_mod, meta_mod) in delta.renamed:
124
self._file_revisions[new_path] = u'renamed from %s' % (old_path,)
125
for path, file_id, kind, text_mod, meta_mod in delta.modified:
127
self._file_revisions[path] = 'modified'
129
for path in self._working_tree.unknowns():
131
self._file_revisions[path] = 'unversioned'
133
def _extract_revision_history(self):
134
"""Find the messages for all revisions in history."""
136
# Unfortunately, there is no WorkingTree.revision_history
137
rev_hist = self._branch.revision_history()
138
if self._working_tree is not None:
139
last_rev = self._working_tree.last_revision()
140
assert last_rev in rev_hist, \
141
"Working Tree's last revision not in branch.revision_history"
142
rev_hist = rev_hist[:rev_hist.index(last_rev)+1]
144
repository = self._branch.repository
145
repository.lock_read()
147
for revision_id in rev_hist:
148
rev = repository.get_revision(revision_id)
149
self._revision_history_info.append(
150
(rev.revision_id, rev.message,
151
rev.timestamp, rev.timezone))
155
def _get_revision_id(self):
156
"""Get the revision id we are working on."""
157
if self._working_tree is not None:
158
return self._working_tree.last_revision()
159
return self._branch.last_revision()
161
def generate(self, to_file):
162
"""Output the version information to the supplied file.
164
:param to_file: The file to write the stream to. The output
165
will already be encoded, so to_file should not try
169
raise NotImplementedError(VersionInfoBuilder.generate)
172
class RioVersionInfoBuilder(VersionInfoBuilder):
173
"""This writes a rio stream out."""
175
def generate(self, to_file):
177
revision_id = self._get_revision_id()
178
if revision_id is not None:
179
info.add('revision-id', revision_id)
180
rev = self._branch.repository.get_revision(revision_id)
181
info.add('date', create_date_str(rev.timestamp, rev.timezone))
182
revno = str(self._branch.revision_id_to_revno(revision_id))
186
info.add('build-date', create_date_str())
187
info.add('revno', revno)
189
if self._branch.nick is not None:
190
info.add('branch-nick', self._branch.nick)
192
if self._check or self._include_file_revs:
193
self._extract_file_revisions()
197
info.add('clean', 'True')
199
info.add('clean', 'False')
201
if self._include_history:
202
self._extract_revision_history()
204
for (revision_id, message,
205
timestamp, timezone) in self._revision_history_info:
206
log.add('id', revision_id)
207
log.add('message', message)
208
log.add('date', create_date_str(timestamp, timezone))
210
log_writer = RioWriter(to_file=sio)
211
log_writer.write_stanza(log)
212
info.add('revisions', sio.getvalue())
214
if self._include_file_revs:
216
for path in sorted(self._file_revisions.keys()):
217
files.add('path', path)
218
files.add('revision', self._file_revisions[path])
220
file_writer = RioWriter(to_file=sio)
221
file_writer.write_stanza(files)
222
info.add('file-revisions', sio.getvalue())
224
writer = RioWriter(to_file=to_file)
225
writer.write_stanza(info)
228
version_formats['rio'] = RioVersionInfoBuilder
177
229
# Default format is rio
178
version_formats[None] = generate_rio_version
230
version_formats[None] = RioVersionInfoBuilder
181
233
# Header and footer for the python format
182
234
_py_version_header = '''#!/usr/bin/env python
184
This file is automatically generated by generate_version_info
235
"""This file is automatically generated by generate_version_info
185
236
It uses the current working tree to determine the revision.
186
237
So don't edit it. :)
201
def generate_python_version(branch, to_file,
202
check_for_clean=False,
203
include_revision_history=False,
204
include_file_revisions=False):
205
"""Create a python version file for this project.
207
:param branch: The branch to write information about
208
:param to_file: The file to write the information
209
:param check_for_clean: If true, check if the branch is clean.
210
This can be expensive for large trees. This is also only
211
valid for branches with working trees.
212
:param include_revision_history: Write out the list of revisions, and
213
the commit message associated with each
214
:param include_file_revisions: Write out the set of last changed revision
217
# TODO: jam 20051228 The python output doesn't actually need to be
218
# encoded, because it should only generate ascii safe output.
219
info = {'build_date':create_date_str()
220
, 'revno':branch.revno()
222
, 'branch_nick':branch.nick
228
# XXX: Compatibility pre/post storage
229
repo = getattr(branch, 'repository', branch)
231
last_rev_id = branch.last_revision()
233
rev = repo.get_revision(last_rev_id)
234
info['revision_id'] = last_rev_id
235
info['date'] = create_date_str(rev.timestamp, rev.timezone)
239
if check_for_clean or include_file_revisions:
240
file_revisions, clean = get_file_revisions(branch, check=check_for_clean)
246
info['clean'] = False
248
info_str = pprint.pformat(info)
249
to_file.write(_py_version_header)
250
to_file.write('version_info = ')
251
to_file.write(info_str)
252
to_file.write('\n\n')
254
if include_revision_history:
255
revs = branch.revision_history()
257
rev = repo.get_revision(rev_id)
258
revisions.append((rev_id, rev.message, rev.timestamp, rev.timezone))
259
revision_str = pprint.pformat(revisions)
260
to_file.write('revisions = ')
261
to_file.write(revision_str)
262
to_file.write('\n\n')
264
to_file.write('revisions = {}\n\n')
266
if include_file_revisions:
267
file_rev_str = pprint.pformat(file_revisions)
268
to_file.write('file_revisions = ')
269
to_file.write(file_rev_str)
270
to_file.write('\n\n')
272
to_file.write('file_revisions = {}\n\n')
274
to_file.write(_py_version_footer)
277
version_formats['python'] = generate_python_version
252
class PythonVersionInfoBuilder(VersionInfoBuilder):
253
"""Create a version file which is a python source module."""
255
def generate(self, to_file):
256
info = {'build_date':create_date_str()
259
, 'branch_nick':self._branch.nick
265
revision_id = self._get_revision_id()
266
if revision_id is None:
269
info['revno'] = self._branch.revision_id_to_revno(revision_id)
270
info['revision_id'] = revision_id
271
rev = self._branch.repository.get_revision(revision_id)
272
info['date'] = create_date_str(rev.timestamp, rev.timezone)
274
if self._check or self._include_file_revs:
275
self._extract_file_revisions()
281
info['clean'] = False
283
info_str = pprint.pformat(info)
284
to_file.write(_py_version_header)
285
to_file.write('version_info = ')
286
to_file.write(info_str)
287
to_file.write('\n\n')
289
if self._include_history:
290
self._extract_revision_history()
291
revision_str = pprint.pformat(self._revision_history_info)
292
to_file.write('revisions = ')
293
to_file.write(revision_str)
294
to_file.write('\n\n')
296
to_file.write('revisions = {}\n\n')
298
if self._include_file_revs:
299
file_rev_str = pprint.pformat(self._file_revisions)
300
to_file.write('file_revisions = ')
301
to_file.write(file_rev_str)
302
to_file.write('\n\n')
304
to_file.write('file_revisions = {}\n\n')
306
to_file.write(_py_version_footer)
309
version_formats['python'] = PythonVersionInfoBuilder