103
98
# is the first parent of the last revision listed
104
99
rev = self.revisions[-1]
105
100
self.base = rev.parents[0].revision_id
101
# In general, if self.base is None, self.base_sha1 should
103
if self.base_sha1 is not None:
104
assert self.base_sha1 == rev.parent[0].revision_sha1
106
105
self.base_sha1 = rev.parents[0].revision_sha1
107
if not self.timestamp and self.date:
108
self.timestamp, self.timezone = common.unpack_highres_date(self.date)
108
110
for rev in self.revisions:
111
def create_maps(self):
112
"""Go through the individual id sections, and generate the
113
id2path and path2id maps.
115
# Rather than use an empty path, the changeset code seems
116
# to like to use "./." for the tree root.
117
self.id2path[self.tree_root_id] = './.'
118
self.path2id['./.'] = self.tree_root_id
119
self.id2parent[self.tree_root_id] = bzrlib.changeset.NULL_ID
120
self.old_id2path = self.id2path.copy()
121
self.old_path2id = self.path2id.copy()
122
self.old_id2parent = self.id2parent.copy()
125
for info in self.file_ids:
126
path, f_id, parent_id = info.split('\t')
127
self.id2path[f_id] = path
128
self.path2id[path] = f_id
129
self.id2parent[f_id] = parent_id
130
if self.old_file_ids:
131
for info in self.old_file_ids:
132
path, f_id, parent_id = info.split('\t')
133
self.old_id2path[f_id] = path
134
self.old_path2id[path] = f_id
135
self.old_id2parent[f_id] = parent_id
137
def get_changeset(self):
138
"""Create a changeset from the data contained within."""
139
from bzrlib.changeset import Changeset, ChangesetEntry, \
140
PatchApply, ReplaceContents
143
entry = ChangesetEntry(self.tree_root_id,
144
bzrlib.changeset.NULL_ID, './.')
145
cset.add_entry(entry)
146
for info, lines in self.actions:
147
parts = info.split(' ')
150
extra = ' '.join(parts[2:])
151
if action == 'renamed':
152
old_path, new_path = extra.split(' => ')
153
old_path = _unescape(old_path)
154
new_path = _unescape(new_path)
156
new_id = self.path2id[new_path]
157
old_id = self.old_path2id[old_path]
158
assert old_id == new_id
160
new_parent = self.id2parent[new_id]
161
old_parent = self.old_id2parent[old_id]
163
entry = ChangesetEntry(old_id, old_parent, old_path)
164
entry.new_path = new_path
165
entry.new_parent = new_parent
167
entry.contents_change = PatchApply(''.join(lines))
168
elif action == 'removed':
169
old_path = _unescape(extra)
170
old_id = self.old_path2id[old_path]
171
old_parent = self.old_id2parent[old_id]
172
entry = ChangesetEntry(old_id, old_parent, old_path)
173
entry.new_path = None
174
entry.new_parent = None
176
# Technically a removed should be a ReplaceContents()
177
# Where you need to have the old contents
178
# But at most we have a remove style patch.
179
#entry.contents_change = ReplaceContents()
181
elif action == 'added':
182
new_path = _unescape(extra)
183
new_id = self.path2id[new_path]
184
new_parent = self.id2parent[new_id]
185
entry = ChangesetEntry(new_id, new_parent, new_path)
189
# Technically an added should be a ReplaceContents()
190
# Where you need to have the old contents
191
# But at most we have an add style patch.
192
#entry.contents_change = ReplaceContents()
193
entry.contents_change = PatchApply(''.join(lines))
194
elif action == 'modified':
195
new_path = _unescape(extra)
196
new_id = self.path2id[new_path]
197
new_parent = self.id2parent[new_id]
198
entry = ChangesetEntry(new_id, new_parent, new_path)
202
# Technically an added should be a ReplaceContents()
203
# Where you need to have the old contents
204
# But at most we have an add style patch.
205
#entry.contents_change = ReplaceContents()
206
entry.contents_change = PatchApply(''.join(lines))
208
raise BadChangeset('Unrecognized action: %r' % action)
209
cset.add_entry(entry)
111
if rev.timestamp is None and self.timestamp is not None:
112
rev.timestamp = self.timestamp
113
rev.timezone = self.timezone
114
if rev.message is None and self.message:
115
rev.message = self.message
116
if rev.committer is None and self.committer:
117
rev.committer = self.committer
212
119
class ChangesetReader(object):
213
120
"""This class reads in a changeset from a file, and returns
404
320
can handle. That extra line is given here.
406
322
line = self._next().next()
407
if line != '# BEGIN BZR FOOTER\n':
408
raise MalformedFooter('Footer did not begin with BEGIN BZR FOOTER')
410
323
for line in self._next():
411
if line == '# END BZR FOOTER\n':
413
324
self._handle_next(line)
325
if self._next_line[:1] != '#':
328
def _update_tree(self, tree):
329
"""This fills out a ChangesetTree based on the information
332
:param tree: A ChangesetTree to update with the new information.
334
from bzrlib.errors import BzrError
335
from common import decode
337
def get_text_id(info, file_id):
339
if info[:8] != 'text-id:':
340
raise BzrError("Text ids should be prefixed with 'text-id:'"
342
text_id = decode(info[8:])
343
elif self.info.text_ids.has_key(file_id):
344
return self.info.text_ids[file_id]
346
# If text_id was not explicitly supplied
347
# then it should be whatever we would guess it to be
348
# based on the base revision, and what we know about
349
# the target revision
350
text_id = common.guess_text_id(tree.base_tree,
351
file_id, self.info.base, True)
352
if (self.info.text_ids.has_key(file_id)
353
and self.info.text_ids[file_id] != text_id):
354
raise BzrError('Mismatched text_ids for file_id {%s}'
355
': %s != %s' % (file_id,
356
self.info.text_ids[file_id],
358
# The Info object makes more sense for where
359
# to store something like text_id, since it is
360
# what will be used to generate stored inventory
362
# The problem is that we are parsing the
363
# ChangesetTree right now, we really modifying
364
# the ChangesetInfo object
365
self.info.text_ids[file_id] = text_id
368
def renamed(kind, extra, lines):
369
info = extra.split('\t')
371
raise BzrError('renamed action lines need both a from and to'
373
old_path = decode(info[0])
374
if info[1][:3] == '=> ':
375
new_path = decode(info[1][3:])
377
new_path = decode(info[1][3:])
379
file_id = tree.path2id(new_path)
381
text_id = get_text_id(info[2], file_id)
383
text_id = get_text_id(None, file_id)
384
tree.note_rename(old_path, new_path)
386
tree.note_patch(new_path, lines)
388
def removed(kind, extra, lines):
389
info = extra.split('\t')
391
# TODO: in the future we might allow file ids to be
392
# given for removed entries
393
raise BzrError('removed action lines should only have the path'
395
path = decode(info[0])
396
tree.note_deletion(path)
398
def added(kind, extra, lines):
399
info = extra.split('\t')
401
raise BzrError('add action lines require the path and file id'
404
raise BzrError('add action lines have fewer than 3 entries.'
406
path = decode(info[0])
407
if info[1][:8] == 'file-id:':
408
raise BzrError('The file-id should follow the path for an add'
410
file_id = decode(info[1][8:])
413
text_id = get_text_id(info[2], file_id)
415
text_id = get_text_id(None, file_id)
416
tree.note_id(file_id, path)
417
tree.note_patch(path, lines)
419
def modified(kind, extra, lines):
420
info = extra.split('\t')
422
raise BzrError('modified action lines have at least'
423
'the path in them: %r' % extra)
424
path = decode(info[0])
426
file_id = tree.path2id(path)
428
text_id = get_text_id(info[1], file_id)
430
text_id = get_text_id(None, file_id)
431
tree.note_patch(path, lines)
440
for action_line, lines in self.info.actions:
441
first = action_line.find(' ')
443
raise BzrError('Bogus action line'
444
' (no opening space): %r' % action_line)
445
second = action_line.find(' ', first)
447
raise BzrError('Bogus action line'
448
' (missing second space): %r' % action_line)
449
action = action_line[:first]
450
kind = action_line[first+1:second]
451
if kind not in ('file', 'directory'):
452
raise BzrError('Bogus action line'
453
' (invalid object kind): %r' % action_line)
454
extra = action_line[second+1:]
456
if action not in valid_actions:
457
raise BzrError('Bogus action line'
458
' (unrecognized action): %r' % action_line)
459
valid_actions[action](kind, extra, lines)
415
461
def read_changeset(from_file):
416
462
"""Read in a changeset from a filelike object (must have "readline" support), and
424
470
class ChangesetTree:
425
471
def __init__(self, base_tree=None):
426
472
self.base_tree = base_tree
473
self._renamed = {} # Mapping from old_path => new_path
474
self._renamed_r = {} # new_path => old_path
475
self._new_id = {} # new_path => new_id
476
self._new_id_r = {} # new_id => new_path
431
477
self.patches = {}
432
478
self.deleted = []
433
479
self.contents_by_id = True
482
return pprint.pformat(self.__dict__)
435
484
def note_rename(self, old_path, new_path):
485
"""A file/directory has been renamed from old_path => new_path"""
436
486
assert not self._renamed.has_key(old_path)
437
487
assert not self._renamed_r.has_key(new_path)
438
488
self._renamed[new_path] = old_path
439
489
self._renamed_r[old_path] = new_path
441
491
def note_id(self, new_id, new_path):
492
"""Files that don't exist in base need a new id."""
442
493
self._new_id[new_path] = new_id
443
494
self._new_id_r[new_id] = new_path
445
496
def note_patch(self, new_path, patch):
497
"""There is a patch for a given filename."""
446
498
self.patches[new_path] = patch
448
500
def note_deletion(self, old_path):
501
"""The file at old_path has been deleted."""
449
502
self.deleted.append(old_path)
451
504
def old_path(self, new_path):
505
"""Get the old_path (path in the base_tree) for the file at new_path"""
453
507
old_path = self._renamed.get(new_path)
454
508
if old_path is not None: