bzr branch
http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
| 757
by Martin Pool - add john's changeset plugin | 1 | #!/usr/bin/env python
 | 
| 2 | """\
 | |
| 3 | Just some work for generating a changeset.
 | |
| 4 | """
 | |
| 5 | ||
| 6 | import bzrlib, bzrlib.errors | |
| 7 | ||
| 8 | import common | |
| 9 | ||
| 10 | from bzrlib.inventory import ROOT_ID | |
| 11 | ||
| 12 | try: | |
| 13 |     set
 | |
| 14 | except NameError: | |
| 15 | from sets import Set as set | |
| 16 | ||
| 17 | def _canonicalize_revision(branch, revno): | |
| 18 | """Turn some sort of revision information into a single | |
| 19 |     set of from-to revision ids.
 | |
| 20 | ||
| 21 |     A revision id can be None if there is no associated revison.
 | |
| 22 | ||
| 23 |     :return: (old, new)
 | |
| 24 |     """
 | |
| 25 |     # This is a little clumsy because revision parsing may return
 | |
| 26 |     # a single entry, or a list
 | |
| 27 | if revno is None: | |
| 28 | new = branch.last_patch() | |
| 29 | else: | |
| 30 | new = branch.lookup_revision(revno) | |
| 31 | ||
| 32 | if new is None: | |
| 33 | raise BzrCommandError('Cannot generate a changset with no commits in tree.') | |
| 34 | ||
| 35 | old = branch.get_revision(new).precursor | |
| 36 | ||
| 37 | return old, new | |
| 38 | ||
| 39 | def _get_trees(branch, revisions): | |
| 40 | """Get the old and new trees based on revision. | |
| 41 |     """
 | |
| 42 | from bzrlib.tree import EmptyTree | |
| 43 | if revisions[0] is None: | |
| 44 | if hasattr(branch, 'get_root_id'): # Watch out for trees with labeled ROOT ids | |
| 45 | old_tree = EmptyTree(branch.get_root_id) | |
| 46 | else: | |
| 47 | old_tree = EmptyTree() | |
| 48 | else: | |
| 49 | old_tree = branch.revision_tree(revisions[0]) | |
| 50 | ||
| 51 | if revisions[1] is None: | |
| 52 |         # This is for the future, once we support rollup revisions
 | |
| 53 |         # Or working tree revisions
 | |
| 54 | new_tree = branch.working_tree() | |
| 55 | else: | |
| 56 | new_tree = branch.revision_tree(revisions[1]) | |
| 57 | return old_tree, new_tree | |
| 58 | ||
| 59 | def _fake_working_revision(branch): | |
| 60 | """Fake a Revision object for the working tree. | |
| 61 |     
 | |
| 62 |     This is for the future, to support changesets against the working tree.
 | |
| 63 |     """
 | |
| 64 | from bzrlib.revision import Revision | |
| 65 | import time | |
| 66 | from bzrlib.osutils import local_time_offset, \ | |
| 67 |             username
 | |
| 68 | ||
| 69 | precursor = branch.last_patch() | |
| 70 | precursor_sha1 = branch.get_revision_sha1(precursor) | |
| 71 | ||
| 72 | return Revision(timestamp=time.time(), | |
| 73 | timezone=local_time_offset(), | |
| 74 | committer=username(), | |
| 75 | precursor=precursor, | |
| 76 | precursor_sha1=precursor_sha1) | |
| 77 | ||
| 78 | ||
| 79 | class MetaInfoHeader(object): | |
| 80 | """Maintain all of the header information about this | |
| 81 |     changeset.
 | |
| 82 |     """
 | |
| 83 | ||
| 84 | def __init__(self, branch, revisions, delta, | |
| 85 | full_remove=True, full_rename=False, | |
| 86 | external_diff_options = None, | |
| 87 | new_tree=None, old_tree=None, | |
| 88 | old_label = '', new_label = ''): | |
| 89 | """ | |
| 90 |         :param full_remove: Include the full-text for a delete
 | |
| 91 |         :param full_rename: Include an add+delete patch for a rename
 | |
| 92 |         """
 | |
| 93 | self.branch = branch | |
| 94 | self.delta = delta | |
| 95 | self.full_remove=full_remove | |
| 96 | self.full_rename=full_rename | |
| 97 | self.external_diff_options = external_diff_options | |
| 98 | self.old_label = old_label | |
| 99 | self.new_label = new_label | |
| 100 | self.old_tree = old_tree | |
| 101 | self.new_tree = new_tree | |
| 102 | self.to_file = None | |
| 103 | self.revno = None | |
| 104 | self.precursor_revno = None | |
| 105 | ||
| 106 | self._get_revision_list(revisions) | |
| 107 | ||
| 108 | def _get_revision_list(self, revisions): | |
| 109 | """This generates the list of all revisions from->to. | |
| 110 | ||
| 111 |         This is for the future, when we support having a rollup changeset.
 | |
| 112 |         For now, the list should only be one long.
 | |
| 113 |         """
 | |
| 114 | old_revno = None | |
| 115 | new_revno = None | |
| 116 | rh = self.branch.revision_history() | |
| 117 | for revno, rev in enumerate(rh): | |
| 118 | if rev == revisions[0]: | |
| 119 | old_revno = revno | |
| 120 | if rev == revisions[1]: | |
| 121 | new_revno = revno | |
| 122 | ||
| 123 | self.revision_list = [] | |
| 124 | if old_revno is None: | |
| 125 | self.base_revision = None # Effectively the EmptyTree() | |
| 126 | old_revno = -1 | |
| 127 | else: | |
| 128 | self.base_revision = self.branch.get_revision(rh[old_revno]) | |
| 129 | if new_revno is None: | |
| 130 |             # For the future, when we support working tree changesets.
 | |
| 131 | for rev_id in rh[old_revno+1:]: | |
| 132 | self.revision_list.append(self.branch.get_revision(rev_id)) | |
| 133 | self.revision_list.append(_fake_working_revision(self.branch)) | |
| 134 | else: | |
| 135 | for rev_id in rh[old_revno+1:new_revno+1]: | |
| 136 | self.revision_list.append(self.branch.get_revision(rev_id)) | |
| 137 | self.precursor_revno = old_revno | |
| 138 | self.revno = new_revno | |
| 139 | ||
| 140 | def _write(self, txt, key=None): | |
| 141 | if key: | |
| 142 | self.to_file.write('# %s: %s\n' % (key, txt)) | |
| 143 | else: | |
| 144 | self.to_file.write('# %s\n' % (txt,)) | |
| 145 | ||
| 146 | def write_meta_info(self, to_file): | |
| 147 | """Write out the meta-info portion to the supplied file. | |
| 148 | ||
| 149 |         :param to_file: Write out the meta information to the supplied
 | |
| 150 |                         file
 | |
| 151 |         """
 | |
| 152 | self.to_file = to_file | |
| 153 | ||
| 154 | self._write_header() | |
| 155 | self._write_diffs() | |
| 156 | self._write_footer() | |
| 157 | ||
| 158 | def _write_header(self): | |
| 159 | """Write the stuff that comes before the patches.""" | |
| 160 | from bzrlib.osutils import username, format_date | |
| 161 | write = self._write | |
| 162 | ||
| 163 | for line in common.get_header(): | |
| 164 | write(line) | |
| 165 | ||
| 166 |         # This grabs the current username, what we really want is the
 | |
| 167 |         # username from the actual patches.
 | |
| 168 |         #write(username(), key='committer')
 | |
| 169 | assert len(self.revision_list) == 1 | |
| 170 | rev = self.revision_list[0] | |
| 171 | write(rev.committer, key='committer') | |
| 172 | write(format_date(rev.timestamp, offset=rev.timezone), key='date') | |
| 173 | write(str(self.revno), key='revno') | |
| 174 | if rev.message: | |
| 175 | self.to_file.write('# message:\n') | |
| 176 | for line in rev.message.split('\n'): | |
| 177 | self.to_file.write('# %s\n' % line) | |
| 178 | write(rev.revision_id, key='revision') | |
| 179 | ||
| 180 | if self.base_revision: | |
| 181 | write(self.base_revision.revision_id, key='precursor') | |
| 182 | write(str(self.precursor_revno), key='precursor revno') | |
| 183 | ||
| 184 | ||
| 185 | write('') | |
| 186 | self.to_file.write('\n') | |
| 187 | ||
| 188 | def _write_footer(self): | |
| 189 | """Write the stuff that comes after the patches. | |
| 190 | ||
| 191 |         This is meant to be more meta-information, which people probably don't want
 | |
| 192 |         to read, but which is required for proper bzr operation.
 | |
| 193 |         """
 | |
| 194 | write = self._write | |
| 195 | ||
| 196 | write('BEGIN BZR FOOTER') | |
| 197 | ||
| 198 | assert len(self.revision_list) == 1 # We only handle single revision entries | |
| 199 | rev = self.revision_list[0] | |
| 200 | write(self.branch.get_revision_sha1(rev.revision_id), | |
| 201 | key='revision sha1') | |
| 202 | if self.base_revision: | |
| 203 | rev_id = self.base_revision.revision_id | |
| 204 | write(self.branch.get_revision_sha1(rev_id), | |
| 205 | key='precursor sha1') | |
| 206 | ||
| 207 | write('%.9f' % rev.timestamp, key='timestamp') | |
| 208 | write(str(rev.timezone), key='timezone') | |
| 209 | ||
| 210 | self._write_ids() | |
| 211 | ||
| 212 | write('END BZR FOOTER') | |
| 213 | ||
| 214 | def _write_revisions(self): | |
| 215 | """Not used. Used for writing multiple revisions.""" | |
| 216 | first = True | |
| 217 | for rev in self.revision_list: | |
| 218 | if rev.revision_id is not None: | |
| 219 | if first: | |
| 220 | self._write('revisions:') | |
| 221 | first = False | |
| 222 | self._write(' '*4 + rev.revision_id + '\t' + self.branch.get_revision_sha1(rev.revision_id)) | |
| 223 | ||
| 224 | ||
| 225 | def _write_ids(self): | |
| 226 | if hasattr(self.branch, 'get_root_id'): | |
| 227 | root_id = self.branch.get_root_id() | |
| 228 | else: | |
| 229 | root_id = ROOT_ID | |
| 230 | ||
| 231 | old_ids = set() | |
| 232 | new_ids = set() | |
| 233 | ||
| 234 | for path, file_id, kind in self.delta.removed: | |
| 235 | old_ids.add(file_id) | |
| 236 | for path, file_id, kind in self.delta.added: | |
| 237 | new_ids.add(file_id) | |
| 238 | for old_path, new_path, file_id, kind, text_modified in self.delta.renamed: | |
| 239 | old_ids.add(file_id) | |
| 240 | new_ids.add(file_id) | |
| 241 | for path, file_id, kind in self.delta.modified: | |
| 242 | new_ids.add(file_id) | |
| 243 | ||
| 244 | self._write(root_id, key='tree root id') | |
| 245 | ||
| 246 | def write_ids(tree, id_set, name): | |
| 247 | if len(id_set) > 0: | |
| 248 | self.to_file.write('# %s ids:\n' % name) | |
| 249 | seen_ids = set([root_id]) | |
| 250 | while len(id_set) > 0: | |
| 251 | file_id = id_set.pop() | |
| 252 | if file_id in seen_ids: | |
| 253 |                     continue
 | |
| 254 | seen_ids.add(file_id) | |
| 255 | ie = tree.inventory[file_id] | |
| 256 | if ie.parent_id not in seen_ids: | |
| 257 | id_set.add(ie.parent_id) | |
| 258 | path = tree.inventory.id2path(file_id) | |
| 259 | self.to_file.write('# %s\t%s\t%s\n' | |
| 260 | % (path.encode('utf8'), file_id.encode('utf8'), | |
| 261 | ie.parent_id.encode('utf8'))) | |
| 262 | write_ids(self.new_tree, new_ids, 'file') | |
| 263 | write_ids(self.old_tree, old_ids, 'old file') | |
| 264 | ||
| 265 | def _write_diffs(self): | |
| 266 | """Write out the specific diffs""" | |
| 267 | from bzrlib.diff import internal_diff, external_diff | |
| 268 | DEVNULL = '/dev/null' | |
| 269 | ||
| 270 | if self.external_diff_options: | |
| 271 | assert isinstance(self.external_diff_options, basestring) | |
| 272 | opts = self.external_diff_options.split() | |
| 273 | def diff_file(olab, olines, nlab, nlines, to_file): | |
| 274 | external_diff(olab, olines, nlab, nlines, to_file, opts) | |
| 275 | else: | |
| 276 | diff_file = internal_diff | |
| 277 | ||
| 278 | for path, file_id, kind in self.delta.removed: | |
| 279 | print >>self.to_file, '*** removed %s %r' % (kind, path) | |
| 280 | if kind == 'file' and self.full_remove: | |
| 281 | diff_file(self.old_label + path, | |
| 282 | self.old_tree.get_file(file_id).readlines(), | |
| 283 | DEVNULL, | |
| 284 |                           [],
 | |
| 285 | self.to_file) | |
| 286 | ||
| 287 | for path, file_id, kind in self.delta.added: | |
| 288 | print >>self.to_file, '*** added %s %r' % (kind, path) | |
| 289 | if kind == 'file': | |
| 290 | diff_file(DEVNULL, | |
| 291 |                           [],
 | |
| 292 | self.new_label + path, | |
| 293 | self.new_tree.get_file(file_id).readlines(), | |
| 294 | self.to_file) | |
| 295 | ||
| 296 | for old_path, new_path, file_id, kind, text_modified in self.delta.renamed: | |
| 297 | print >>self.to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path) | |
| 298 | if self.full_rename and kind == 'file': | |
| 299 | diff_file(self.old_label + old_path, | |
| 300 | self.old_tree.get_file(file_id).readlines(), | |
| 301 | DEVNULL, | |
| 302 |                           [],
 | |
| 303 | self.to_file) | |
| 304 | diff_file(DEVNULL, | |
| 305 |                           [],
 | |
| 306 | self.new_label + new_path, | |
| 307 | self.new_tree.get_file(file_id).readlines(), | |
| 308 | self.to_file) | |
| 309 | elif text_modified: | |
| 310 | diff_file(self.old_label + old_path, | |
| 311 | self.old_tree.get_file(file_id).readlines(), | |
| 312 | self.new_label + new_path, | |
| 313 | self.new_tree.get_file(file_id).readlines(), | |
| 314 | self.to_file) | |
| 315 | ||
| 316 | for path, file_id, kind in self.delta.modified: | |
| 317 | print >>self.to_file, '*** modified %s %r' % (kind, path) | |
| 318 | if kind == 'file': | |
| 319 | diff_file(self.old_label + path, | |
| 320 | self.old_tree.get_file(file_id).readlines(), | |
| 321 | self.new_label + path, | |
| 322 | self.new_tree.get_file(file_id).readlines(), | |
| 323 | self.to_file) | |
| 324 | ||
| 325 | def show_changeset(branch, revision=None, specific_files=None, | |
| 326 | external_diff_options=None, to_file=None, | |
| 327 | include_full_diff=False): | |
| 328 | from bzrlib.diff import compare_trees | |
| 329 | ||
| 330 | if to_file is None: | |
| 331 | import sys | |
| 332 | to_file = sys.stdout | |
| 333 | revisions = _canonicalize_revision(branch, revision) | |
| 334 | ||
| 335 | old_tree, new_tree = _get_trees(branch, revisions) | |
| 336 | ||
| 337 | delta = compare_trees(old_tree, new_tree, want_unchanged=False, | |
| 338 | specific_files=specific_files) | |
| 339 | ||
| 340 | meta = MetaInfoHeader(branch, revisions, delta, | |
| 341 | external_diff_options=external_diff_options, | |
| 342 | old_tree=old_tree, new_tree=new_tree) | |
| 343 | meta.write_meta_info(to_file) | |
| 344 | ||
| 345 |